sunnypilot v2026.003.000 release

date: 2026-08-19T09:43:43
master commit: ba29a38507
This commit is contained in:
github-actions[bot]
2026-08-19 09:43:44 +00:00
commit e01ac7f80f
4046 changed files with 997234 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
brew "git-lfs"
brew "capnp"
brew "coreutils"
brew "eigen"
brew "ffmpeg"
brew "glfw"
brew "libusb"
brew "libtool"
brew "llvm"
brew "openssl@3.0"
brew "qt@5"
brew "zeromq"
cask "gcc-arm-embedded"
brew "portaudio"
brew "gcc@13"
+20
View File
@@ -0,0 +1,20 @@
## CTF
Welcome to the first part of the comma CTF!
* all the flags are contained in this route: `0c7f0c7f0c7f0c7f|2021-10-13--13-00-00`
* there's 2 flags in each segment, with roughly increasing difficulty
* everything you'll need to find the flags is in the openpilot repo
* grep is also your friend
* first, [setup](https://github.com/commaai/openpilot/tree/master/tools) your PC
* read the docs & checkout out the tools in openpilot/tools/
* tip: once you get the replay and UI up, start by familiarizing yourself with seeking in replay
getting started
```bash
# start the route replay
cd openpilot/tools/replay
./replay '0c7f0c7f0c7f0c7f|2021-10-13--13-00-00' --dcam --ecam
# start the UI in another terminal
openpilot/selfdrive/ui/ui
```
+64
View File
@@ -0,0 +1,64 @@
# openpilot tools
## System Requirements
openpilot is developed and tested on **Ubuntu 24.04**, which is the primary development target aside from the [supported embedded hardware](https://github.com/commaai/openpilot#running-on-a-dedicated-device-in-a-car).
Most of openpilot should work natively on macOS. On Windows you can use WSL for a nearly native Ubuntu experience. Running natively on any other system is not currently recommended and will likely require modifications.
## Native setup on Ubuntu 24.04 and macOS
Follow these instructions for a fully managed setup experience. If you'd like to manage the dependencies yourself, just read the setup scripts in this directory.
**1. Clone openpilot**
``` bash
git clone https://github.com/commaai/openpilot.git
```
**2. Run the setup script**
``` bash
cd openpilot
tools/op.sh setup
```
**3. Activate a Python shell**
Activate a shell with the Python dependencies installed:
``` bash
source .venv/bin/activate
```
**4. Build openpilot**
``` bash
scons -u
```
## WSL on Windows
[Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/about) should provide a similar experience to native Ubuntu. [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/compare-versions) specifically has been reported by several users to be a seamless experience.
Follow [these instructions](https://docs.microsoft.com/en-us/windows/wsl/install) to setup the WSL and install the `Ubuntu-24.04` distribution. Once your Ubuntu WSL environment is setup, follow the Linux setup instructions to finish setting up your environment. See [these instructions](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) for running GUI apps.
**NOTE**: If you are running WSL 2 and experiencing performance issues with the UI or simulator, you may need to explicitly enable hardware acceleration by setting `GALLIUM_DRIVER=d3d12` before commands. Add `export GALLIUM_DRIVER=d3d12` to your `~/.bashrc` file to make it automatic for future sessions.
## CTF
Learn about the openpilot ecosystem and tools by playing our [CTF](/tools/CTF.md).
## Directory Structure
```
├── car_porting/ # Tools for porting new cars
├── release/ # Scripts for building openpilot releases
└── scripts/ # Miscellaneous scripts
```
Development tools such as cabana, plotjuggler, and replay live in [openpilot/tools/](/openpilot/tools/):
```
├── cabana/ # View and plot CAN messages from drives or in realtime
├── camerastream/ # Cameras stream over the network
├── joystick/ # Control your car with a joystick
├── lib/ # Libraries to support the tools and reading openpilot logs
├── plotjuggler/ # A tool to plot openpilot logs
├── replay/ # Replay drives and mock openpilot services
└── sim/ # Run openpilot in a simulator
```
+129
View File
@@ -0,0 +1,129 @@
# tools/car_porting
Check out [this blog post](https://blog.comma.ai/how-to-write-a-car-port-for-openpilot/) for a high-level overview of porting a car.
## Useful car porting utilities
Testing car ports in your car is very time-consuming. Check out these utilities to do basic checks on your work before running it in your car.
### [Cabana](/openpilot/tools/cabana/README.md)
View your car's CAN signals through DBC files, which openpilot uses to parse and create messages that talk to the car.
Example:
```bash
> openpilot/tools/cabana/cabana '1bbe6bf2d62f58a8|2022-07-14--17-11-43'
```
### [tools/car_porting/auto_fingerprint.py](/tools/car_porting/auto_fingerprint.py)
Given a route and platform, automatically inserts FW fingerprints from the platform into the correct place in fingerprints.py
Example:
```bash
> python3 tools/car_porting/auto_fingerprint.py '1bbe6bf2d62f58a8|2022-07-14--17-11-43' 'OUTBACK'
Attempting to add fw version for: OUTBACK
```
### [openpilot/selfdrive/car/tests/test_car_interfaces.py](/openpilot/selfdrive/car/tests/test_car_interfaces.py)
Finds common bugs for car interfaces, without even requiring a route.
#### Example: Typo in signal name
```bash
> tools/test_runner.py openpilot/selfdrive/car/tests/test_car_interfaces.py -k subaru # replace with the brand you are working on
=====================================================================
FAILED openpilot/selfdrive/car/tests/test_car_interfaces.py::TestCarInterfaces::test_car_interfaces_165_SUBARU_LEGACY_7TH_GEN - KeyError: 'CruiseControlOOPS'
```
### [tools/car_porting/test_car_model.py](/tools/car_porting/test_car_model.py)
Given a route, runs most of the car interface to check for common errors like missing signals, blocked panda messages, and safety mismatches.
#### Example: panda safety mismatch for gasPressed
```bash
> python3 tools/car_porting/test_car_model.py '4822a427b188122a|2023-08-14--16-22-21'
=====================================================================
FAIL: test_panda_safety_carstate (__main__.CarModelTestCase.test_panda_safety_carstate)
Assert that panda safety matches openpilot's carState
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/batman/openpilot/opendbc_repo/opendbc/car/tests/test_models.py", line 440, in test_panda_safety_carstate
self.assertFalse(failed_checks, f"panda safety doesn't agree with CarState: {failed_checks}")
AssertionError: {'gasPressed': 116} is not false : panda safety doesn't agree with CarState: {'gasPressed': 116}
```
## Jupyter notebooks
To use these notebooks, install Jupyter within your [openpilot virtual environment](/tools/README.md).
```bash
uv pip install jupyter ipykernel
```
Launching:
```bash
jupyter notebook
```
### [examples/subaru_steer_temp_fault.ipynb](/tools/car_porting/examples/subaru_steer_temp_fault.ipynb)
An example of searching through a database of segments for a specific condition, and plotting the results.
![steer warning example](https://github.com/commaai/openpilot/assets/9648890/d60ad120-4b44-4974-ac79-adc660fb8fe2)
*a plot of the steer_warning vs steering angle, where we can see it is clearly caused by a large steering angle change*
### [examples/subaru_long_accel.ipynb](/tools/car_porting/examples/subaru_long_accel.ipynb)
An example of plotting the response of an actuator when it is active.
![brake pressure example](https://github.com/commaai/openpilot/assets/9648890/8f32cf1d-8fc0-4407-b540-70625ebbf082)
*a plot of the brake_pressure vs acceleration, where we can see it is a fairly linear response.*
### [examples/ford_vin_fingerprint.ipynb](/tools/car_porting/examples/ford_vin_fingerprint.ipynb)
In this example, we use the public comma car segments database to check if vin fingerprinting is feasible for ford.
```
vin: 1FM5K8GC7LGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: 00000000000XXXXXX real platform: FORD ESCAPE 4TH GEN determined platform: mock correct: False
vin: 3FTTW8F98NRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False
vin: 1FTVW1EL4NWXXXXXX real platform: FORD F-150 LIGHTNING 1ST GEN determined platform: FORD F-150 LIGHTNING 1ST GEN correct: True
vin: 1FM5K7LC0MGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: WF0NXXGCHNJXXXXXX real platform: FORD FOCUS 4TH GEN determined platform: mock correct: False
vin: 1FMCU9J94MUXXXXXX real platform: FORD ESCAPE 4TH GEN determined platform: mock correct: False
vin: 5LM5J7XC9LGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: 3FMCR9B69NRXXXXXX real platform: FORD BRONCO SPORT 1ST GEN determined platform: mock correct: False
vin: 3FMTK3SU0MMXXXXXX real platform: FORD MUSTANG MACH-E 1ST GEN determined platform: FORD MUSTANG MACH-E 1ST GEN correct: True
vin: 1FM5K8HC7MGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: 1FM5K8GC7NGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: 5LM5J7XC8MGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False
vin: 3FTTW8E31PRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False
vin: 3FTTW8E99NRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False
```
### [examples/find_segments_with_message.ipynb](/tools/car_porting/examples/find_segments_with_message.ipynb)
Searches for segments where a set of given CAN message IDs are present. In the example, we search for all messages
used for CAN-based ignition detection.
```
Match found: 46b21f1c5f7aa885/2024-01-23--15-19-34/20/s JEEP GRAND CHEROKEE V6 2018 ['VW CAN Ign']
Match found: a63a23c3e628f288/2023-11-05--18-36-20/8/s JEEP GRAND CHEROKEE V6 2018 ['VW CAN Ign']
Match found: ce31b7a998781ba8/2024-01-19--07-05-29/23/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']
Match found: e1dfba62a4e33f7b/2023-12-25--19-31-00/4/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']
Match found: e1dfba62a4e33f7b/2024-01-10--14-33-57/2/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']
Match found: ae679616266f4096/2023-12-05--15-43-46/4/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']
Match found: ae679616266f4096/2023-11-18--17-49-42/3/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']
Match found: ae679616266f4096/2024-01-03--21-57-09/25/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']
Match found: 6dae2984cc53cd7f/2023-12-10--11-53-15/17/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']
Match found: 6dae2984cc53cd7f/2023-12-03--17-31-17/29/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']
Match found: 6dae2984cc53cd7f/2023-11-27--23-29-07/1/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']
```
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
from collections import defaultdict
from opendbc.car.debug.format_fingerprints import format_brand_fw_versions
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.fw_versions import MODEL_TO_BRAND, match_fw_to_car
from openpilot.tools.lib.logreader import LogReader, ReadMode
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="?")
args = parser.parse_args()
lr = LogReader(args.route, ReadMode.QLOG)
CP = lr.first("carParams")
assert CP is not None, "No carParams in route"
carPlatform = MIGRATION.get(CP.carFingerprint, CP.carFingerprint)
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]
print("Attempting to add fw version for:", platform)
fw_versions: dict[str, dict[tuple, list[bytes]]] = defaultdict(lambda: defaultdict(list))
brand = MODEL_TO_BRAND[platform]
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
key = (fw.ecu.raw, addr, subAddr)
fw_versions[platform][key].append(fw.fwVersion)
format_brand_fw_versions(brand, fw_versions)
@@ -0,0 +1,231 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 85,
"id": "facb8edc-9924-491a-a4dd-fe6135b0c6c4",
"metadata": {},
"outputs": [],
"source": [
"# Import all cars from opendbc\n",
"\n",
"from opendbc.car.values import PLATFORMS as TEST_PLATFORMS\n",
"\n",
"# Example: add additional platforms/segments to test outside of commaCarSegments\n",
"\n",
"EXTRA_SEGMENTS = {\n",
" # \"81dd9e9fe256c397/0000001f--97c42cf98d\", # Volkswagen ID.4 test route, new car port, not in public dataset\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 86,
"id": "ed1c8aec-c274-4c61-b83d-711ea194bf86",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Searching 221 platforms\n",
"No segments available for DODGE_DURANGO\n",
"No segments available for FORD_RANGER_MK2\n",
"No segments available for HOLDEN_ASTRA\n",
"No segments available for CADILLAC_ATS\n",
"No segments available for CHEVROLET_MALIBU\n",
"No segments available for CADILLAC_XT4\n",
"No segments available for CHEVROLET_VOLT_2019\n",
"No segments available for CHEVROLET_TRAVERSE\n",
"No segments available for GMC_YUKON\n",
"No segments available for HONDA_ODYSSEY_CHN\n",
"No segments available for HYUNDAI_KONA_2022\n",
"No segments available for HYUNDAI_NEXO_1ST_GEN\n",
"No segments available for GENESIS_GV70_ELECTRIFIED_1ST_GEN\n",
"No segments available for GENESIS_G80_2ND_GEN_FL\n",
"No segments available for RIVIAN_R1_GEN1\n",
"No segments available for SUBARU_FORESTER_HYBRID\n",
"No segments available for TESLA_MODEL_3\n",
"No segments available for TESLA_MODEL_Y\n",
"No segments available for TOYOTA_RAV4_PRIME\n",
"No segments available for TOYOTA_SIENNA_4TH_GEN\n",
"No segments available for LEXUS_LC_TSS2\n",
"No segments available for VOLKSWAGEN_CADDY_MK3\n",
"No segments available for VOLKSWAGEN_CRAFTER_MK2\n",
"No segments available for VOLKSWAGEN_JETTA_MK6\n",
"Searching 577 segments\n"
]
}
],
"source": [
"import random\n",
"\n",
"from openpilot.tools.lib.logreader import LogReader\n",
"from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database\n",
"\n",
"\n",
"MAX_SEGS_PER_PLATFORM = 3 # Increase this to search more segments\n",
"\n",
"database = get_comma_car_segments_database()\n",
"TEST_SEGMENTS = []\n",
"\n",
"print(f\"Searching {len(TEST_PLATFORMS)} platforms\")\n",
"\n",
"for platform in TEST_PLATFORMS:\n",
" if platform not in database:\n",
" print(f\"No segments available for {platform}\")\n",
" continue\n",
"\n",
" all_segments = database[platform]\n",
" NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n",
" TEST_SEGMENTS.extend(random.sample(all_segments, NUM_SEGMENTS))\n",
"\n",
"TEST_SEGMENTS.extend(EXTRA_SEGMENTS)\n",
"\n",
"print(f\"Searching {len(TEST_SEGMENTS)} segments\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c75e8f2-4f5f-4f89-b8db-5223a6534a9f",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "27a243c33de44498b2b946190df44b23",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"segments searched: 0%| | 0/577 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Match found: 0f53b336851e1384/2023-11-20--09-44-03/12/s CHRYSLER PACIFICA HYBRID 2018 ['VW CAN Ign']\n",
"Match found: 7620ad20d3cefc64/2023-10-28--08-14-40/3/s CHRYSLER PACIFICA HYBRID 2018 ['VW CAN Ign']\n",
"Match found: 00d247a9bb1f9196/2023-11-06--13-33-17/9/s CHRYSLER PACIFICA HYBRID 2018 ['VW CAN Ign']\n",
"Match found: 120a432f63cb0de2/2023-10-30--20-01-34/1/s CHRYSLER PACIFICA HYBRID 2019 ['VW CAN Ign']\n",
"Match found: b70b56b76a6217f2/2023-12-19--08-30-22/35/s CHRYSLER PACIFICA HYBRID 2019 ['VW CAN Ign']\n",
"Match found: 97e388680a6716ed/2024-01-17--10-15-13/9/s CHRYSLER PACIFICA HYBRID 2019 ['VW CAN Ign']\n",
"Match found: 2137b01aa0ca63f9/2024-01-06--22-06-14/70/s CHRYSLER PACIFICA 2018 ['VW CAN Ign']\n",
"Match found: 8fc6a1b72c8b1357/2023-11-06--07-50-05/8/s CHRYSLER PACIFICA 2018 ['VW CAN Ign']\n",
"Match found: 7e705eb5c27a49cc/2024-01-18--16-51-20/3/s CHRYSLER PACIFICA 2018 ['VW CAN Ign']\n",
"Match found: 12208e5acdc97eb3/2024-01-20--14-46-24/12/s CHRYSLER PACIFICA 2020 ['VW CAN Ign']\n",
"Match found: 12208e5acdc97eb3/2023-11-30--12-01-09/2/s CHRYSLER PACIFICA 2020 ['VW CAN Ign']\n",
"Match found: 9cad19e0efce3650/2024-01-26--10-24-52/27/s CHRYSLER PACIFICA 2020 ['VW CAN Ign']\n",
"Match found: 9db428338427dec2/2023-11-05--18-40-09/21/s JEEP GRAND CHEROKEE V6 2018 ['VW CAN Ign']\n",
"Match found: d50ada8ee55a5e74/2023-12-11--13-38-09/0/s JEEP GRAND CHEROKEE V6 2018 ['VW CAN Ign']\n",
"Match found: 900dfa83b4addfe6/2023-12-30--19-20-08/28/s JEEP GRAND CHEROKEE V6 2018 ['VW CAN Ign']\n",
"Match found: 20acda0eb23d7f23/2024-01-19--17-33-26/41/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']\n",
"Match found: 1cc3b46843cad2ca/2024-01-10--20-20-54/24/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']\n",
"Match found: 2d9b6425552c52c1/2023-12-07--10-31-46/22/s JEEP GRAND CHEROKEE 2019 ['VW CAN Ign']\n",
"Match found: ae679616266f4096/2023-12-04--13-13-56/16/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']\n",
"Match found: ae679616266f4096/2024-01-08--07-58-12/65/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']\n",
"Match found: ae679616266f4096/2023-12-05--15-43-46/25/s RAM HD 5TH GEN ['Tesla 3/Y CAN Ign']\n",
"Match found: 6dae2984cc53cd7f/2024-01-09--21-41-11/4/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']\n",
"Match found: 440a155809ba2b6d/2023-12-30--08-51-53/2/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']\n",
"Match found: 6dae2984cc53cd7f/2024-01-06--10-11-07/1/s FORD BRONCO SPORT 1ST GEN ['Rivian CAN Ign']\n",
"Match found: a4218e6416dfd978/2023-11-27--13-48-46/19/s FORD ESCAPE 4TH GEN ['Rivian CAN Ign']\n",
"Match found: a4218e6416dfd978/2023-11-10--14-13-14/0/s FORD ESCAPE 4TH GEN ['Rivian CAN Ign']\n",
"Match found: a4218e6416dfd978/2023-11-27--13-48-46/4/s FORD ESCAPE 4TH GEN ['Rivian CAN Ign']\n",
"Match found: 8a732841c3a8d5ef/2023-12-10--19-02-33/3/s FORD EXPLORER 6TH GEN ['Rivian CAN Ign']\n",
"Match found: 0b91b433b9332780/2023-12-28--14-02-49/4/s FORD EXPLORER 6TH GEN ['Rivian CAN Ign']\n",
"Match found: 8a732841c3a8d5ef/2023-11-09--07-28-12/1/s FORD EXPLORER 6TH GEN ['Rivian CAN Ign']\n",
"Match found: e886087f430e7fe7/2023-11-05--19-59-40/59/s FORD FOCUS 4TH GEN ['Rivian CAN Ign']\n",
"Match found: e886087f430e7fe7/2023-11-05--19-59-40/82/s FORD FOCUS 4TH GEN ['Rivian CAN Ign']\n",
"Match found: e886087f430e7fe7/2023-11-05--19-59-40/106/s FORD FOCUS 4TH GEN ['Rivian CAN Ign']\n"
]
}
],
"source": [
"from openpilot.tools.lib.logreader import comma_car_segments_source\n",
"from tqdm.notebook import tnrange\n",
"\n",
"# Example search for CAN ignition messages\n",
"# Be careful when filtering by bus, account for odd harness arrangements on Honda/HKG\n",
"\n",
"BUSES_TO_SEARCH = [0, 1, 2]\n",
"\n",
"# Support for external Red Panda\n",
"EXTERNAL_PANDA_BUSES = [bus + 4 for bus in BUSES_TO_SEARCH]\n",
"\n",
"MESSAGES_TO_FIND = {\n",
" 0x1F1: \"GM CAN Ign\",\n",
" 0x152: \"Rivian CAN Ign\",\n",
" 0x221: \"Tesla 3/Y CAN Ign\",\n",
" 0x9E: \"Mazda CAN Ign\",\n",
" 0x3C0: \"VW CAN Ign\",\n",
"}\n",
"\n",
"progress_bar = tnrange(len(TEST_SEGMENTS), desc=\"segments searched\")\n",
"\n",
"for segment in TEST_SEGMENTS:\n",
" lr = LogReader(segment, sources=[comma_car_segments_source])\n",
" CP = lr.first(\"carParams\")\n",
" if CP is None:\n",
" progress_bar.update()\n",
" continue\n",
"\n",
" can_packets = [msg for msg in lr if msg.which() == \"can\"]\n",
" matched_messages = set()\n",
"\n",
" for packet in can_packets:\n",
" for msg in packet.can:\n",
" if msg.address in MESSAGES_TO_FIND and msg.src in (BUSES_TO_SEARCH + EXTERNAL_PANDA_BUSES):\n",
" # print(msg)\n",
" matched_messages.add(msg.address)\n",
"\n",
" if len(matched_messages) > 0:\n",
" message_names = [MESSAGES_TO_FIND[message] for message in matched_messages]\n",
" print(f\"Match found: {segment:<45} {CP.carFingerprint:<38} {message_names}\")\n",
"\n",
" progress_bar.update()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7724dd97-f62e-4fd3-9f64-63d49be669d2",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "9f393e00-8efd-40fb-a41e-d312531a83e8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,176 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"jupyter": {
"is_executing": true
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Got 9 Ford cars from opendbc\n"
]
}
],
"source": [
"\"\"\"In this example, we use the public comma car segments database to check if vin fingerprinting is feasible for ford.\"\"\"\n",
"\n",
"from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n",
"from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database\n",
"from opendbc.car.ford.values import CAR\n",
"\n",
"database = get_comma_car_segments_database()\n",
"\n",
"platforms = [c.value for c in CAR]\n",
"print(f\"Got {len(platforms)} Ford cars from opendbc\")"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"# Adapted from https://github.com/commaai/openpilot/issues/31052#issuecomment-1902690083\n",
"\n",
"MODEL_YEAR_CODES = {'M': 2021, 'N': 2022, 'P': 2023, 'R': 2024, 'S': 2025}\n",
"\n",
"\n",
"F150_CODES = ['F1C', 'F1E', 'W1C', 'W1E', 'X1C', 'X1E', 'W1R', 'W1P', 'W1S', 'W1T']\n",
"LIGHTNING_CODES = ['L', 'V']\n",
"MACHE_CODES = ['K1R', 'K1S', 'K2S', 'K3R', 'K3S', 'K4S']\n",
"\n",
"FORD_VIN_START = ['1FT', '3FM', '5LM']\n",
"\n",
"def ford_vin_fingerprint(vin): # Check if it's a Ford vehicle and determine the model\n",
" vin_positions_567 = vin[4:7]\n",
"\n",
" if vin.startswith('1FT'):\n",
" if vin_positions_567 in F150_CODES:\n",
" if vin[7] in LIGHTNING_CODES:\n",
" return \"FORD F-150 LIGHTNING 1ST GEN\"\n",
" else:\n",
" return \"FORD F-150 14TH GEN\"\n",
" elif vin.startswith('3FM'):\n",
" if vin_positions_567 in MACHE_CODES:\n",
" return \"FORD MUSTANG MACH-E 1ST GEN\"\n",
" elif vin.startswith('5LM'):\n",
" pass\n",
"\n",
" return \"mock\""
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Collecting segments from commaCarSegments dataset:\n",
"Got 287 segments for platform FORD_BRONCO_SPORT_MK1, sampling 5 segments\n",
"Got 137 segments for platform FORD_ESCAPE_MK4, sampling 5 segments\n",
"Got 1041 segments for platform FORD_EXPLORER_MK6, sampling 5 segments\n",
"Got 5 segments for platform FORD_F_150_MK14, sampling 5 segments\n",
"Got 3 segments for platform FORD_F_150_LIGHTNING_MK1, sampling 3 segments\n",
"Got 56 segments for platform FORD_FOCUS_MK4, sampling 5 segments\n",
"Got 637 segments for platform FORD_MAVERICK_MK1, sampling 5 segments\n",
"Got 3 segments for platform FORD_MUSTANG_MACH_E_MK1, sampling 3 segments\n",
"Skipping platform: FORD_RANGER_MK2, no data available\n",
"Segment collection finished\n"
]
}
],
"source": [
"import random\n",
"\n",
"MAX_SEGS_PER_PLATFORM = 5\n",
"\n",
"VINS_TO_CHECK = set()\n",
"\n",
"print(\"Collecting segments from commaCarSegments dataset:\")\n",
"for platform in platforms:\n",
" if platform not in database:\n",
" print(f\"Skipping platform: {platform}, no data available\")\n",
" continue\n",
"\n",
" all_segments = database[platform]\n",
"\n",
" NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n",
"\n",
" print(f\"Got {len(all_segments)} segments for platform {platform}, sampling {NUM_SEGMENTS} segments\")\n",
"\n",
" segments = random.sample(all_segments, NUM_SEGMENTS)\n",
"\n",
" for segment in segments:\n",
" lr = LogReader(segment, sources=[comma_car_segments_source])\n",
" CP = lr.first(\"carParams\")\n",
" if \"FORD\" not in CP.carFingerprint:\n",
" print(segment, CP.carFingerprint)\n",
" VINS_TO_CHECK.add((CP.carVin, CP.carFingerprint))\n",
"\n",
"print(\"Segment collection finished\")"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"vin: 3FMCR9B69NRXXXXXX real platform: FORD BRONCO SPORT 1ST GEN determined platform: mock correct: False\n",
"vin: 00000000000XXXXXX real platform: FORD F-150 14TH GEN determined platform: mock correct: False\n",
"vin: 1FMCU9J94MUXXXXXX real platform: FORD ESCAPE 4TH GEN determined platform: mock correct: False\n",
"vin: 3FMTK3SU0MMXXXXXX real platform: FORD MUSTANG MACH-E 1ST GEN determined platform: FORD MUSTANG MACH-E 1ST GEN correct: True\n",
"vin: 1FM5K8HC7MGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False\n",
"vin: 5LM5J7XC9LGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False\n",
"vin: 1FTVW1EL4NWXXXXXX real platform: FORD F-150 LIGHTNING 1ST GEN determined platform: FORD F-150 LIGHTNING 1ST GEN correct: True\n",
"vin: WF0NXXGCHNJXXXXXX real platform: FORD FOCUS 4TH GEN determined platform: mock correct: False\n",
"vin: 3FTTW8E99NRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False\n",
"vin: 1FM5K8GC7LGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False\n",
"vin: 3FTTW8E33NRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False\n",
"vin: 5LM5J7XC1LGXXXXXX real platform: FORD EXPLORER 6TH GEN determined platform: mock correct: False\n",
"vin: 3FTTW8E3XPRXXXXXX real platform: FORD MAVERICK 1ST GEN determined platform: mock correct: False\n"
]
}
],
"source": [
"for vin, real_fingerprint in VINS_TO_CHECK:\n",
" determined_fingerprint = ford_vin_fingerprint(vin)\n",
" print(f\"vin: {vin} real platform: {real_fingerprint: <30} \" +\n",
" f\"determined platform: {determined_fingerprint: <30} correct: {real_fingerprint == determined_fingerprint}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,273 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 62,
"id": "228a6736-de31-4255-9d72-a6ff391b968d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found 6 qualifying vehicles:\n",
" KIA_EV6\n",
" HYUNDAI_KONA_EV_2ND_GEN\n",
" HYUNDAI_IONIQ_5\n",
" KIA_NIRO_EV_2ND_GEN\n",
" HYUNDAI_IONIQ_6\n",
" GENESIS_GV60_EV_1ST_GEN\n"
]
}
],
"source": [
"from opendbc.car.hyundai.values import CAR, HyundaiFlags\n",
"\n",
"TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) & set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD electric vehicles only\n",
"#TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) - set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD hybrid and ICE vehicles only\n",
"\n",
"print(f\"Found {len(TEST_PLATFORMS)} qualifying vehicles:\")\n",
"for platform in TEST_PLATFORMS:\n",
" print(f\" {platform}\")"
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "ed1c8aec-c274-4c61-b83d-711ea194bf86",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Collecting segments from commaCarSegments dataset:\n",
"Got 1300 segments for platform KIA_EV6, sampling 5 segments\n",
"Got 9 segments for platform HYUNDAI_KONA_EV_2ND_GEN, sampling 5 segments\n",
"Got 1570 segments for platform HYUNDAI_IONIQ_5, sampling 5 segments\n",
"Got 34 segments for platform KIA_NIRO_EV_2ND_GEN, sampling 5 segments\n",
"Got 974 segments for platform HYUNDAI_IONIQ_6, sampling 5 segments\n",
"Got 157 segments for platform GENESIS_GV60_EV_1ST_GEN, sampling 5 segments\n",
"Collected 30 segments for analysis\n"
]
}
],
"source": [
"import random\n",
"\n",
"from openpilot.tools.lib.logreader import LogReader\n",
"from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database\n",
"from opendbc.car.hyundai.values import CAR\n",
"\n",
"database = get_comma_car_segments_database()\n",
"TEST_SEGMENTS = []\n",
"\n",
"MAX_SEGS_PER_PLATFORM = 5 # TODO: Increase this to search more segments\n",
"\n",
"print(\"Collecting segments from commaCarSegments dataset:\")\n",
"for platform in TEST_PLATFORMS:\n",
" assert(platform in database)\n",
" #if platform not in database:\n",
" # print(f\"Skipping platform: {platform}, no data available\")\n",
" # continue\n",
"\n",
" all_segments = database[platform]\n",
"\n",
" NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n",
"\n",
" print(f\"Got {len(all_segments)} segments for platform {platform}, sampling {NUM_SEGMENTS} segments\")\n",
"\n",
" TEST_SEGMENTS.extend(random.sample(all_segments, NUM_SEGMENTS))\n",
"\n",
"print(f\"Collected {len(TEST_SEGMENTS)} segments for analysis\")\n"
]
},
{
"cell_type": "code",
"execution_count": 64,
"id": "0c75e8f2-4f5f-4f89-b8db-5223a6534a9f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Analyzing segment ff2bd20623fcaeaa/2023-11-26--16-27-04/5/s for KIA EV6 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 3f1a6480f940cf9a/2024-01-10--23-06-11/16/s for KIA EV6 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment b0a9998109ed0053/2023-12-15--11-10-18/12/s for KIA EV6 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 6e14aa2ed85025df/2023-11-15--13-18-12/24/s for KIA EV6 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment a43f21df3a1ca12d/2024-01-25--08-56-22/16/s for KIA EV6 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 1618132d68afc876/2023-12-05--13-49-24/11/s for HYUNDAI KONA ELECTRIC 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 1618132d68afc876/2023-11-26--12-31-18/17/s for HYUNDAI KONA ELECTRIC 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 1618132d68afc876/2023-12-05--11-51-44/3/s for HYUNDAI KONA ELECTRIC 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 1618132d68afc876/2023-08-27--09-32-14/13/s for HYUNDAI KONA 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 1618132d68afc876/2024-01-25--15-07-04/24/s for HYUNDAI KONA ELECTRIC 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 223780ed74116bc2/2023-11-16--09-44-56/15/s for HYUNDAI IONIQ 5 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment ba9951252624f37d/2024-01-20--22-33-23/118/s for HYUNDAI IONIQ 5 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 8379b28e51ceb3b1/2023-11-09--23-21-58/92/s for HYUNDAI IONIQ 5 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 26fac43e27cd6091/2023-11-06--12-23-21/9/s for HYUNDAI IONIQ 5 2022\n",
" GEAR_SHIFTER gear=1.0\n",
" ACCELERATOR gear=0.0\n",
"Analyzing segment 5edb897a0ec7a477/2024-01-13--20-41-36/101/s for HYUNDAI IONIQ 5 2022\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 66cf8ea23b7c2789/2023-12-04--13-48-53/5/s for KIA NIRO EV 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment b153671049a867b3/2023-12-10--20-31-37/2/s for KIA NIRO EV 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment b153671049a867b3/2023-12-03--21-08-30/14/s for KIA NIRO EV 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment b153671049a867b3/2023-11-07--19-52-23/0/s for KIA NIRO EV 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment b153671049a867b3/2023-07-12--19-25-18/6/s for KIA NIRO EV 2ND GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 9ea4578ee2b1abcb/2023-11-18--07-59-26/11/s for HYUNDAI IONIQ 6 2023\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 0ad7facc77922c3e/2023-12-21--17-47-25/18/s for HYUNDAI IONIQ 6 2023\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 26968f888e7330d3/2024-01-02--11-18-37/8/s for HYUNDAI IONIQ 6 2023\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 9ea4578ee2b1abcb/2023-11-27--21-03-24/33/s for HYUNDAI IONIQ 6 2023\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment df7fdd56970d90fe/2024-01-07--01-04-39/26/s for HYUNDAI IONIQ 6 2023\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 94542b2d06f7a9a6/2023-12-11--14-45-44/0/s for GENESIS GV60 ELECTRIC 1ST GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 94542b2d06f7a9a6/2023-12-11--20-57-09/8/s for GENESIS GV60 ELECTRIC 1ST GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 94542b2d06f7a9a6/2024-01-03--12-52-38/5/s for GENESIS GV60 ELECTRIC 1ST GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 94542b2d06f7a9a6/2024-01-19--19-57-52/47/s for GENESIS GV60 ELECTRIC 1ST GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analyzing segment 94542b2d06f7a9a6/2024-01-03--13-01-23/1/s for GENESIS GV60 ELECTRIC 1ST GEN\n",
" GEAR_SHIFTER gear=4.0\n",
" ACCELERATOR gear=5.0\n",
"Analysis finished\n"
]
}
],
"source": [
"import copy\n",
"\n",
"from opendbc.can.parser import CANParser\n",
"from opendbc.car.hyundai.values import DBC\n",
"from opendbc.car.hyundai.hyundaicanfd import CanBus\n",
"\n",
"from openpilot.selfdrive.pandad import can_capnp_to_list\n",
"from openpilot.tools.lib.logreader import comma_car_segments_source\n",
"\n",
"message_names = [\"GEAR_SHIFTER\", \"ACCELERATOR\", \"GEAR\", \"GEAR_ALT\", \"GEAR_ALT_2\"]\n",
"\n",
"for segment in TEST_SEGMENTS:\n",
" lr = LogReader(segment, sources=[comma_car_segments_source])\n",
" CP = lr.first(\"carParams\")\n",
" if CP is None:\n",
" continue\n",
"\n",
" can_msgs = [msg for msg in lr if msg.which() == \"can\"]\n",
" parser_messages = []\n",
" for name in message_names:\n",
" parser_messages.append((name, 0))\n",
" cp = CANParser(DBC[platform][\"pt\"], parser_messages, CanBus(CP).ECAN)\n",
"\n",
" parsed_message_history = []\n",
" examples = []\n",
"\n",
" for msg in can_msgs:\n",
" cp.update_strings(can_capnp_to_list([msg.as_builder().to_bytes()]))\n",
" parsed_message_history.append(copy.copy(cp.vl))\n",
"\n",
" print(f\"Analyzing segment {segment:<44} for {CP.carFingerprint}\")\n",
" for name in message_names:\n",
" if parsed_message_history[0][name][\"CHECKSUM\"] != 0: # Message is present for this segment\n",
" gear_prev = parsed_message_history[0][name][\"GEAR\"]\n",
" print(f\" {name:<15} gear={gear_prev}\")\n",
" for i, parsed_messages in enumerate(parsed_message_history):\n",
" gear = parsed_messages[name][\"GEAR\"]\n",
" if gear != gear_prev:\n",
" print(\" *** Signal transition found! ***\")\n",
" examples.append(i)\n",
" gear_prev = gear\n",
"\n",
"print(\"Analysis finished\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7724dd97-f62e-4fd3-9f64-63d49be669d2",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "9f393e00-8efd-40fb-a41e-d312531a83e8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,260 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"from opendbc.car import structs\n",
"from opendbc.car.subaru.values import CAR, SubaruFlags\n",
"from opendbc.car.subaru.fingerprints import FW_VERSIONS\n",
"\n",
"TEST_PLATFORMS = set(CAR) - CAR.with_flags(SubaruFlags.PREGLOBAL)\n",
"\n",
"Ecu = structs.CarParams.Ecu\n",
"\n",
"FW_BY_ECU = {platform: {ecu: versions for (ecu, addr, sub_addr), versions in fw_versions.items()} for platform, fw_versions in FW_VERSIONS.items()}"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"PLATFORM_CODES = {\n",
" Ecu.abs: {\n",
" 0: {\n",
" b'\\xa5': [CAR.SUBARU_ASCENT, CAR.SUBARU_ASCENT_2023],\n",
" b'\\xa2': [CAR.SUBARU_IMPREZA, CAR.SUBARU_IMPREZA_2020, CAR.SUBARU_CROSSTREK_HYBRID],\n",
" b'\\xa1': [CAR.SUBARU_OUTBACK, CAR.SUBARU_LEGACY, CAR.SUBARU_OUTBACK_2023],\n",
" b'\\xa3': [CAR.SUBARU_FORESTER, CAR.SUBARU_FORESTER_HYBRID, CAR.SUBARU_FORESTER_2022],\n",
" b'z': [CAR.SUBARU_IMPREZA],\n",
" }\n",
" }\n",
"}\n",
"\n",
"YEAR_CODES = {\n",
" Ecu.abs: {\n",
" 2: {\n",
" b'\\x18': 2018,\n",
" b'\\x19': 2019,\n",
" b'\\x20': 2020,\n",
" b'\\x21': 2021,\n",
" b'\\x22': 2022,\n",
" b'\\x23': 2023,\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def get_codes(platforms, codes):\n",
" results = []\n",
" for platform in platforms:\n",
" for ecu in codes:\n",
" for i in codes[ecu]:\n",
" if isinstance(i, tuple):\n",
" j = slice(i[0], i[1])\n",
" else:\n",
" j = slice(i, i+1)\n",
" for version in FW_BY_ECU[platform][ecu]:\n",
" code = version[j]\n",
" if code not in codes[ecu][i]:\n",
" print(f\"{platform} {code.hex()} not in {codes[ecu][i].keys()}\")\n",
" else:\n",
" results.append((platform, codes[ecu][i][code]))\n",
" return results"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"SUBARU_IMPREZA 08 not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"SUBARU_IMPREZA 08 not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"SUBARU_IMPREZA 0c not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"SUBARU_IMPREZA 0c not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"SUBARU_IMPREZA 2e not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"SUBARU_IMPREZA 3f not in dict_keys([b'\\x18', b'\\x19', b' ', b'!', b'\"', b'#'])\n",
"correct_year=False platform=SUBARU_FORESTER year=2018 years=[2019, 2020, 2021]\n",
"correct_year=False platform=SUBARU_FORESTER year=2018 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_FORESTER year=2019 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_FORESTER year=2019 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_FORESTER year=2019 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_FORESTER year=2020 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_FORESTER year=2020 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2022 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_OUTBACK year=2022 years=[2020, 2021, 2022]\n",
"correct_year=False platform=SUBARU_FORESTER_HYBRID year=2019 years=[2020]\n",
"correct_year=False platform=SUBARU_CROSSTREK_HYBRID year=2019 years=[2020]\n",
"correct_year=False platform=SUBARU_CROSSTREK_HYBRID year=2021 years=[2020]\n",
"correct_year=True platform=SUBARU_ASCENT_2023 year=2023 years=[2023]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2019 years=[2017, 2018, 2019]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2019 years=[2017, 2018, 2019]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2018 years=[2017, 2018, 2019]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2019 years=[2017, 2018, 2019]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2019 years=[2017, 2018, 2019]\n",
"correct_year=True platform=SUBARU_IMPREZA year=2019 years=[2017, 2018, 2019]\n",
"correct_year=False platform=SUBARU_FORESTER_2022 year=2021 years=[2022, 2023, 2024]\n",
"correct_year=False platform=SUBARU_FORESTER_2022 year=2021 years=[2022, 2023, 2024]\n",
"correct_year=True platform=SUBARU_FORESTER_2022 year=2022 years=[2022, 2023, 2024]\n",
"correct_year=True platform=SUBARU_FORESTER_2022 year=2022 years=[2022, 2023, 2024]\n",
"correct_year=True platform=SUBARU_ASCENT year=2019 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_ASCENT year=2021 years=[2019, 2020, 2021]\n",
"correct_year=True platform=SUBARU_OUTBACK_2023 year=2023 years=[2023]\n",
"correct_year=True platform=SUBARU_OUTBACK_2023 year=2023 years=[2023]\n",
"correct_year=False platform=SUBARU_IMPREZA_2020 year=2019 years=[2020, 2021, 2022]\n",
"correct_year=False platform=SUBARU_IMPREZA_2020 year=2019 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_IMPREZA_2020 year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_IMPREZA_2020 year=2021 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_IMPREZA_2020 year=2021 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_IMPREZA_2020 year=2021 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_IMPREZA_2020 year=2021 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_LEGACY year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_LEGACY year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_LEGACY year=2020 years=[2020, 2021, 2022]\n",
"correct_year=True platform=SUBARU_LEGACY year=2020 years=[2020, 2021, 2022]\n"
]
}
],
"source": [
"def test_year_code(platform, year):\n",
" car_docs = CAR(platform).config.car_docs\n",
" if isinstance(car_docs, list):\n",
" car_docs = car_docs[0]\n",
" years = [int(y) for y in car_docs.year_list]\n",
" correct_year = year in years\n",
" print(f\"{correct_year=!s: <6} {platform=: <32} {year=: <5} {years=}\")\n",
"\n",
"codes = get_codes(TEST_PLATFORMS, YEAR_CODES)\n",
"for platform, year in codes:\n",
" test_year_code(platform, year)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER_HYBRID platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_CROSSTREK_HYBRID platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_CROSSTREK_HYBRID platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_ASCENT_2023 platforms=['SUBARU_ASCENT', 'SUBARU_ASCENT_2023']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER_2022 platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER_2022 platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER_2022 platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_FORESTER_2022 platforms=['SUBARU_FORESTER', 'SUBARU_FORESTER_HYBRID', 'SUBARU_FORESTER_2022']\n",
"in_possible_platforms=True platform=SUBARU_ASCENT platforms=['SUBARU_ASCENT', 'SUBARU_ASCENT_2023']\n",
"in_possible_platforms=True platform=SUBARU_ASCENT platforms=['SUBARU_ASCENT', 'SUBARU_ASCENT_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK_2023 platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_OUTBACK_2023 platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_IMPREZA_2020 platforms=['SUBARU_IMPREZA', 'SUBARU_IMPREZA_2020', 'SUBARU_CROSSTREK_HYBRID']\n",
"in_possible_platforms=True platform=SUBARU_LEGACY platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_LEGACY platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_LEGACY platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n",
"in_possible_platforms=True platform=SUBARU_LEGACY platforms=['SUBARU_OUTBACK', 'SUBARU_LEGACY', 'SUBARU_OUTBACK_2023']\n"
]
}
],
"source": [
"def test_platform_code(platform, platforms):\n",
" platforms = [str(p) for p in platforms]\n",
" in_possible_platforms = platform in platforms\n",
" print(f\"{in_possible_platforms=!s: <6} {platform=: <32} {platforms=}\")\n",
"\n",
"codes = get_codes(TEST_PLATFORMS, PLATFORM_CODES)\n",
"for platform, possible_platforms in codes:\n",
" test_platform_code(platform, possible_platforms)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,128 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"segments = [\n",
" \"d9df6f87e8feff94|2023-03-28--17-41-10/1:12\"\n",
"]\n",
"platform = \"SUBARU_OUTBACK\"\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"import numpy as np\n",
"\n",
"from opendbc.can.parser import CANParser\n",
"from opendbc.car.subaru.values import DBC\n",
"\n",
"from openpilot.selfdrive.pandad import can_capnp_to_list\n",
"from openpilot.tools.lib.logreader import LogReader\n",
"\n",
"\"\"\"\n",
"In this example, we plot the relationship between Cruise_Brake and Acceleration for stock eyesight.\n",
"\"\"\"\n",
"\n",
"for segment in segments:\n",
" lr = LogReader(segment)\n",
"\n",
" messages = [\n",
" (\"ES_Distance\", 20),\n",
" (\"ES_Brake\", 20),\n",
" (\"ES_Status\", 20),\n",
" ]\n",
"\n",
" cp = CANParser(DBC[platform][\"pt\"], messages, 1)\n",
"\n",
" es_distance_history = []\n",
" es_status_history = []\n",
" es_brake_history = []\n",
" acceleration_history = []\n",
"\n",
" last_acc = 0\n",
"\n",
" for msg in lr:\n",
" if msg.which() == \"can\":\n",
" cp.update_strings(can_capnp_to_list([msg.as_builder().to_bytes()]))\n",
" es_distance_history.append(copy.copy(cp.vl[\"ES_Distance\"]))\n",
" es_brake_history.append(copy.copy(cp.vl[\"ES_Brake\"]))\n",
" es_status_history.append(copy.copy(cp.vl[\"ES_Status\"]))\n",
"\n",
" acceleration_history.append(last_acc)\n",
"\n",
" if msg.which() == \"carState\":\n",
" last_acc = msg.carState.aEgo"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def process(history, func):\n",
" return np.array([func(h) for h in history])\n",
"\n",
"cruise_activated = process(es_status_history, lambda es_status: es_status[\"Cruise_Activated\"])\n",
"cruise_throttle = process(es_distance_history, lambda es_distance: es_distance[\"Cruise_Throttle\"])\n",
"cruise_rpm = process(es_status_history, lambda es_status: es_status[\"Cruise_RPM\"])\n",
"cruise_brake = process(es_brake_history, lambda es_brake: es_brake[\"Brake_Pressure\"])\n",
"acceleration = process(acceleration_history, lambda acc: acc)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"valid_brake = (cruise_activated==1) & (cruise_brake>0) # only when cruise is activated and eyesight is braking\n",
"\n",
"ax = plt.figure().add_subplot()\n",
"\n",
"ax.set_title(\"Brake_Pressure vs Acceleration\")\n",
"ax.set_xlabel(\"Brake_Pessure\")\n",
"ax.set_ylabel(\"Acceleration\")\n",
"ax.scatter(cruise_brake[valid_brake], -acceleration[valid_brake])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,118 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# An example of searching through a database of segments for a specific condition, and plotting the results.\n",
"\n",
"segments = [\n",
" \"c3d1ccb52f5f9d65|2023-07-22--01-23-20/6:10\",\n",
"]\n",
"platform = \"SUBARU_OUTBACK\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from opendbc.can.parser import CANParser\n",
"from opendbc.car.subaru.values import CanBus, DBC\n",
"\n",
"from openpilot.selfdrive.pandad import can_capnp_to_list\n",
"from openpilot.tools.lib.logreader import LogReader\n",
"\n",
"\"\"\"\n",
"In this example, we search for positive transitions of Steer_Warning, which indicate that the EPS\n",
"has stopped responding to our messages. This analysis would allow you to find the cause of these\n",
"steer warnings and potentially work around them.\n",
"\"\"\"\n",
"\n",
"for segment in segments:\n",
" lr = LogReader(segment)\n",
"\n",
" can_msgs = [msg for msg in lr if msg.which() == \"can\"]\n",
"\n",
" messages = [\n",
" (\"Steering_Torque\", 50)\n",
" ]\n",
"\n",
" cp = CANParser(DBC[platform][\"pt\"], messages, CanBus.main)\n",
"\n",
" steering_torque_history = []\n",
" examples = []\n",
"\n",
" for msg in can_msgs:\n",
" cp.update_strings(can_capnp_to_list([msg.as_builder().to_bytes()]))\n",
" steering_torque_history.append(copy.copy(cp.vl[\"Steering_Torque\"]))\n",
"\n",
" steer_warning_last = False\n",
" for i, steering_torque_msg in enumerate(steering_torque_history):\n",
" steer_warning = steering_torque_msg[\"Steer_Warning\"]\n",
"\n",
" steer_angle = steering_torque_msg[\"Steering_Angle\"]\n",
"\n",
" if steer_warning and not steer_warning_last: # positive transition of \"Steer_Warning\"\n",
" examples.append(i)\n",
"\n",
" steer_warning_last = steer_warning\n",
"\n",
" FRAME_DELTA = 100 # plot this many frames around the positive transition\n",
"\n",
" for example in examples:\n",
" fig, axs = plt.subplots(2)\n",
"\n",
" min_frame = int(example-FRAME_DELTA/2)\n",
" max_frame = int(example+FRAME_DELTA/2)\n",
"\n",
" steering_angle_history = [msg[\"Steering_Angle\"] for msg in steering_torque_history[min_frame:max_frame]]\n",
" steering_warning_history = [msg[\"Steer_Warning\"] for msg in steering_torque_history[min_frame:max_frame]]\n",
"\n",
" xs = np.arange(-FRAME_DELTA/2, FRAME_DELTA/2)\n",
"\n",
" axs[0].plot(xs, steering_angle_history)\n",
" axs[0].set_ylabel(\"Steering Angle (deg)\")\n",
" axs[1].plot(xs, steering_warning_history)\n",
" axs[1].set_ylabel(\"Steer Warning\")\n",
"\n",
" plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
import os
import time
import argparse
import signal
from collections import defaultdict
import openpilot.cereal.messaging as messaging
from openpilot.tools.lib.logreader import LogReader
def sigint_handler(signal, frame):
exit(0)
signal.signal(signal.SIGINT, sigint_handler)
class SteeringAccuracyTool:
all_groups = {"germany": (45, "45 - up m/s // 162 - up km/h // 101 - up mph"),
"veryfast": (35, "35 - 45 m/s // 126 - 162 km/h // 78 - 101 mph"),
"fast": (25, "25 - 35 m/s // 90 - 126 km/h // 56 - 78 mph"),
"medium": (15, "15 - 25 m/s // 54 - 90 km/h // 34 - 56 mph"),
"slow": (5, " 5 - 15 m/s // 18 - 54 km/h // 11 - 34 mph"),
"crawl": (0, " 0 - 5 m/s // 0 - 18 km/h // 0 - 11 mph")}
def __init__(self, args):
self.msg_cnt = 0
self.cnt = 0
self.total_error = 0
if args.group == "all":
self.display_groups = self.all_groups.keys()
elif args.group in self.all_groups.keys():
self.display_groups = [args.group]
else:
raise ValueError("invalid speed group, see help")
self.speed_group_stats = {}
for group in self.all_groups:
self.speed_group_stats[group] = defaultdict(lambda: {'err': 0, "cnt": 0, "=": 0, "+": 0, "-": 0, "steer": 0, "limited": 0, "saturated": 0, "dpp": 0})
def update(self, sm):
self.msg_cnt += 1
lateralControlState = sm['controlsState'].lateralControlState
control_type = list(lateralControlState.to_dict().keys())[0]
control_state = lateralControlState.__getattr__(control_type)
v_ego = sm['carState'].vEgo
active = sm['controlsState'].active
steer = sm['carOutput'].actuatorsOutput.torque
standstill = sm['carState'].standstill
steer_limited_by_safety = abs(sm['carControl'].actuators.torque - sm['carControl'].actuatorsOutput.torque) > 1e-2
overriding = sm['carState'].steeringPressed
changing_lanes = sm['modelV2'].meta.laneChangeState != 0
model_points = sm['modelV2'].position.y
# must be engaged, not at standstill, not overriding steering, and not changing lanes
if active and not standstill and not overriding and not changing_lanes:
self.cnt += 1
# wait 5 seconds after engage / standstill / override / lane change
if self.cnt >= 500:
actual_angle = control_state.steeringAngleDeg
desired_angle = control_state.steeringAngleDesiredDeg
# calculate error before rounding, then round for stats grouping
angle_error = abs(desired_angle - actual_angle)
actual_angle = round(actual_angle, 1)
desired_angle = round(desired_angle, 1)
angle_error = round(angle_error, 2)
angle_abs = int(abs(round(desired_angle, 0)))
for group, group_props in self.all_groups.items():
if v_ego > group_props[0]:
# collect stats
self.speed_group_stats[group][angle_abs]["cnt"] += 1
self.speed_group_stats[group][angle_abs]["err"] += angle_error
self.speed_group_stats[group][angle_abs]["steer"] += abs(steer)
if len(model_points):
self.speed_group_stats[group][angle_abs]["dpp"] += abs(model_points[0])
if steer_limited_by_safety:
self.speed_group_stats[group][angle_abs]["limited"] += 1
if control_state.saturated:
self.speed_group_stats[group][angle_abs]["saturated"] += 1
if actual_angle == desired_angle:
self.speed_group_stats[group][angle_abs]["="] += 1
else:
if desired_angle == 0.:
overshoot = True
else:
overshoot = desired_angle < actual_angle if desired_angle > 0. else desired_angle > actual_angle
self.speed_group_stats[group][angle_abs]["+" if overshoot else "-"] += 1
break
else:
self.cnt = 0
if self.msg_cnt % 100 == 0:
print(chr(27) + "[2J")
if self.cnt != 0:
print("COLLECTING ...\n")
else:
print("DISABLED (not active, standstill, steering override, or lane change)\n")
for group in self.display_groups:
if len(self.speed_group_stats[group]) > 0:
print(f"speed group: {group:10s} {self.all_groups[group][1]:>96s}")
print(f" {'-'*118}")
for k in sorted(self.speed_group_stats[group].keys()):
v = self.speed_group_stats[group][k]
print(f' {k:#2}° | actuator:{int(v["steer"] / v["cnt"] * 100):#3}% ' +
f'| error: {round(v["err"] / v["cnt"], 2):2.2f}° | -:{int(v["-"] / v["cnt"] * 100):#3}% ' +
f'| =:{int(v["="] / v["cnt"] * 100):#3}% | +:{int(v["+"] / v["cnt"] * 100):#3}% | lim:{v["limited"]:#5} ' +
f'| sat:{v["saturated"]:#5} | path dev: {round(v["dpp"] / v["cnt"], 2):2.2f}m | total: {v["cnt"]:#5}')
print("")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Steering accuracy measurement tool')
parser.add_argument('--route', help="route name")
parser.add_argument('--addr', default='127.0.0.1', help="IP address for optional ZMQ listener, default to msgq")
parser.add_argument('--group', default='all', help="speed group to display, [crawl|slow|medium|fast|veryfast|germany|all], default to all")
args = parser.parse_args()
tool = SteeringAccuracyTool(args)
if args.route is not None:
print(f"loading {args.route}...")
lr = LogReader(args.route, sort_by_time=True)
sm = {}
for msg in lr:
if msg.which() == 'carState':
sm['carState'] = msg.carState
elif msg.which() == 'carControl':
sm['carControl'] = msg.carControl
elif msg.which() == 'controlsState':
sm['controlsState'] = msg.controlsState
elif msg.which() == 'modelV2':
sm['modelV2'] = msg.modelV2
if msg.which() == 'carControl' and 'carState' in sm and 'controlsState' in sm and 'modelV2' in sm:
tool.update(sm)
else:
if args.addr != "127.0.0.1":
os.environ["ZMQ"] = "1"
messaging.reset_context()
carControl = messaging.sub_sock('carControl', addr=args.addr, conflate=True)
sm = messaging.SubMaster(['carState', 'carControl', 'carOutput', 'controlsState', 'modelV2'], addr=args.addr)
time.sleep(1) # Make sure all submaster data is available before going further
print("waiting for messages...")
while messaging.recv_one(carControl):
sm.update()
tool.update(sm)
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import argparse
import sys
import unittest
from opendbc.car.tests.routes import CarTestRoute
from opendbc.car.tests.test_models import TestCarModelBase
from openpilot.tools.lib.route import SegmentRange
def create_test_models_suite(routes: list[CarTestRoute]) -> unittest.TestSuite:
test_suite = unittest.TestSuite()
for test_route in routes:
# create new test case and discover tests
test_case_args = {"platform": test_route.car_model, "test_route": test_route}
CarModelTestCase = type("CarModelTestCase", (TestCarModelBase,), test_case_args)
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CarModelTestCase))
return test_suite
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Test any route against common issues with a new car port. " +
"Uses opendbc_repo/opendbc/car/tests/test_models.py")
parser.add_argument("route_or_segment_name", help="Specify route to run tests on")
parser.add_argument("--car", help="Specify car model for test route")
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
sr = SegmentRange(args.route_or_segment_name)
test_routes = [CarTestRoute(sr.route_name, args.car, segment=seg_idx) for seg_idx in sr.seg_idxs]
test_suite = create_test_models_suite(test_routes)
unittest.TextTestRunner().run(test_suite)
Executable
+518
View File
@@ -0,0 +1,518 @@
#!/usr/bin/env bash
if [[ ! "${BASH_SOURCE[0]}" = "${0}" ]]; then
echo "Invalid invocation! This script must not be sourced."
echo "Run 'op.sh' directly or check your .bashrc for a valid alias"
return 0
fi
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
UNDERLINE='\033[4m'
BOLD='\033[1m'
NC='\033[0m'
SHELL_NAME="$(basename ${SHELL})"
RC_FILE="${HOME}/.$(basename ${SHELL})rc"
if [ "$(uname)" == "Darwin" ] && [ $SHELL == "/bin/bash" ]; then
RC_FILE="$HOME/.bash_profile"
fi
function retry() {
local attempts=$1
shift
for i in $(seq 1 "$attempts"); do
if "$@"; then
return 0
fi
if [ "$i" -lt "$attempts" ]; then
echo " Attempt $i/$attempts failed, retrying in 5s..."
sleep 5
fi
done
return 1
}
function op_run_command() {
CMD="$*"
echo -e "${BOLD}Running command →${NC} $CMD"
for ((i=0; i<$((19 + ${#CMD})); i++)); do
echo -n "─"
done
echo -e "┘\n"
if [[ -z "$DRY" ]]; then
"$@"
fi
}
# be default, assume openpilot dir is in current directory
OPENPILOT_ROOT=$(pwd)
function op_get_openpilot_dir() {
# First try traversing up the directory tree
while [[ "$OPENPILOT_ROOT" != '/' ]];
do
if find "$OPENPILOT_ROOT/launch_openpilot.sh" -maxdepth 1 -mindepth 1 &> /dev/null; then
return 0
fi
OPENPILOT_ROOT="$(readlink -f "$OPENPILOT_ROOT/"..)"
done
# Fallback to hardcoded directories if not found
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
for dir in "$(readlink -f "$SCRIPT_DIR/../..")" "$HOME/openpilot" "/data/openpilot"; do
if [[ -f "$dir/launch_openpilot.sh" ]]; then
OPENPILOT_ROOT="$dir"
return 0
fi
done
}
function op_install_post_commit() {
op_get_openpilot_dir
if [[ ! -d $OPENPILOT_ROOT/.git/hooks/post-commit.d ]]; then
mkdir $OPENPILOT_ROOT/.git/hooks/post-commit.d
mv $OPENPILOT_ROOT/.git/hooks/post-commit $OPENPILOT_ROOT/.git/hooks/post-commit.d 2>/dev/null || true
fi
cd $OPENPILOT_ROOT/.git/hooks
ln -sf ../../scripts/post-commit post-commit
}
function op_check_openpilot_dir() {
echo "Checking for openpilot directory..."
if [[ -f "$OPENPILOT_ROOT/launch_openpilot.sh" ]]; then
echo -e " ↳ [${GREEN}${NC}] openpilot found."
return 0
fi
echo -e " ↳ [${RED}${NC}] openpilot directory not found! Make sure that you are"
echo " inside the openpilot directory or specify one with the"
echo " --dir option!"
return 1
}
function op_check_git() {
echo "Checking for git..."
if ! command -v "git" > /dev/null 2>&1; then
echo -e " ↳ [${RED}${NC}] git not found on your system!"
return 1
else
echo -e " ↳ [${GREEN}${NC}] git found."
fi
echo "Checking for git lfs files..."
if [[ $(file -b $OPENPILOT_ROOT/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) == "data" ]]; then
echo -e " ↳ [${GREEN}${NC}] git lfs files found."
else
echo -e " ↳ [${RED}${NC}] git lfs files not found! Run 'git lfs pull'"
return 1
fi
echo "Checking for git submodules..."
for name in $(git config --file .gitmodules --get-regexp path | awk '{ print $2 }' | tr '\n' ' '); do
if [[ -z $(ls $OPENPILOT_ROOT/$name) ]]; then
echo -e " ↳ [${RED}${NC}] git submodule $name not found! Run 'git submodule update --init --recursive'"
return 1
fi
done
echo -e " ↳ [${GREEN}${NC}] git submodules found."
}
function op_check_os() {
echo "Checking for compatible os version..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
if [ -f "/etc/os-release" ]; then
source /etc/os-release
case "$VERSION_CODENAME" in
"jammy" | "kinetic" | "noble" | "focal")
echo -e " ↳ [${GREEN}${NC}] Ubuntu $VERSION_CODENAME detected."
;;
* )
echo -e " ↳ [${RED}${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!"
return 1
;;
esac
else
echo -e " ↳ [${RED}${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!"
return 1
fi
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo -e " ↳ [${GREEN}${NC}] macOS detected."
else
echo -e " ↳ [${RED}${NC}] OS type $OSTYPE not supported!"
return 1
fi
}
function op_check_venv() {
echo "Checking for venv..."
if [[ -f $OPENPILOT_ROOT/.venv/bin/activate ]]; then
echo -e " ↳ [${GREEN}${NC}] venv detected."
else
echo -e " ↳ [${RED}${NC}] Can't activate venv in $OPENPILOT_ROOT. Assuming global env!"
fi
}
function op_before_cmd() {
if [[ ! -z "$NO_VERIFY" ]]; then
return 0
fi
op_get_openpilot_dir
cd $OPENPILOT_ROOT
result="$((op_check_openpilot_dir ) 2>&1)" || (echo -e "$result" && return 1)
result="${result}\n$(( op_check_git ) 2>&1)" || (echo -e "$result" && return 1)
result="${result}\n$(( op_check_os ) 2>&1)" || (echo -e "$result" && return 1)
result="${result}\n$(( op_check_venv ) 2>&1)" || (echo -e "$result" && return 1)
op_activate_venv
if [[ -z $VERBOSE ]]; then
echo -e "${BOLD}Checking system →${NC} [${GREEN}${NC}]"
else
echo -e "$result"
fi
}
function op_setup() {
echo "Installing op system-wide..."
OP_SH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )/op.sh"
CMD=$(cat <<EOF
alias op='$OP_SH "\$@"'
_op_completions() { [ "\$COMP_CWORD" -eq 1 ] && COMPREPLY=(\$(compgen -W "\$(awk '/shift 1; op_/{print \$1}' $OP_SH)" -- "\${COMP_WORDS[1]}")); }
[ -n "\$BASH_VERSION" ] && complete -F _op_completions -o default op
EOF
)
grep -q "alias op=" "$RC_FILE" 2>/dev/null || printf '\n%s\n' "$CMD" >> "$RC_FILE"
echo -e "[${GREEN}${NC}] op installed successfully. Open a new shell to use it."
op_get_openpilot_dir
cd $OPENPILOT_ROOT
op_check_openpilot_dir
op_check_os
# Submodules must be present before uv sync: pyproject path sources
# (pandacan, opendbc, msgq, ...) live in the submodule checkouts.
echo "Getting git submodules..."
st="$(date +%s)"
if ! retry 3 git submodule update --jobs 4 --init --recursive; then
echo -e "[${RED}${NC}] Getting git submodules failed!"
return 1
fi
et="$(date +%s)"
echo -e "[${GREEN}${NC}] Submodules installed successfully in $((et - st)) seconds."
echo "Installing dependencies..."
st="$(date +%s)"
SETUP_SCRIPT="tools/setup_dependencies.sh"
if ! $OPENPILOT_ROOT/$SETUP_SCRIPT; then
echo -e "[${RED}${NC}] Dependencies installation failed!"
return 1
fi
et="$(date +%s)"
echo -e "[${GREEN}${NC}] Dependencies installed successfully in $((et - st)) seconds."
op_activate_venv
echo "Pulling git lfs files..."
st="$(date +%s)"
if ! retry 3 git lfs pull; then
echo -e "[${RED}${NC}] Pulling git lfs files failed!"
return 1
fi
et="$(date +%s)"
echo -e "[${GREEN}${NC}] Files pulled successfully in $((et - st)) seconds."
op_check
}
function op_auth() {
op_before_cmd
op_run_command openpilot/tools/lib/auth.py "$@"
}
function op_activate_venv() {
# bash 3.2 can't handle this without the 'set +e'
set +e
source $OPENPILOT_ROOT/.venv/bin/activate &> /dev/null || true
set -e
# persist venv on PATH across GitHub Actions steps
if [ -n "$GITHUB_PATH" ]; then
echo "$OPENPILOT_ROOT/.venv/bin" >> "$GITHUB_PATH"
fi
}
function op_venv() {
op_before_cmd
if [[ ! -f $OPENPILOT_ROOT/.venv/bin/activate ]]; then
echo -e "No venv found in $OPENPILOT_ROOT"
return 1
fi
case $SHELL_NAME in
"zsh")
ZSHRC_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'tmp_zsh')
echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/bin/activate" >> $ZSHRC_DIR/.zshrc
ZDOTDIR=$ZSHRC_DIR zsh ;;
*)
bash --rcfile <(echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/bin/activate") ;;
esac
}
function op_adb() {
op_before_cmd
op_run_command tools/scripts/adb_ssh.sh "$@"
}
function op_ssh() {
op_before_cmd
op_run_command tools/scripts/ssh.py "$@"
}
function op_script() {
op_before_cmd
case $1 in
som-debug ) op_run_command panda/scripts/som_debug.sh "${@:2}" ;;
* )
echo -e "Unknown script '$1'. Available scripts:"
echo -e " ${BOLD}som-debug${NC} SOM serial debug console via panda"
return 1
;;
esac
}
function op_check() {
VERBOSE=1
op_before_cmd
unset VERBOSE
}
function op_esim() {
op_before_cmd
op_run_command openpilot/common/esim/esim.py "$@"
}
function op_build() {
CDIR=$(pwd)
op_before_cmd
cd "$CDIR"
if [[ -f "/AGNOS" ]]; then
# needed on AGNOS to not run out of memory
op_run_command openpilot/system/manager/build.py
else
op_run_command scons -u "$@"
fi
}
function op_juggle() {
op_before_cmd
op_run_command openpilot/tools/plotjuggler/juggle.py "$@"
}
function op_lint() {
op_before_cmd
op_run_command scripts/lint/lint.sh "$@"
}
function op_test() {
op_before_cmd
op_run_command tools/test_runner.py "$@"
}
function op_replay() {
op_before_cmd
op_run_command openpilot/tools/replay/replay "$@"
}
function op_cabana() {
op_before_cmd
op_run_command openpilot/tools/cabana/cabana "$@"
}
function op_sim() {
op_before_cmd
op_run_command exec openpilot/tools/sim/run_bridge.py &
op_run_command exec openpilot/tools/sim/launch_openpilot.sh
}
function op_clip() {
op_before_cmd
op_run_command openpilot/tools/clip/run.py "$@"
}
function op_check_agnos_update() {
if [[ ! -f "/AGNOS" ]]; then
return 0
fi
local choice current_version target_version
current_version="$(< /VERSION)"
target_version="$(unset AGNOS_VERSION; source "$OPENPILOT_ROOT/launch_env.sh"; echo "$AGNOS_VERSION")"
if [[ "$current_version" == "$target_version" ]]; then
return 0
fi
echo -e "${BOLD}AGNOS update available:${NC} $current_version$target_version"
if read -r -p "Install it now? [y/N] " choice && [[ "$choice" =~ ^[Yy]$ ]]; then
op_run_command "$OPENPILOT_ROOT/openpilot/common/hardware/comma/agnos.py" --swap \
"$OPENPILOT_ROOT/openpilot/common/hardware/comma/agnos.json"
if read -r -p "Reboot now to apply the update? [y/N] " choice && [[ "$choice" =~ ^[Yy]$ ]]; then
op_run_command sudo reboot
else
echo "Reboot before starting openpilot to apply the AGNOS update."
fi
fi
}
function op_switch() {
REMOTE="origin"
if [ "$#" -gt 1 ]; then
REMOTE="$1"
shift
fi
if [ -z "$1" ]; then
echo -e "${BOLD}${UNDERLINE}Usage:${NC} op switch [REMOTE] <BRANCH>"
return 1
fi
BRANCH="$1"
git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git submodule deinit --all --force
git fetch "$REMOTE" "$BRANCH"
git checkout -f FETCH_HEAD
git checkout -B "$BRANCH" --track "$REMOTE"/"$BRANCH"
git submodule deinit --all --force
git reset --hard "${REMOTE}/${BRANCH}"
git clean -df
git submodule update --init --recursive
git submodule foreach git reset --hard
git submodule foreach git clean -df
# remove openpilot update flag if present
rm -f .overlay_init
op_check_agnos_update
}
function op_start() {
if [[ -f "/AGNOS" ]]; then
op_before_cmd
op_check_agnos_update
op_run_command sudo systemctl restart comma $@
fi
}
function op_stop() {
if [[ -f "/AGNOS" ]]; then
op_before_cmd
op_run_command sudo systemctl stop comma $@
fi
}
function op_default() {
echo "An openpilot helper"
echo ""
echo -e "${BOLD}${UNDERLINE}Description:${NC}"
echo " op is your entry point for all things related to openpilot development."
echo " op is only a wrapper for existing scripts, tools, and commands."
echo " op will always show you what it will run on your system."
echo ""
echo -e "${BOLD}${UNDERLINE}Usage:${NC} op [OPTIONS] <COMMAND>"
echo ""
echo -e "${BOLD}${UNDERLINE}Commands [System]:${NC}"
echo -e " ${BOLD}auth${NC} Authenticate yourself for API use"
echo -e " ${BOLD}check${NC} Check the development environment (git, os) to start using openpilot"
echo -e " ${BOLD}esim${NC} Manage eSIM profiles on your comma device"
echo -e " ${BOLD}venv${NC} Activate the python virtual environment"
echo -e " ${BOLD}setup${NC} Install the 'op' tool and openpilot dependencies"
echo -e " ${BOLD}build${NC} Run the openpilot build system in the current working directory"
echo -e " ${BOLD}switch${NC} Switch to a different git branch with a clean slate (nukes any changes)"
echo -e " ${BOLD}start${NC} Starts (or restarts) openpilot"
echo -e " ${BOLD}stop${NC} Stops openpilot"
echo ""
echo -e "${BOLD}${UNDERLINE}Commands [Tooling]:${NC}"
echo -e " ${BOLD}juggle${NC} Run PlotJuggler"
echo -e " ${BOLD}replay${NC} Run Replay"
echo -e " ${BOLD}cabana${NC} Run Cabana"
echo -e " ${BOLD}clip${NC} Run clip (linux only)"
echo -e " ${BOLD}adb${NC} Run adb shell"
echo -e " ${BOLD}ssh${NC} comma prime SSH helper"
echo ""
echo -e "${BOLD}${UNDERLINE}Commands [Scripts]:${NC}"
echo -e " ${BOLD}script${NC} Run a script (e.g. op script som-debug)"
echo ""
echo -e "${BOLD}${UNDERLINE}Commands [Testing]:${NC}"
echo -e " ${BOLD}sim${NC} Run openpilot in a simulator"
echo -e " ${BOLD}lint${NC} Run the linter"
echo -e " ${BOLD}post-commit${NC} Install the linter as a post-commit hook"
echo -e " ${BOLD}test${NC} Run all unit tests"
echo ""
echo -e "${BOLD}${UNDERLINE}Options:${NC}"
echo -e " ${BOLD}-d, --dir${NC}"
echo " Specify the openpilot directory you want to use"
echo -e " ${BOLD}--dry${NC}"
echo " Don't actually run anything, just print what would be run"
echo -e " ${BOLD}-n, --no-verify${NC}"
echo " Skip environment check before running commands"
echo ""
echo -e "${BOLD}${UNDERLINE}Examples:${NC}"
echo " op setup"
echo " Run the setup script to install"
echo " openpilot's dependencies."
echo ""
echo " op build -j4"
echo " Compile openpilot using 4 cores"
echo ""
echo " op juggle --demo"
echo " Run PlotJuggler on the demo route"
}
function _op() {
# parse Options
case $1 in
-d | --dir ) shift 1; OPENPILOT_ROOT="$1"; shift 1 ;;
--dry ) shift 1; DRY="1" ;;
-n | --no-verify ) shift 1; NO_VERIFY="1" ;;
esac
# parse Commands
case $1 in
auth ) shift 1; op_auth "$@" ;;
venv ) shift 1; op_venv "$@" ;;
check ) shift 1; op_check "$@" ;;
esim ) shift 1; op_esim "$@" ;;
setup ) shift 1; op_setup "$@" ;;
build ) shift 1; op_build "$@" ;;
juggle ) shift 1; op_juggle "$@" ;;
cabana ) shift 1; op_cabana "$@" ;;
lint ) shift 1; op_lint "$@" ;;
test ) shift 1; op_test "$@" ;;
replay ) shift 1; op_replay "$@" ;;
clip ) shift 1; op_clip "$@" ;;
sim ) shift 1; op_sim "$@" ;;
switch ) shift 1; op_switch "$@" ;;
start ) shift 1; op_start "$@" ;;
stop ) shift 1; op_stop "$@" ;;
restart ) shift 1; op_restart "$@" ;;
post-commit ) shift 1; op_install_post_commit "$@" ;;
adb ) shift 1; op_adb "$@" ;;
ssh ) shift 1; op_ssh "$@" ;;
script ) shift 1; op_script "$@" ;;
* ) op_default "$@" ;;
esac
}
_op "$@"
+35
View File
@@ -0,0 +1,35 @@
# openpilot releases
```
## release checklist
### Go to staging
- [ ] make a GitHub issue to track release with this checklist
- [ ] create release master branch
- [ ] create a branch from upstream master named `zerotentwo` for release `v0.10.2`
- [ ] revert risky commits (double check with autonomy team)
- [ ] push the new branch
- [ ] push to staging:
- [ ] make sure you are on the newly created release master branch (`zerotentwo`)
- [ ] run `BRANCH=devel-staging tools/release/build_stripped.sh`. Jenkins will then automatically build staging on device, run `test_onroad` and update the staging branch
- [ ] bump version on master: `openpilot/common/version.h` and `RELEASES.md`
- [ ] post on Discord, tag `@release crew`
### Go to release
- [ ] before going to release, test the following:
- [ ] update from previous release -> new release
- [ ] update from new release -> previous release
- [ ] fresh install with `openpilot-test.comma.ai`
- [ ] drive on fresh install
- [ ] no submodules or LFS
- [ ] check sentry, MTBF, etc.
- [ ] stress test passes in production
- [ ] publish the blog post
- [ ] `git reset --hard origin/release-mici-staging`
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
- [ ] create GitHub release
- [ ] final test install on `openpilot.comma.ai`
- [ ] update factory provisioning
- [ ] close out milestone and issue
- [ ] post on Discord, X, etc.
```
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -e
set -x
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR
BUILD_DIR=/data/openpilot
SOURCE_DIR="$(git rev-parse --show-toplevel)"
export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo"
if [ -z "$RELEASE_BRANCH" ]; then
echo "RELEASE_BRANCH is not set"
exit 1
fi
BUILD_BRANCH=release-mici-staging
# set git identity
source $DIR/identity.sh
echo "[-] Setting up repo T=$SECONDS"
if ! git -C "$SOURCE_DIR" worktree remove --force "$BUILD_DIR" 2>/dev/null; then
rm -rf $BUILD_DIR
fi
git -C "$SOURCE_DIR" worktree prune
git -C "$SOURCE_DIR" worktree add --detach --no-checkout "$BUILD_DIR"
cd $BUILD_DIR
git update-ref -d "refs/heads/$BUILD_BRANCH"
git symbolic-ref HEAD "refs/heads/$BUILD_BRANCH"
git read-tree --empty
# do the files copy
echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR
./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" --
# in the directory
cd $BUILD_DIR
# use the full CPU available for speeding up the build.
# openpilot resets the CPU frequencies when test_onroad.py runs below.
for policy in /sys/devices/system/cpu/cpufreq/policy*; do
[ -d "$policy" ] || continue
hardware_max="$(cat "$policy/cpuinfo_max_freq")"
echo "$hardware_max" | sudo tee "$policy/scaling_max_freq" >/dev/null
done
scons
if [ -n "$INCLUDE_BIG_MODEL" ]; then
test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest
fi
if [ -z "$PANDA_DEBUG_BUILD" ]; then
# release panda fw
CERT=/data/pandaextra/certs/release RELEASE=1 scons panda/
else
# build with ALLOW_DEBUG=1 to enable features like experimental longitudinal
scons panda/
fi
# Ensure no submodules in release
if test "$(git submodule--helper list | wc -l)" -gt "0"; then
echo "submodules found:"
git submodule--helper list
exit 1
fi
git submodule status
# Cleanup
find . -name '*.a' -delete
find . -name '*.o' -delete
find . -name '*.os' -delete
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
rm -rf .sconsign.dblite Jenkinsfile tools/release/
rm -f openpilot/selfdrive/modeld/models/*.onnx*
rm -f openpilot/sunnypilot/modeld*/models/*.onnx*
find openpilot/third_party/ -name '*x86*' -exec rm -r {} +
find openpilot/third_party/ -name '*Darwin*' -exec rm -r {} +
# Restore third_party
git checkout openpilot/third_party/
# Mark as prebuilt release
touch prebuilt
VERSION=$(cat openpilot/sunnypilot/common/version.h | awk -F[\"-] '{print $2}')
# Add built files to git
# writing larger objects is faster than compressing them on-device
git -c core.compression=0 add -f .
git -c core.compression=0 -c gc.auto=0 commit -m "openpilot v$VERSION"
# Run tests
cd $BUILD_DIR
RELEASE=1 ./openpilot/selfdrive/test/test_onroad.py
#tools/test_runner.py openpilot/selfdrive/car/tests/test_car_interfaces.py
echo "[-] pushing release T=$SECONDS"
REFS=()
for branch in ${RELEASE_BRANCH//,/ }; do
REFS+=("$BUILD_BRANCH:$branch")
done
# uploading the larger pack is faster than spending CPU to optimize it
git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "${REFS[@]}"
echo "[-] done T=$SECONDS"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -ex
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
SOURCE_DIR="$(git -C $DIR rev-parse --show-toplevel)"
if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$(mktemp -d)"
fi
# set git identity
source $DIR/identity.sh
echo "[-] Setting up target repo T=$SECONDS"
rm -rf $TARGET_DIR
mkdir -p $TARGET_DIR
cd $TARGET_DIR
cp -r $SOURCE_DIR/.git $TARGET_DIR
echo "[-] setting up stripped branch sync T=$SECONDS"
cd $TARGET_DIR
# tmp branch
git checkout --orphan tmp
# remove everything except .git
echo "[-] erasing old sunnypilot T=$SECONDS"
git submodule deinit -f --all
git rm -rf --cached .
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
# do the files copy
echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR
./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$TARGET_DIR" --
# in the directory
cd $TARGET_DIR
rm -rf .git/modules/
find openpilot/selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./openpilot/common/file_chunker.py {} \;
# include source commit hash and build date in commit
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
GIT_COMMIT_DATE=$(git --git-dir=$SOURCE_DIR/.git show --no-patch --format='%ct %ci' HEAD)
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
VERSION=$(cat $SOURCE_DIR/openpilot/sunnypilot/common/version.h | awk -F\" '{print $2}')
echo -n "$GIT_HASH" > git_src_commit
echo -n "$GIT_COMMIT_DATE" > git_src_commit_date
echo "[-] committing version $VERSION T=$SECONDS"
# writing larger objects is faster than compressing them on-device
git -c core.compression=0 add -f .
git status
git -c core.compression=0 commit -a -m "sunnypilot v$VERSION release
date: $DATETIME
master commit: $GIT_HASH
"
# should be no submodules or LFS files
git submodule status
if [ ! -z "$(git lfs ls-files)" ]; then
echo "LFS files detected!"
exit 1
fi
# ensure files are within GitHub's limit
BIG_FILES="$(find . -type f -not -path './.git/*' -size +95M)"
if [ ! -z "$BIG_FILES" ]; then
printf '\n\n\n'
echo "Found files exceeding GitHub's 100MB limit:"
echo "$BIG_FILES"
exit 1
fi
if [ ! -z "$BRANCH" ]; then
echo "[-] Pushing to $BRANCH T=$SECONDS"
# uploading the larger pack is faster than spending CPU to optimize it
git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin tmp:$BRANCH
fi
echo "[-] done T=$SECONDS, ready at $TARGET_DIR"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR
if [ ! -z "$(git status --porcelain)" ]; then
echo "Dirty working tree after build:"
git status --porcelain
exit 1
fi
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
has_submodule_changes() {
local submodule_path="$1"
if [ -n "$SUBMODULE_PATHS" ]; then
echo "$SUBMODULE_PATHS" | grep -q "$submodule_path"
return $?
fi
return 1
}
while read hash submodule ref; do
if [ -z "$hash" ] || [ -z "$submodule" ]; then
continue
fi
hash=$(echo "$hash" | sed 's/^[+-]//')
if [ "$submodule" = "tinygrad_repo" ]; then
echo "Skipping $submodule"
continue
fi
if [ "$CHECK_PR_REFS" = "true" ] && has_submodule_changes "$submodule"; then
echo "Checking $submodule (non-master): verifying hash $hash exists"
git -C $submodule fetch --depth 100 origin
if git -C $submodule cat-file -e $hash 2>/dev/null; then
echo "$submodule ok (hash exists)"
else
echo "$submodule: $hash does not exist in the repository"
exit 1
fi
else
git -C $submodule fetch --depth 100 origin master
git -C $submodule branch -r --contains $hash | grep "origin/master"
if [ "$?" -eq 0 ]; then
echo "$submodule ok"
else
echo "$submodule: $hash is not on master"
exit 1
fi
fi
done <<< $(git submodule status --recursive)
+4
View File
@@ -0,0 +1,4 @@
export GIT_COMMITTER_NAME="github-actions[bot]"
export GIT_COMMITTER_EMAIL="github-actions[bot]@users.noreply.github.com"
export GIT_AUTHOR_NAME="github-actions[bot]"
export GIT_AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import importlib
import shutil
import sys
import tempfile
import zipapp
from argparse import ArgumentParser
from pathlib import Path
from openpilot.common.basedir import BASEDIR
DIRS = ['openpilot']
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po']
EXCLUDE = ['openpilot/selfdrive/assets/training']
INTERPRETER = '/usr/bin/env python3'
def copy(src, dest):
if any(src.endswith(ext) for ext in EXTS) and not any(exc in src for exc in EXCLUDE):
shutil.copy2(src, dest, follow_symlinks=True)
if __name__ == '__main__':
parser = ArgumentParser(prog='pack.py', description="package script into a portable executable", epilog='comma.ai')
parser.add_argument('-e', '--entrypoint', help="function to call in module, default is 'main'", default='main')
parser.add_argument('-o', '--output', help='output file')
parser.add_argument('module', help="the module to target, e.g. 'openpilot.system.ui.spinner'")
args = parser.parse_args()
print('WARNING: copying all files! make sure to run scons and git tree is clean')
if not args.output:
args.output = args.module
try:
mod = importlib.import_module(args.module)
except ModuleNotFoundError:
print(f'{args.module} not found, typo?')
sys.exit(1)
if not hasattr(mod, args.entrypoint):
print(f'{args.module} does not have a {args.entrypoint}() function, typo?')
sys.exit(1)
with tempfile.TemporaryDirectory() as tmp:
for directory in DIRS:
shutil.copytree(BASEDIR + '/' + directory, tmp + '/' + directory, symlinks=False, dirs_exist_ok=True, copy_function=copy)
entry = f'{args.module}:{args.entrypoint}'
zipapp.create_archive(tmp, target=args.output, interpreter=INTERPRETER, main=entry)
print(f'created executable {Path(args.output).resolve()}')
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import os
import re
import subprocess
import sys
HERE = os.path.abspath(os.path.dirname(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "../.."))
blacklist = [
".git/",
".venv/",
".github/workflows/",
"matlab.*.md",
# no LFS or submodules in release
".lfsconfig",
".gitattributes",
".git$",
".gitmodules",
".run/",
".idea/",
]
# gets you through the blacklist
whitelist: list[str] = [
]
if __name__ == "__main__":
tracked_files = subprocess.check_output(["git", "ls-files", "-z", "--recurse-submodules"], cwd=ROOT).split(b"\0")
for tracked_file in tracked_files:
if not tracked_file:
continue
rf = os.fsdecode(tracked_file)
if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"):
continue
blacklisted = any(re.search(p, rf) for p in blacklist)
whitelisted = any(re.search(p, rf) for p in whitelist)
if blacklisted and not whitelisted:
continue
sys.stdout.buffer.write(tracked_file + b"\0")
View File
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
# Forward all openpilot service ports
while IFS=' ' read -r name port; do
adb forward "tcp:${port}" "tcp:${port}" > /dev/null
done < <(python3 - <<'PY'
from openpilot.cereal.services import SERVICE_LIST
FNV_PRIME = 0x100000001b3
FNV_OFFSET_BASIS = 0xcbf29ce484222325
START_PORT = 8023
MAX_PORT = 65535
PORT_RANGE = MAX_PORT - START_PORT
MASK = 0xffffffffffffffff
def fnv1a(endpoint: str) -> int:
h = FNV_OFFSET_BASIS
for b in endpoint.encode():
h ^= b
h = (h * FNV_PRIME) & MASK
return h
ports = set()
for name in SERVICE_LIST.keys():
port = START_PORT + fnv1a(name) % PORT_RANGE
ports.add((name, port))
for name, port in sorted(ports):
print(f"{name} {port}")
PY
)
# Forward SSH port, finding a free local port if 2222 is taken.
SSH_PORT=2222
while ss -tln | grep -q ":${SSH_PORT} "; do
SSH_PORT=$((SSH_PORT + 1))
done
adb forward tcp:${SSH_PORT} tcp:22
# SSH!
ssh comma@localhost -p ${SSH_PORT} "$@"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
import argparse
import binascii
import time
from collections import defaultdict
import openpilot.cereal.messaging as messaging
from tools.scripts.car.can_table import can_table
from openpilot.tools.lib.logreader import LogIterable, LogReader
RED = '\033[91m'
CLEAR = '\033[0m'
def update(msgs, bus, dat, low_to_high, high_to_low, quiet=False):
for x in msgs:
if x.which() != 'can':
continue
for y in x.can:
if y.src == bus:
dat[y.address] = y.dat
i = int.from_bytes(y.dat, byteorder='big')
l_h = low_to_high[y.address]
h_l = high_to_low[y.address]
change = None
if (i | l_h) != l_h:
low_to_high[y.address] = i | l_h
change = "+"
if (~i | h_l) != h_l:
high_to_low[y.address] = ~i | h_l
change = "-"
if change and not quiet:
print(f"{time.monotonic():.2f}\t{hex(y.address)} ({y.address})\t{change}{binascii.hexlify(y.dat)}")
def can_printer(bus=0, init_msgs=None, new_msgs=None, table=False):
logcan = messaging.sub_sock('can', timeout=10)
dat = defaultdict(int)
low_to_high = defaultdict(int)
high_to_low = defaultdict(int)
if init_msgs is not None:
update(init_msgs, bus, dat, low_to_high, high_to_low, quiet=True)
low_to_high_init = low_to_high.copy()
high_to_low_init = high_to_low.copy()
if new_msgs is not None:
update(new_msgs, bus, dat, low_to_high, high_to_low)
else:
# Live mode
print(f"Waiting for messages on bus {bus}")
try:
while 1:
can_recv = messaging.drain_sock(logcan)
update(can_recv, bus, dat, low_to_high, high_to_low)
time.sleep(0.02)
except KeyboardInterrupt:
pass
print("\n\n")
tables = ""
for addr in sorted(dat.keys()):
init = low_to_high_init[addr] & high_to_low_init[addr]
now = low_to_high[addr] & high_to_low[addr]
d = now & ~init
if d == 0:
continue
b = d.to_bytes(len(dat[addr]), byteorder='big')
byts = ''.join([(c if c == '0' else f'{RED}{c}{CLEAR}') for c in str(binascii.hexlify(b))[2:-1]])
header = f"{hex(addr).ljust(6)}({str(addr).ljust(4)})"
print(header, byts)
tables += f"{header}\n"
tables += can_table(b) + "\n\n"
if table:
print(tables)
if __name__ == "__main__":
desc = """Collects messages and prints when a new bit transition is observed.
This is very useful to find signals based on user triggered actions, such as blinkers and seatbelt.
Leave the script running until no new transitions are seen, then perform the action."""
parser = argparse.ArgumentParser(description=desc,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--bus", type=int, help="CAN bus to print out", default=0)
parser.add_argument("--table", action="store_true", help="Print a cabana-like table")
parser.add_argument("init", type=str, nargs='?', help="Route or segment to initialize with. Use empty quotes to compare against all zeros.")
parser.add_argument("comp", type=str, nargs='?', help="Route or segment to compare against init")
args = parser.parse_args()
init_lr: LogIterable | None = None
new_lr: LogIterable | None = None
if args.init:
if args.init == '':
init_lr = []
else:
init_lr = LogReader(args.init)
if args.comp:
new_lr = LogReader(args.comp)
can_printer(args.bus, init_msgs=init_lr, new_msgs=new_lr, table=args.table)
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
import binascii
import time
from collections import defaultdict
import openpilot.cereal.messaging as messaging
def can_printer(bus, max_msg, addr, ascii_decode):
logcan = messaging.sub_sock('can', addr=addr)
start = time.monotonic()
lp = time.monotonic()
msgs = defaultdict(list)
while 1:
can_recv = messaging.drain_sock(logcan, wait_for_one=True)
for x in can_recv:
for y in x.can:
if y.src == bus:
msgs[y.address].append(y.dat)
if time.monotonic() - lp > 0.1:
dd = chr(27) + "[2J"
dd += f"{time.monotonic() - start:5.2f}\n"
for _addr in sorted(msgs.keys()):
a = f"\"{msgs[_addr][-1].decode('ascii', 'backslashreplace')}\"" if ascii_decode else ""
x = binascii.hexlify(msgs[_addr][-1]).decode('ascii')
freq = len(msgs[_addr]) / (time.monotonic() - start)
if max_msg is None or _addr < max_msg:
dd += f"{_addr:04X}({_addr:4d})({len(msgs[_addr]):6d})({freq:3}dHz) {x.ljust(20)} {a}\n"
print(dd)
lp = time.monotonic()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="simple CAN data viewer",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--bus", type=int, help="CAN bus to print out", default=0)
parser.add_argument("--max_msg", type=int, help="max addr")
parser.add_argument("--ascii", action='store_true', help="decode as ascii")
parser.add_argument("--addr", default="127.0.0.1")
args = parser.parse_args()
can_printer(args.bus, args.max_msg, args.addr, args.ascii)
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
import pandas as pd
import openpilot.cereal.messaging as messaging
def can_table(dat):
rows = []
for b in dat:
r = list(bin(b).lstrip('0b').zfill(8))
r += [hex(b)]
rows.append(r)
df = pd.DataFrame(data=rows)
df.columns = [str(n) for n in range(7, -1, -1)] + [' ']
table = df.to_markdown(tablefmt='grid')
return table
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Cabana-like table of bits for your terminal",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("addr", type=str, nargs=1)
parser.add_argument("bus", type=int, default=0, nargs='?')
args = parser.parse_args()
addr = int(args.addr[0], 0)
can = messaging.sub_sock('can', conflate=False, timeout=None)
print(f"waiting for {hex(addr)} ({addr}) on bus {args.bus}...")
latest = None
while True:
for msg in messaging.drain_sock(can, wait_for_one=True):
for m in msg.can:
if m.address == addr and m.src == args.bus:
latest = m
if latest is None:
continue
table = can_table(latest.dat)
print(f"\n\n{hex(addr)} ({addr}) on bus {args.bus}\n{table}")
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import sys
import argparse
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, MessageTimeoutError, SESSION_TYPE, DTC_GROUP_TYPE
from opendbc.car.structs import CarParams
from panda import Panda
parser = argparse.ArgumentParser(description="clear DTC status")
parser.add_argument("addr", type=lambda x: int(x,0), nargs="?", default=0x7DF) # default is functional (broadcast) address
parser.add_argument("--bus", type=int, default=0)
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, args.addr, bus=args.bus)
print("extended diagnostic session ...")
try:
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
except MessageTimeoutError:
# functional address isn't properly handled so a timeout occurs
if args.addr != 0x7DF:
raise
print("clear diagnostic info ...")
try:
uds_client.clear_diagnostic_information(DTC_GROUP_TYPE.ALL)
except MessageTimeoutError:
# functional address isn't properly handled so a timeout occurs
if args.addr != 0x7DF:
pass
print("")
print("you may need to power cycle your vehicle now")
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.disable_ecu import disable_ecu
from openpilot.selfdrive.car.card import can_comm_callbacks
if __name__ == "__main__":
sendcan = messaging.pub_sock('sendcan')
logcan = messaging.sub_sock('can')
can_callbacks = can_comm_callbacks(logcan, sendcan)
time.sleep(1)
# honda bosch radar disable
disabled = disable_ecu(*can_callbacks, bus=1, addr=0x18DAB0F1, com_cont_req=b'\x28\x83\x03', timeout=0.5)
print(f"disabled: {disabled}")
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import argparse
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.carlog import carlog
from opendbc.car.ecu_addrs import get_all_ecu_addrs
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import can_comm_callbacks, obd_callback
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get addresses of all ECUs')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--bus', type=int, default=1)
parser.add_argument('--no-obd', action='store_true')
parser.add_argument('--timeout', type=float, default=1.0)
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
logcan = messaging.sub_sock('can')
sendcan = messaging.pub_sock('sendcan')
can_callbacks = can_comm_callbacks(logcan, sendcan)
# Set up params for pandad
params = Params()
params.remove("FirmwareQueryDone")
params.put_bool("IsOffroad", True, block=True)
time.sleep(0.2) # thread is 10 Hz
params.put_bool("IsOffroad", False, block=True)
obd_callback(params)(not args.no_obd)
print("Getting ECU addresses ...")
ecu_addrs = get_all_ecu_addrs(*can_callbacks, args.bus, args.timeout)
print()
print("Found ECUs on rx addresses:")
for addr, subaddr, _ in ecu_addrs:
msg = f" {hex(addr)}"
if subaddr is not None:
msg += f" (sub-address: {hex(subaddr)})"
print(msg)
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
import time
import argparse
import openpilot.cereal.messaging as messaging
from opendbc.car.structs import car
from opendbc.car.carlog import carlog
from opendbc.car.fw_versions import get_fw_versions, match_fw_to_car
from opendbc.car.vin import get_vin
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import can_comm_callbacks, obd_callback
from typing import Any
Ecu = car.CarParams.Ecu
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get firmware version of ECUs')
parser.add_argument('--scan', action='store_true')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--brand', help='Only query addresses/with requests for this brand')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
logcan = messaging.sub_sock('can')
pandaStates_sock = messaging.sub_sock('pandaStates')
sendcan = messaging.pub_sock('sendcan')
can_callbacks = can_comm_callbacks(logcan, sendcan)
# Set up params for pandad
params = Params()
params.remove("FirmwareQueryDone")
params.put_bool("IsOffroad", True, block=True)
time.sleep(0.2) # thread is 10 Hz
params.put_bool("IsOffroad", False, block=True)
set_obd_multiplexing = obd_callback(params)
extra: Any = None
if args.scan:
extra = {}
# Honda
for i in range(256):
extra[(Ecu.unknown, 0x18da00f1 + (i << 8), None)] = []
extra[(Ecu.unknown, 0x700 + i, None)] = []
extra[(Ecu.unknown, 0x750, i)] = []
extra = {"any": {"debug": extra}}
t = time.monotonic()
print("Getting vin...")
set_obd_multiplexing(True)
vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (0, 1))
print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}')
print(f"Getting VIN took {time.monotonic() - t:.3f} s")
print()
t = time.monotonic()
fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, progress=True)
_, candidates = match_fw_to_car(fw_vers, vin)
print()
print("Found FW versions")
print("{")
padding = max([len(fw.brand) for fw in fw_vers] or [0])
for version in fw_vers:
subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand:{padding}}, bus: {version.bus}, OBD: {version.obdMultiplexing} - " +
f"(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion!r}]")
print("}")
print()
print("Possible matches:", candidates)
print(f"Getting fw took {time.monotonic() - t:.3f} s")
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Some Hyundai radars can be reconfigured to output (debug) radar points on bus 1.
Reconfiguration is done over UDS by reading/writing to 0x0142 using the Read/Write Data By Identifier
endpoints (0x22 & 0x2E). This script checks your radar firmware version against a list of known
firmware versions. If you want to try on a new radar make sure to note the default config value
in case it's different from the other radars and you need to revert the changes.
After changing the config the car should not show any faults when openpilot is not running.
These config changes are persistent across car reboots. You need to run this script again
to go back to the default values.
USE AT YOUR OWN RISK! Safety features, like AEB and FCW, might be affected by these changes."""
import sys
import argparse
from typing import NamedTuple
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, SESSION_TYPE, DATA_IDENTIFIER_TYPE
from opendbc.car.structs import CarParams
from panda.python import Panda
class ConfigValues(NamedTuple):
default_config: bytes
tracks_enabled: bytes
# If your radar supports changing data identifier 0x0142 as well make a PR to
# this file to add your firmware version. Make sure to post a drive as proof!
# NOTE: these firmware versions do not match what openpilot uses
# because this script uses a different diagnostic session type
SUPPORTED_FW_VERSIONS = {
# 2020 SONATA
b"DN8_ SCC FHCUP 1.00 1.00 99110-L0000\x19\x08)\x15T ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"DN8_ SCC F-CUP 1.00 1.00 99110-L0000\x19\x08)\x15T ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2021 SONATA HYBRID
b"DNhe SCC FHCUP 1.00 1.00 99110-L5000\x19\x04&\x13' ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"DNhe SCC FHCUP 1.00 1.02 99110-L5000 \x01#\x15# ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 PALISADE
b"LX2_ SCC FHCUP 1.00 1.04 99110-S8100\x19\x05\x02\x16V ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2022 PALISADE
b"LX2_ SCC FHCUP 1.00 1.00 99110-S8110!\x04\x05\x17\x01 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 SANTA FE
b"TM__ SCC F-CUP 1.00 1.03 99110-S2000\x19\x050\x13' ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 GENESIS G70
b'IK__ SCC F-CUP 1.00 1.02 96400-G9100\x18\x07\x06\x17\x12 ': ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2019 SANTA FE
b"TM__ SCC F-CUP 1.00 1.00 99110-S1210\x19\x01%\x168 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"TM__ SCC F-CUP 1.00 1.02 99110-S2000\x18\x07\x08\x18W ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2021 K5 HEV
b"DLhe SCC FHCUP 1.00 1.02 99110-L7000 \x01 \x102 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2022 Niro EV
b"DEev SCC F-CUP 1.00 1.00 99110-Q4600\x01\x42 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"DEev SCC F-CUP 1.00 1.00 99110-Q4600 \x07\x03\t% ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='configure radar to output points (or reset to default)')
parser.add_argument('--default', action="store_true", default=False, help='reset to default configuration (default: false)')
parser.add_argument('--debug', action="store_true", default=False, help='enable debug output (default: false)')
parser.add_argument('--bus', type=int, default=0, help='can bus to use (default: 0)')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
confirm = input("power on the vehicle keeping the engine off (press start button twice) then type OK to continue: ").upper().strip()
if confirm != "OK":
print("\nyou didn't type 'OK! (aborted)")
sys.exit(0)
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
print("\n[START DIAGNOSTIC SESSION]")
session_type : SESSION_TYPE = 0x07
uds_client.diagnostic_session_control(session_type)
print("[HARDWARE/SOFTWARE VERSION]")
fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100
fw_version = uds_client.read_data_by_identifier(fw_version_data_id)
print(fw_version)
if fw_version not in SUPPORTED_FW_VERSIONS.keys():
print("radar not supported! (aborted)")
sys.exit(1)
print("[GET CONFIGURATION]")
config_data_id : DATA_IDENTIFIER_TYPE = 0x0142
current_config = uds_client.read_data_by_identifier(config_data_id)
config_values = SUPPORTED_FW_VERSIONS[fw_version]
new_config = config_values.default_config if args.default else config_values.tracks_enabled
print(f"current config: 0x{current_config.hex()}")
if current_config != new_config:
print("[CHANGE CONFIGURATION]")
print(f"new config: 0x{new_config.hex()}")
uds_client.write_data_by_identifier(config_data_id, new_config)
if not args.default and current_config != SUPPORTED_FW_VERSIONS[fw_version].default_config:
print("\ncurrent config does not match expected default! (aborted)")
sys.exit(1)
print("[DONE]")
print("\nrestart your vehicle and ensure there are no faults")
if not args.default:
print("you can run this script again with --default to go back to the original (factory) settings")
else:
print("[DONE]")
print("\ncurrent config is already the desired configuration")
sys.exit(0)
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import matplotlib.pyplot as plt
from functools import partial
from tqdm import tqdm
from typing import NamedTuple
from openpilot.tools.lib.logreader import LogReader
from openpilot.selfdrive.locationd.models.pose_kf import EARTH_G
RLOG_MIN_LAT_ACTIVE = 50
RLOG_MIN_STEERING_UNPRESSED = 50
RLOG_MIN_REQUESTING_MAX = 25 # sample many times after reaching max torque
QLOG_DECIMATION = 10
class Event(NamedTuple):
lateral_accel: float
speed: float
roll: float
timestamp: float # relative to start of route (s)
def find_events(lr: LogReader, extrapolate: bool = False, qlog: bool = False) -> list[Event]:
min_lat_active = RLOG_MIN_LAT_ACTIVE // QLOG_DECIMATION if qlog else RLOG_MIN_LAT_ACTIVE
min_steering_unpressed = RLOG_MIN_STEERING_UNPRESSED // QLOG_DECIMATION if qlog else RLOG_MIN_STEERING_UNPRESSED
min_requesting_max = RLOG_MIN_REQUESTING_MAX // QLOG_DECIMATION if qlog else RLOG_MIN_REQUESTING_MAX
# if we test with driver torque safety, max torque can be slightly noisy
steer_threshold = 0.7 if extrapolate else 0.95
events = []
# state tracking
steering_unpressed = 0 # frames
requesting_max = 0 # frames
lat_active = 0 # frames
# current state
curvature = 0
v_ego = 0
roll = 0
out_torque = 0
start_ts = 0
for msg in lr:
if msg.which() == 'carControl':
if start_ts == 0:
start_ts = msg.logMonoTime
lat_active = lat_active + 1 if msg.carControl.latActive else 0
elif msg.which() == 'carOutput':
out_torque = msg.carOutput.actuatorsOutput.torque
requesting_max = requesting_max + 1 if abs(out_torque) > steer_threshold else 0
elif msg.which() == 'carState':
steering_unpressed = steering_unpressed + 1 if not msg.carState.steeringPressed else 0
v_ego = msg.carState.vEgo
elif msg.which() == 'controlsState':
curvature = msg.controlsState.curvature
elif msg.which() == 'vehicleParameters':
roll = msg.vehicleParameters.roll
if lat_active > min_lat_active and steering_unpressed > min_steering_unpressed and requesting_max > min_requesting_max:
# TODO: record max lat accel at the end of the event, need to use the past lat accel as overriding can happen before we detect it
requesting_max = 0
factor = 1 / abs(out_torque)
current_lateral_accel = (curvature * v_ego ** 2 * factor) - roll * EARTH_G
events.append(Event(current_lateral_accel, v_ego, roll, round((msg.logMonoTime - start_ts) * 1e-9, 2)))
print(events[-1])
return events
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Find max lateral acceleration events",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("route", nargs='+')
parser.add_argument("-e", "--extrapolate", action="store_true", help="Extrapolates max lateral acceleration events linearly. " +
"This option can be far less accurate.")
args = parser.parse_args()
events = []
for route in tqdm(args.route):
try:
lr = LogReader(route, sort_by_time=True)
except Exception:
print(f'Skipping {route}')
continue
qlog = route.endswith('/q')
if qlog:
print('WARNING: Treating route as qlog!')
print('Finding events...')
events += lr.run_across_segments(8, partial(find_events, extrapolate=args.extrapolate, qlog=qlog), disable_tqdm=True)
print()
print(f'Found {len(events)} events')
perc_left_accel = -np.percentile([-ev.lateral_accel for ev in events if ev.lateral_accel < 0] or [0], 90)
perc_right_accel = np.percentile([ev.lateral_accel for ev in events if ev.lateral_accel > 0] or [0], 90)
CP = lr.first('carParams')
plt.ion()
plt.clf()
plt.suptitle(f'{CP.carFingerprint} - Max lateral acceleration events')
plt.title(', '.join(args.route))
plt.scatter([ev.speed for ev in events], [ev.lateral_accel for ev in events], label='max lateral accel events')
plt.plot([0, 35], [3, 3], c='r', label='ISO 11270 - 3 m/s^2')
plt.plot([0, 35], [-3, -3], c='r')
plt.plot([0, 35], [perc_left_accel, perc_left_accel], c='g', linestyle='--', label='90th percentile left lateral accel')
plt.plot([0, 35], [perc_right_accel, perc_right_accel], c='#ff7f0e', linestyle='--', label='90th percentile right lateral accel')
plt.text(0.4, float(perc_left_accel + 0.4), f'{perc_left_accel:.2f} m/s^2', verticalalignment='center', fontsize=12)
plt.text(0.4, float(perc_right_accel - 0.4), f'{perc_right_accel:.2f} m/s^2', verticalalignment='center', fontsize=12)
plt.xlim(0, 35)
plt.ylim(-5, 5)
plt.xlabel('speed (m/s)')
plt.ylabel('lateral acceleration (m/s^2)')
plt.legend()
plt.show(block=True)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
import os
import argparse
import struct
from collections import deque
from statistics import mean
from openpilot.cereal import log
import openpilot.cereal.messaging as messaging
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Sniff a communication socket')
parser.add_argument('--addr', default='127.0.0.1')
args = parser.parse_args()
if args.addr != "127.0.0.1":
os.environ["ZMQ"] = "1"
messaging.reset_context()
poller = messaging.Poller()
messaging.sub_sock('can', poller, addr=args.addr)
active = 0
start_t = 0
start_v = 0
max_v = 0
max_t = 0
window = deque(maxlen=10)
avg = 0
while 1:
polld = poller.poll(1000)
for sock in polld:
msg = sock.receive()
with log.Event.from_bytes(msg) as log_evt:
evt = log_evt
for item in evt.can:
if item.address == 0xe4 and item.src == 128:
torque_req = struct.unpack('!h', item.dat[0:2])[0]
# print(torque_req)
active = abs(torque_req) > 0
if abs(torque_req) < 100:
if max_v > 5:
print(f'{start_v} -> {max_v} = {round(max_v - start_v, 2)} over {round(max_t - start_t, 2)}s')
start_t = evt.logMonoTime / 1e9
start_v = avg
max_t = 0
max_v = 0
if item.address == 0x1ab and item.src == 0:
motor_torque = ((item.dat[0] & 0x3) << 8) + item.dat[1]
window.append(motor_torque)
avg = mean(window)
#print(f'{evt.logMonoTime}: {avg}')
if active and avg > max_v + 0.5:
max_v = avg
max_t = evt.logMonoTime / 1e9
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import sys
import argparse
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, SESSION_TYPE, DTC_REPORT_TYPE, DTC_STATUS_MASK_TYPE, get_dtc_num_as_str, get_dtc_status_names
from opendbc.car.structs import CarParams
from panda import Panda
parser = argparse.ArgumentParser(description="read DTC status")
parser.add_argument("addr", type=lambda x: int(x,0))
parser.add_argument("--bus", type=int, default=0)
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, args.addr, bus=args.bus)
print("extended diagnostic session ...")
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
print("read diagnostic codes ...")
data = uds_client.read_dtc_information(DTC_REPORT_TYPE.DTC_BY_STATUS_MASK, DTC_STATUS_MASK_TYPE.ALL)
print("status availability:", " ".join(get_dtc_status_names(data[0])))
print("DTC status:")
for i in range(1, len(data), 4):
dtc_num = get_dtc_num_as_str(data[i:i+3])
dtc_status = " ".join(get_dtc_status_names(data[i+3]))
print(dtc_num, dtc_status)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
from sklearn import linear_model
from opendbc.car.toyota.values import STEER_THRESHOLD
from openpilot.tools.lib.logreader import LogReader
MIN_SAMPLES = 30 * 100
def to_signed(n, bits):
if n >= (1 << max((bits - 1), 0)):
n = n - (1 << max(bits, 0))
return n
def get_eps_factor(lr, plot=False):
engaged = False
steering_pressed = False
torque_cmd, eps_torque = None, None
cmds, eps = [], []
for msg in lr:
if msg.which() != 'can':
continue
for m in msg.can:
if m.address == 0x2e4 and m.src == 128:
engaged = bool(m.dat[0] & 1)
torque_cmd = to_signed((m.dat[1] << 8) | m.dat[2], 16)
elif m.address == 0x260 and m.src == 0:
eps_torque = to_signed((m.dat[5] << 8) | m.dat[6], 16)
steering_pressed = abs(to_signed((m.dat[1] << 8) | m.dat[2], 16)) > STEER_THRESHOLD
if engaged and torque_cmd is not None and eps_torque is not None and not steering_pressed:
cmds.append(torque_cmd)
eps.append(eps_torque)
else:
if len(cmds) > MIN_SAMPLES:
break
cmds, eps = [], []
if len(cmds) < MIN_SAMPLES:
raise Exception("too few samples found in route")
lm = linear_model.LinearRegression(fit_intercept=False)
lm.fit(np.array(cmds).reshape(-1, 1), eps)
scale_factor = 1. / lm.coef_[0]
if plot:
plt.plot(np.array(eps) * scale_factor)
plt.plot(cmds)
plt.show()
return scale_factor
if __name__ == "__main__":
lr = LogReader(sys.argv[1])
n = get_eps_factor(lr, plot="--plot" in sys.argv)
print("EPS torque factor: ", n)
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import argparse
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.carlog import carlog
from opendbc.car.vin import get_vin
from openpilot.selfdrive.car.card import can_comm_callbacks
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get VIN of the car')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--bus', type=int, default=1)
parser.add_argument('--timeout', type=float, default=0.1)
parser.add_argument('--retry', type=int, default=5)
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
sendcan = messaging.pub_sock('sendcan')
logcan = messaging.sub_sock('can')
can_callbacks = can_comm_callbacks(logcan, sendcan)
time.sleep(1)
vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (args.bus,), args.timeout, args.retry)
print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}')
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
import argparse
import struct
from enum import IntEnum
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, MessageTimeoutError, NegativeResponseError, SESSION_TYPE,\
DATA_IDENTIFIER_TYPE, ACCESS_TYPE
from opendbc.car.structs import CarParams
from panda import Panda
from datetime import date
# TODO: extend UDS library to allow custom/vendor-defined data identifiers without ignoring type checks
class VOLKSWAGEN_DATA_IDENTIFIER_TYPE(IntEnum):
CODING = 0x0600
# TODO: extend UDS library security_access() to take an access level offset per ISO 14229-1:2020 10.4 and remove this
class ACCESS_TYPE_LEVEL_1(IntEnum):
REQUEST_SEED = ACCESS_TYPE.REQUEST_SEED + 2
SEND_KEY = ACCESS_TYPE.SEND_KEY + 2
MQB_EPS_CAN_ADDR = 0x712
RX_OFFSET = 0x6a
if __name__ == "__main__":
desc_text = "Shows Volkswagen EPS software and coding info, and enables or disables Heading Control Assist " + \
"(Lane Assist). Useful for enabling HCA on cars without factory Lane Assist that want to use " + \
"openpilot integrated at the CAN gateway (J533)."
epilog_text = "This tool is meant to run directly on a vehicle-installed comma three, with the " + \
"openpilot/tmux processes stopped. It should also work on a separate PC with a USB-attached comma " + \
"panda. Vehicle ignition must be on. Recommend engine not be running when making changes. Must " + \
"turn ignition off and on again for any changes to take effect."
parser = argparse.ArgumentParser(description=desc_text, epilog=epilog_text)
parser.add_argument("--debug", action="store_true", help="enable ISO-TP/UDS stack debugging output")
parser.add_argument("action", choices={"show", "enable", "disable"}, help="show or modify current EPS HCA config")
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, 1, timeout=0.2)
try:
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
except MessageTimeoutError:
print("Timeout opening session with EPS")
quit()
odx_file, current_coding = None, None
try:
hw_pn = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_HARDWARE_NUMBER).decode("utf-8")
sw_pn = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_SPARE_PART_NUMBER).decode("utf-8")
sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8")
component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8")
odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00')
current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING)
coding_text = current_coding.hex()
print("\nEPS diagnostic data\n")
print(f" Part No HW: {hw_pn}")
print(f" Part No SW: {sw_pn}")
print(f" SW Version: {sw_ver}")
print(f" Component: {component}")
print(f" Coding: {coding_text}")
print(f" ASAM Dataset: {odx_file}")
except NegativeResponseError:
print("Error fetching data from EPS")
quit()
except MessageTimeoutError:
print("Timeout fetching data from EPS")
quit()
coding_variant, current_coding_array, coding_byte, coding_bit = None, None, 0, 0
coding_length = len(current_coding)
# EPS_MQB_ZFLS
if odx_file in ("EV_SteerAssisMQB", "EV_SteerAssisMNB"):
coding_variant = "ZFLS"
coding_byte = 0
coding_bit = 4
# MQB_PP_APA, MQB_VWBS_GEN2
elif odx_file in ("EV_SteerAssisVWBSMQBA", "EV_SteerAssisVWBSMQBGen2"):
coding_variant = "APA"
coding_byte = 3
coding_bit = 0
else:
print("Configuration changes not yet supported on this EPS!")
quit()
current_coding_array = struct.unpack(f"!{coding_length}B", current_coding)
hca_enabled = (current_coding_array[coding_byte] & (1 << coding_bit) != 0)
hca_text = ("DISABLED", "ENABLED")[hca_enabled]
print(f" Lane Assist: {hca_text}")
try:
params = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION).decode("utf-8")
param_version_system_params = params[1:3]
param_vehicle_type = params[3:5]
param_index_char_curve = params[5:7]
param_version_char_values = params[7:9]
param_version_memory_map = params[9:11]
print("\nEPS parameterization (per-vehicle calibration) data\n")
print(f" Version of system parameters: {param_version_system_params}")
print(f" Vehicle type: {param_vehicle_type}")
print(f" Index of characteristic curve: {param_index_char_curve}")
print(f" Version of characteristic values: {param_version_char_values}")
print(f" Version of memory map: {param_version_memory_map}")
except (NegativeResponseError, MessageTimeoutError):
print("Error fetching parameterization data from EPS!")
quit()
if args.action in ["enable", "disable"]:
print("\nAttempting configuration update")
assert(coding_variant in ("ZFLS", "APA"))
# ZFLS EPS config coding length can be anywhere from 1 to 4 bytes, but the
# bit we care about is always in the same place in the first byte
if args.action == "enable":
new_byte = current_coding_array[coding_byte] | (1 << coding_bit)
else:
new_byte = current_coding_array[coding_byte] & ~(1 << coding_bit)
new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:]
try:
seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED)
key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that
uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key))
except (NegativeResponseError, MessageTimeoutError):
print("Security access failed!")
print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)")
quit()
try:
# Programming date and tester number must be written before making
# a change, or write to CODING will fail with request sequence error
# Encoding on tester is unclear, it contains the workshop code in the
# last two bytes, but not the VZ/importer or tester serial number
# Can't seem to read it back, but we can read the calibration tester,
# so fib a little and say that same tester did the programming
current_date = date.today()
formatted_date = current_date.strftime('%y-%m-%d')
year, month, day = (int(part) for part in formatted_date.split('-'))
prog_date = bytes([year, month, day])
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num)
uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding)
except (NegativeResponseError, MessageTimeoutError):
print("Writing new configuration failed!")
print("Make sure the comma processes are stopped: tmux kill-session -t comma")
quit()
try:
# Read back result just to make 100% sure everything worked
current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex()
print(f" New coding: {current_coding_text}")
except (NegativeResponseError, MessageTimeoutError):
print("Reading back updated coding failed!")
quit()
print("EPS configuration successfully updated")
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import sys
import math
import datetime
from collections import Counter
from pprint import pprint
from typing import cast
from openpilot.cereal.services import SERVICE_LIST
from openpilot.tools.lib.logreader import LogReader, ReadMode
from openpilot.selfdrive.test.process_replay.migration import migrate_all
if __name__ == "__main__":
cnt_events: Counter = Counter()
cams = [s for s in SERVICE_LIST if s.endswith('CameraState')]
cnt_cameras = dict.fromkeys(cams, 0)
events: list[tuple[float, set[str]]] = []
alerts: list[tuple[float, str]] = []
start_time = math.inf
end_time = -math.inf
ignition_off = None
for msg in migrate_all(LogReader(sys.argv[1], ReadMode.QLOG)):
t = (msg.logMonoTime - start_time) / 1e9
end_time = max(end_time, msg.logMonoTime)
start_time = min(start_time, msg.logMonoTime)
if msg.which() == 'onroadEvents':
for e in msg.onroadEvents:
cnt_events[e.name] += 1
ae = {str(e.name) for e in msg.onroadEvents if e.name not in ('pedalPressed', 'steerOverride', 'gasPressedOverride')}
if len(events) == 0 or ae != events[-1][1]:
events.append((t, ae))
elif msg.which() == 'selfdriveState':
at = msg.selfdriveState.alertType
if "/override" not in at or "lanechange" in at.lower():
if len(alerts) == 0 or alerts[-1][1] != at:
alerts.append((t, at))
elif msg.which() == 'pandaStates':
if ignition_off is None:
ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates)
if not ign:
ignition_off = msg.logMonoTime
break
elif msg.which() in cams:
cnt_cameras[msg.which()] += 1
duration = (end_time - start_time) / 1e9
print("Events")
pprint(cnt_events)
print("\n")
print("Events")
for t, evt in events:
print(f"{t:8.2f} {evt}")
print("\n")
print("Cameras")
for k, v in cnt_cameras.items():
s = SERVICE_LIST[k]
expected_frames = int(s.frequency * duration / cast(float, s.decimation))
print(" ", k.ljust(20), f"{v}, {v/expected_frames:.1%} of expected")
print("\n")
print("Alerts")
for t, a in alerts:
print(f"{t:8.2f} {a}")
print("\n")
if ignition_off is not None:
ignition_off = round((ignition_off - start_time) / 1e9, 2)
print("Ignition off at", ignition_off)
print("Route duration", datetime.timedelta(seconds=duration))
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import time
import random
from openpilot.cereal import log
from opendbc.car.structs import car
import openpilot.cereal.messaging as messaging
from opendbc.car.honda.interface import CarInterface
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.selfdrived.events import ET, Events
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager
from openpilot.system.manager.process_config import managed_processes
EventName = log.OnroadEvent.EventName
def randperc() -> float:
return 100. * random.random()
def cycle_alerts(duration=200, is_metric=False):
# all alerts
#alerts = list(EVENTS.keys())
# this plays each type of audible alert
alerts = [
(EventName.buttonEnable, ET.ENABLE),
(EventName.buttonCancel, ET.USER_DISABLE),
(EventName.wrongGear, ET.NO_ENTRY),
(EventName.locationdTemporaryError, ET.SOFT_DISABLE),
(EventName.paramsdTemporaryError, ET.SOFT_DISABLE),
(EventName.accFaulted, ET.IMMEDIATE_DISABLE),
# DM sequence
(EventName.driverDistracted1, ET.WARNING),
(EventName.driverDistracted2, ET.WARNING),
(EventName.driverDistracted3, ET.WARNING),
]
# debug alerts
alerts = [
#(EventName.highCpuUsage, ET.NO_ENTRY),
#(EventName.lowMemory, ET.PERMANENT),
#(EventName.overheat, ET.PERMANENT),
#(EventName.outOfSpace, ET.PERMANENT),
#(EventName.modeldLagging, ET.PERMANENT),
#(EventName.processNotRunning, ET.NO_ENTRY),
#(EventName.commIssue, ET.NO_ENTRY),
#(EventName.calibrationInvalid, ET.PERMANENT),
(EventName.cameraMalfunction, ET.PERMANENT),
(EventName.cameraFrameRate, ET.PERMANENT),
]
cameras = ['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']
CS = car.CarState.new_message()
CP = CarInterface.get_non_essential_params("HONDA_CIVIC")
sm = messaging.SubMaster(['deviceState', 'pandaStates', 'narrowRoadCameraState', 'modelV2', 'extrinsicsCalibration',
'driverMonitoringState', 'longitudinalPlan', 'deviceMotion',
'managerState'] + cameras)
pm = messaging.PubMaster(['selfdriveState', 'pandaStates', 'deviceState'])
events = Events()
AM = AlertManager()
frame = 0
while True:
for alert, et in alerts:
events.clear()
events.add(alert)
sm['deviceState'].freeSpacePercent = randperc()
sm['deviceState'].memoryUsagePercent = int(randperc())
sm['deviceState'].cpuTempC = [randperc() for _ in range(3)]
sm['deviceState'].gpuTempC = [randperc() for _ in range(3)]
sm['deviceState'].cpuUsagePercent = [int(randperc()) for _ in range(8)]
sm['modelV2'].frameDropPerc = randperc()
if random.random() > 0.25:
sm['modelV2'].velocity.x = [random.random(), ]
if random.random() > 0.25:
CS.vEgo = random.random()
procs = [p.get_process_state_msg() for p in managed_processes.values()]
random.shuffle(procs)
for i in range(random.randint(0, 10)):
procs[i].shouldBeRunning = True
sm['managerState'].processes = procs
sm['extrinsicsCalibration'].rpyCalib = [-1 * random.random() for _ in range(random.randint(0, 3))]
for s in sm.data.keys():
prob = 0.3 if s in cameras else 0.08
sm.alive[s] = random.random() > prob
sm.valid[s] = random.random() > prob
sm.freq_ok[s] = random.random() > prob
a = events.create_alerts([et, ], [CP, CS, sm, is_metric, 0])
AM.add_many(frame, a)
alert = AM.process_alerts(frame, [])
print(alert)
for _ in range(duration):
dat = messaging.new_message('selfdriveState')
dat.selfdriveState.enabled = False
if alert:
dat.selfdriveState.alertText1 = alert.alert_text_1
dat.selfdriveState.alertText2 = alert.alert_text_2
dat.selfdriveState.alertSize = alert.alert_size
dat.selfdriveState.alertStatus = alert.alert_status
dat.selfdriveState.alertType = alert.alert_type
dat.selfdriveState.alertSound = alert.audible_alert
pm.send('selfdriveState', dat)
dat = messaging.new_message('deviceState')
dat.deviceState.started = True
pm.send('deviceState', dat)
dat = messaging.new_message('pandaStates', 1)
dat.pandaStates[0].ignitionLine = True
dat.pandaStates[0].pandaType = log.PandaState.PandaType.uno
pm.send('pandaStates', dat)
frame += 1
time.sleep(DT_CTRL)
if __name__ == '__main__':
cycle_alerts()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import argparse
from opendbc.car import uds
from openpilot.tools.lib.live_logreader import live_logreader
from openpilot.tools.lib.logreader import LogReader, ReadMode
def main(route: str | None, addrs: list[int], rxoffset: int | None):
"""
TODO:
- highlight TX vs RX clearly
- disambiguate sendcan and can (useful to know if something sent on sendcan made it to the bus on can->128)
- print as fixed width table, easier to read
"""
if route is None:
lr = live_logreader()
else:
lr = LogReader(route, default_mode=ReadMode.RLOG, sort_by_time=True)
start_mono_time = None
prev_mono_time = 0
# include rx addresses
addrs = addrs + [uds.get_rx_addr_for_tx_addr(addr, rxoffset) for addr in addrs]
for msg in lr:
if msg.which() == 'can':
if start_mono_time is None:
start_mono_time = msg.logMonoTime
if msg.which() in ("can", 'sendcan'):
for can in getattr(msg, msg.which()):
if can.address in addrs or not len(addrs):
if msg.logMonoTime != prev_mono_time:
print()
prev_mono_time = msg.logMonoTime
print(f"{msg.which():>7}: rxaddr={can.address}, bus={str(can.src) + ',':<4} {round((msg.logMonoTime - start_mono_time) * 1e-6)} ms, " +
f"0x{can.dat.hex()}, {can.dat}, {len(can.dat)=}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View back and forth ISO-TP communication between various ECUs given an address')
parser.add_argument('route', nargs='?', help='Route name, live if not specified')
parser.add_argument('--addrs', nargs='*', default=[], help='List of tx address to view (0x7e0 for engine)')
parser.add_argument('--rxoffset', default='0x8')
args = parser.parse_args()
addrs = [int(addr, base=16) if addr.startswith('0x') else int(addr) for addr in args.addrs]
rxoffset = int(args.rxoffset, base=16) if args.rxoffset else None
main(args.route, addrs, rxoffset)
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import argparse
import shlex
import subprocess
import threading
import time
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from openpilot.common.basedir import BASEDIR
def build_rsync_cmd(args) -> list[str]:
ssh = [
"ssh",
"-o", "ControlMaster=auto",
"-o", f"ControlPath=/tmp/devsync-{args.ip}.ctl",
"-o", "ControlPersist=10m",
"-o", "StrictHostKeyChecking=accept-new",
]
if args.identity:
ssh += ["-i", args.identity]
return [
"rsync", "-az",
"--files-from=-", "--from0",
"-e", " ".join(shlex.quote(p) for p in ssh),
"--out-format=%n",
BASEDIR + "/", f"comma@{args.ip}:{args.remote}/",
]
def git_tracked_files() -> bytes:
return subprocess.check_output(
["git", "-C", BASEDIR, "ls-files", "--recurse-submodules", "-z"]
)
class Handler(FileSystemEventHandler):
def __init__(self, sync_fn):
self.dirty = threading.Event()
self.sync_fn = sync_fn
def on_any_event(self, event):
if not event.is_directory:
self.dirty.set()
def run(self):
while True:
time.sleep(1)
if self.dirty.is_set():
self.dirty.clear()
self.sync_fn()
def main():
p = argparse.ArgumentParser()
p.add_argument("ip", help="device IP / hostname")
p.add_argument("--remote", default="/data/openpilot", help="remote path on device")
p.add_argument("-i", "--identity", default=None, help="ssh identity file")
args = p.parse_args()
print(f"[devsync] watching {BASEDIR}")
print(f"[devsync] target comma@{args.ip}:{args.remote}")
def run_sync():
file_list = git_tracked_files()
cmd = build_rsync_cmd(args)
t0 = time.monotonic()
r = subprocess.run(cmd, input=file_list, capture_output=True)
dt = time.monotonic() - t0
if r.returncode:
print(f"[devsync] ERR rc={r.returncode} in {dt:.2f}s")
return
files = [ln for ln in r.stdout.decode().splitlines() if ln.strip()]
msg = f"{len(files)} files: {', '.join(files)}" if files else "no changes"
print(f"[devsync] {dt:.2f}s · {msg}")
run_sync()
handler = Handler(run_sync)
obs = Observer()
obs.schedule(handler, BASEDIR, recursive=True)
obs.start()
handler.run()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[devsync] stopping")
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import sys
import argparse
import json
import codecs
from openpilot.cereal import log
from openpilot.cereal.services import SERVICE_LIST
from openpilot.tools.lib.live_logreader import raw_live_logreader
codecs.register_error("strict", codecs.backslashreplace_errors)
def hexdump(msg):
m = str.upper(msg.hex())
m = [m[i:i+2] for i in range(0,len(m),2)]
m = [m[i:i+16] for i in range(0,len(m),16)]
for row,dump in enumerate(m):
addr = '%08X:' % (row*16)
raw = ' '.join(dump[:8]) + ' ' + ' '.join(dump[8:])
space = ' ' * (48 - len(raw))
asci = ''.join(chr(int(x,16)) if 0x20 <= int(x,16) <= 0x7E else '.' for x in dump)
print(f'{addr} {raw} {space} {asci}')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Dump communication sockets. See openpilot/cereal/services.py for a complete list of available sockets.')
parser.add_argument('--pipe', action='store_true')
parser.add_argument('--raw', action='store_true')
parser.add_argument('--json', action='store_true')
parser.add_argument('--dump-json', action='store_true')
parser.add_argument('--no-print', action='store_true')
parser.add_argument('--addr', default='127.0.0.1')
parser.add_argument('--values', help='values to monitor (instead of entire event)')
parser.add_argument("socket", type=str, nargs='*', default=list(SERVICE_LIST.keys()), help="socket names to dump. defaults to all services defined in cereal")
args = parser.parse_args()
lr = raw_live_logreader(args.socket, args.addr)
values = None
if args.values:
values = [s.strip().split(".") for s in args.values.split(",")]
for msg in lr:
with log.Event.from_bytes(msg) as evt:
if not args.no_print:
if args.pipe:
sys.stdout.write(str(msg))
sys.stdout.flush()
elif args.raw:
hexdump(msg)
elif args.json:
print(json.loads(msg))
elif args.dump_json:
print(json.dumps(evt.to_dict()))
elif values:
print(f"logMonotime = {evt.logMonoTime}")
for value in values:
if hasattr(evt, value[0]):
item = evt
for key in value:
item = getattr(item, key)
print(f"{'.'.join(value)} = {item}")
print("")
else:
try:
print(evt)
except UnicodeDecodeError:
w = evt.which()
s = f"( logMonoTime {evt.logMonoTime} \n {w} = "
s += str(evt.__getattr__(w))
s += f"\n valid = {evt.valid} )"
print(s)
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import os
import sys
import wave
import argparse
import numpy as np
from openpilot.tools.lib.logreader import LogReader, ReadMode
def extract_audio(route_or_segment_name, output_file=None, play=False):
lr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE)
audio_messages = list(lr.filter("rawAudioData"))
if not audio_messages:
print("No rawAudioData messages found in logs")
return
sample_rate = audio_messages[0].sampleRate
audio_chunks = []
total_frames = 0
for msg in audio_messages:
audio_array = np.frombuffer(msg.data, dtype=np.int16)
audio_chunks.append(audio_array)
total_frames += len(audio_array)
full_audio = np.concatenate(audio_chunks)
print(f"Found {total_frames} frames from {len(audio_messages)} audio messages at {sample_rate} Hz")
if output_file:
if write_wav_file(output_file, full_audio, sample_rate):
print(f"Audio written to {output_file}")
else:
print("Audio extraction canceled.")
if play:
play_audio(full_audio, sample_rate)
def write_wav_file(filename, audio_data, sample_rate):
if os.path.exists(filename):
if input(f"File '{filename}' exists. Overwrite? (y/N): ").lower() not in ['y', 'yes']:
return False
with wave.open(filename, 'wb') as wav_file:
wav_file.setnchannels(1) # Mono
wav_file.setsampwidth(2) # 16-bit
wav_file.setframerate(sample_rate)
wav_file.writeframes(audio_data.tobytes())
return True
def play_audio(audio_data, sample_rate):
try:
import sounddevice as sd
print("Playing audio... Press Ctrl+C to stop")
sd.play(audio_data, sample_rate)
sd.wait()
except KeyboardInterrupt:
print("\nPlayback stopped")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract audio data from openpilot logs")
parser.add_argument("-o", "--output", help="Output WAV file path")
parser.add_argument("--play", action="store_true", help="Play audio with sounddevice")
parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name")
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
args = parser.parse_args()
output_file = args.output
if not args.output and not args.play:
output_file = "extracted_audio.wav"
extract_audio(args.route_or_segment_name.strip(), output_file, args.play)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
import argparse
import json
import openpilot.cereal.messaging as messaging
from openpilot.tools.lib.logreader import LogReader
LEVELS = {
"DEBUG": 10,
"INFO": 20,
"WARNING": 30,
"ERROR": 40,
"CRITICAL": 50,
}
OPERATING_SYSTEM_LOG_SOURCE = {
0: "MAIN",
1: "RADIO",
2: "EVENTS",
3: "SYSTEM",
4: "CRASH",
5: "KERNEL",
}
def print_logmessage(t, msg, min_level):
try:
log = json.loads(msg)
if log['levelnum'] >= min_level:
print(f"[{t / 1e9:.6f}] {log['filename']}:{log.get('lineno', '')} - {log.get('funcname', '')}: {log['msg']}")
if 'exc_info' in log:
print(log['exc_info'])
except json.decoder.JSONDecodeError:
print(f"[{t / 1e9:.6f}] decode error: {msg}")
def print_operating_system_log(t, msg):
source = msg.tag or OPERATING_SYSTEM_LOG_SOURCE.get(msg.id, "SYSTEM")
try:
m = json.loads(msg.message)['MESSAGE']
except Exception:
m = msg.message
print(f"[{t / 1e9:.6f}] {source} {msg.pid} {msg.tag} - {m}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--absolute', action='store_true')
parser.add_argument('--level', default='DEBUG')
parser.add_argument('--addr', default='127.0.0.1')
parser.add_argument("route", type=str, nargs='*', help="route name + segment number for offline usage")
args = parser.parse_args()
min_level = LEVELS[args.level]
if args.route:
st = None if not args.absolute else 0
for route in args.route:
lr = LogReader(route, sort_by_time=True)
for m in lr:
if st is None:
st = m.logMonoTime
if m.which() == 'logMessage':
print_logmessage(m.logMonoTime-st, m.logMessage, min_level)
elif m.which() == 'errorLogMessage':
print_logmessage(m.logMonoTime-st, m.errorLogMessage, min_level)
elif m.which() == 'operatingSystemLog':
print_operating_system_log(m.logMonoTime-st, m.operatingSystemLog)
else:
sm = messaging.SubMaster(['logMessage', 'operatingSystemLog'], addr=args.addr)
while True:
sm.update()
if sm.updated['logMessage']:
print_logmessage(sm.logMonoTime['logMessage'], sm['logMessage'], min_level)
if sm.updated['operatingSystemLog']:
print_operating_system_log(sm.logMonoTime['operatingSystemLog'], sm['operatingSystemLog'])
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import sys
from openpilot.tools.lib.logreader import LogReader, ReadMode
def get_fingerprint(lr):
# TODO: make this a nice tool for car ports. should also work with qlogs for FW
fw = None
vin = None
msgs = {}
for msg in lr:
if msg.which() == 'carParams':
fw = msg.carParams.carFw
vin = msg.carParams.carVin
elif msg.which() == 'can':
for c in msg.can:
# read also msgs sent by EON on CAN bus 0x80 and filter out the
# addr with more than 11 bits
if c.src % 0x80 == 0 and c.address < 0x800 and c.address not in (0x7df, 0x7e0, 0x7e8):
msgs[c.address] = len(c.dat)
# show CAN fingerprint
fingerprint = ', '.join(f"{v[0]}: {v[1]}" for v in sorted(msgs.items()))
print(f"\nfound {len(msgs)} messages. CAN fingerprint:\n")
print(fingerprint)
# TODO: also print the fw fingerprint merged with the existing ones
# show FW fingerprint
if fw:
print("\nFW fingerprint:\n")
for f in fw:
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
print(f" {f.fwVersion},")
print(" ],")
print()
print(f"VIN: {vin}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: ./fingerprint_from_route.py <route>")
sys.exit(1)
lr = LogReader(sys.argv[1], ReadMode.QLOG)
get_fingerprint(lr)
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import random
from collections import defaultdict
from tqdm import tqdm
from opendbc.car.fw_versions import match_fw_to_car_fuzzy
from opendbc.car.toyota.values import FW_VERSIONS as TOYOTA_FW_VERSIONS
from opendbc.car.honda.values import FW_VERSIONS as HONDA_FW_VERSIONS
from opendbc.car.hyundai.values import FW_VERSIONS as HYUNDAI_FW_VERSIONS
from opendbc.car.volkswagen.values import FW_VERSIONS as VW_FW_VERSIONS
FWS = {}
FWS.update(TOYOTA_FW_VERSIONS)
FWS.update(HONDA_FW_VERSIONS)
FWS.update(HYUNDAI_FW_VERSIONS)
FWS.update(VW_FW_VERSIONS)
if __name__ == "__main__":
total = 0
match = 0
wrong_match = 0
confusions = defaultdict(set)
for _ in tqdm(range(1000)):
for candidate, fws in FWS.items():
fw_dict = {}
for (_, addr, subaddr), fw_list in fws.items():
fw_dict[(addr, subaddr)] = [random.choice(fw_list)]
matches = match_fw_to_car_fuzzy(fw_dict, log=False, exclude=candidate)
total += 1
if len(matches) == 1:
if list(matches)[0] == candidate:
match += 1
else:
confusions[candidate] |= matches
wrong_match += 1
print()
for candidate, wrong_matches in sorted(confusions.items()):
print(candidate, wrong_matches)
print()
print(f"Total fuzz cases: {total}")
print(f"Correct matches: {match}")
print(f"Wrong matches: {wrong_match}")
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# simple script to get a vehicle fingerprint.
# Instructions:
# - connect to a Panda
# - run openpilot/selfdrive/pandad/pandad
# - launching this script
# Note: it's very important that the car is in stock mode, in order to collect a complete fingerprint
# - since some messages are published at low frequency, keep this script running for at least 30s,
# until all messages are received at least once
import openpilot.cereal.messaging as messaging
logcan = messaging.sub_sock('can')
msgs = {}
while True:
lc = messaging.recv_sock(logcan, True)
if lc is None:
continue
for c in lc.can:
# read also msgs sent by EON on CAN bus 0x80 and filter out the
# addr with more than 11 bits
if c.src % 0x80 == 0 and c.address < 0x800 and c.address not in (0x7df, 0x7e0, 0x7e8):
msgs[c.address] = len(c.dat)
fingerprint = ', '.join(f"{v[0]}: {v[1]}" for v in sorted(msgs.items()))
print(f"number of messages {len(msgs)}:")
print(f"fingerprint {fingerprint}")
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import capnp
from collections import defaultdict
from openpilot.cereal.messaging import SubMaster
def cputime_total(ct):
return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq
def cputime_busy(ct):
return ct.user + ct.nice + ct.system + ct.irq + ct.softirq
def proc_cputime_total(ct):
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
def proc_name(proc):
name = proc.name
if len(proc.cmdline):
name = proc.cmdline[0]
if len(proc.exe):
name = proc.exe + " - " + name
return name
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--mem', action='store_true')
parser.add_argument('--cpu', action='store_true')
args = parser.parse_args()
sm = SubMaster(['deviceState', 'procLog'])
last_temp = 0.0
last_mem = 0.0
total_times = [0.]*8
busy_times = [0.]*8
prev_proclog: capnp._DynamicStructReader | None = None
prev_proclog_t: int | None = None
while True:
sm.update()
if sm.updated['deviceState']:
t = sm['deviceState']
last_temp = np.mean(t.cpuTempC)
last_mem = t.memoryUsagePercent
if sm.updated['procLog']:
m = sm['procLog']
cores = [0.]*8
total_times_new = [0.]*8
busy_times_new = [0.]*8
for c in m.cpuTimes:
n = c.cpuNum
total_times_new[n] = cputime_total(c)
busy_times_new[n] = cputime_busy(c)
for n in range(8):
t_busy = busy_times_new[n] - busy_times[n]
t_total = total_times_new[n] - total_times[n]
cores[n] = t_busy / t_total
total_times = total_times_new[:]
busy_times = busy_times_new[:]
print(f"CPU {100.0 * np.mean(cores):.2f}% - RAM: {last_mem:.2f}% - Temp {last_temp:.2f}C")
if args.cpu and prev_proclog is not None and prev_proclog_t is not None:
procs: dict[str, float] = defaultdict(float)
dt = (sm.logMonoTime['procLog'] - prev_proclog_t) / 1e9
for proc in m.procs:
try:
name = proc_name(proc)
prev_proc = [p for p in prev_proclog.procs if proc.pid == p.pid][0]
cpu_time = proc_cputime_total(proc) - proc_cputime_total(prev_proc)
cpu_usage = cpu_time / dt * 100.
procs[name] += cpu_usage
except IndexError:
pass
print("Top CPU usage:")
for k, v in sorted(procs.items(), key=lambda item: item[1], reverse=True)[:10]:
print(f"{k.rjust(70)} {v:.2f} %")
print()
if args.mem:
mems = {}
for proc in m.procs:
name = proc_name(proc)
mems[name] = float(proc.memRss) / 1e6
print("Top memory usage:")
for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]:
print(f"{k.rjust(70)} {v:.2f} MB")
print()
prev_proclog = m
prev_proclog_t = sm.logMonoTime['procLog']
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import argparse
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from openpilot.selfdrive.test.mem_usage import DEMO_ROUTE, print_report
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze memory usage from route logs")
parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path")
parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})")
args = parser.parse_args()
if args.demo:
route = DEMO_ROUTE
elif args.route:
route = args.route
else:
parser.error("provide a route or use --demo")
print(f"Reading logs from: {route}")
proc_logs = []
device_states = []
for msg in LogReader(route):
if msg.which() == 'procLog':
proc_logs.append(msg)
elif msg.which() == 'deviceState':
device_states.append(msg)
print_report(proc_logs, device_states)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
from opendbc.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()
@@ -0,0 +1 @@
clpeak/
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR
if [ ! -d "$DIR/clpeak" ]; then
git clone https://github.com/krrishnarraj/clpeak.git
cd clpeak
git fetch
git checkout ec2d3e70e1abc7738b81f9277c7af79d89b2133b
git reset --hard origin/master
git submodule update --init --recursive --remote
git apply ../run_continuously.patch
fi
cd clpeak
mkdir build || true
cd build
cmake ..
cmake --build .
@@ -0,0 +1,13 @@
diff --git a/src/clpeak.cpp b/src/clpeak.cpp
index 8cb192b..b6fe6f5 100644
--- a/src/clpeak.cpp
+++ b/src/clpeak.cpp
@@ -47,7 +47,7 @@ int clPeak::runAll()
log->xmlOpenTag("clpeak");
log->xmlAppendAttribs("os", OS_NAME);
- for (size_t p = 0; p < platforms.size(); p++)
+ for (size_t p = 0; p < platforms.size(); (p+1 % platforms.size()))
{
if (forcePlatform && (p != specifiedPlatform))
continue;
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
cd /sys/kernel/tracing
echo 1 > tracing_on
echo boot > trace_clock
echo 1000 > buffer_size_kb
# /sys/kernel/tracing/available_events
echo 0 > events/enable
#echo 1 > events/irq/enable
#echo 1 > events/sched/enable
#echo 1 > events/kgsl/enable
#echo 1 > events/camera/enable
echo 1 > events/workqueue/enable
echo > trace
sleep 2
echo 0 > tracing_on
cp trace /tmp/trace
chown comma: /tmp/trace
echo /tmp/trace
@@ -0,0 +1,2 @@
palanteer/
viewer
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
cd $DIR
if [ ! -d palanteer ]; then
git clone https://github.com/dfeneyrou/palanteer
pip install wheel
sudo apt install libunwind-dev libdw-dev
fi
cd palanteer
git pull
mkdir -p build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
pip install --force-reinstall python/dist/palanteer*.whl
cp bin/palanteer $DIR/viewer
@@ -0,0 +1,7 @@
trace_*
tracebox
trace_processor
perfetto/
configs/
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
if [ ! -d perfetto ]; then
git clone https://android.googlesource.com/platform/external/perfetto/
fi
cd perfetto
tools/install-build-deps --linux-arm
tools/gn gen --args='is_debug=false target_os="linux" target_cpu="arm64"' out/linux
tools/ninja -C out/linux tracebox traced traced_probes perfetto
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
DEST=tici:/data/openpilot/selfdrive/debug/profiling/perfetto
scp -r perfetto/out/linux/tracebox $DEST
scp -r perfetto/test/configs $DEST
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR
OUT=trace_
sudo ./tracebox -o $OUT --txt -c configs/scheduling.cfg
sudo chown $USER:$USER $OUT
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
curl -LO https://get.perfetto.dev/trace_processor
chmod +x ./trace_processor
./trace_processor --httpd
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
DEST=tici:/data/openpilot/selfdrive/debug/profiling/perfetto
scp tici:/data/openpilot/selfdrive/debug/profiling/perfetto/trace_* .
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
# find process with name passed in (excluding this process)
for PID in $(pgrep -f $1); do
if [ "$PID" != "$$" ]; then
ps -p $PID -o args
TRACE_PID=$PID
break
fi
done
if [ -z "$TRACE_PID" ]; then
echo "could not find PID for $1"
exit 1
fi
sudo env PATH=$PATH py-spy record -d 5 -o /tmp/perf$TRACE_PID.svg -p $TRACE_PID &&
google-chrome /tmp/perf$TRACE_PID.svg
@@ -0,0 +1 @@
SnapdragonProfiler/
@@ -0,0 +1,13 @@
snapdragon profiler
--------
* download from https://developer.qualcomm.com/software/snapdragon-profiler/tools-archive (need a qc developer account)
* choose v2021.5 (verified working with 24.04 dev environment)
* unzip to openpilot/selfdrive/debug/profiling/snapdragon/SnapdragonProfiler
* run ```./setup-profiler.sh```
* run ```./setup-agnos.sh```
* run ```openpilot/selfdrive/debug/adb.sh``` on device
* run the ```adb connect xxx``` command that was given to you on local pc
* cd to SnapdragonProfiler and run ```./run_sdp.sh```
* connect to device -> choose device you just setup
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# TODO: there's probably a better way to do this
cd SnapdragonProfiler/service
mv android real_android
ln -s agl/ android
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# install depends
sudo apt update
sudo apt-get install libc++1 libc++abi1 default-jre android-tools-adb gtk-sharp2
# setup mono
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
sudo apt install apt-transport-https ca-certificates
echo "deb https://download.mono-project.com/repo/ubuntu stable-xenial main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list
sudo apt update
sudo apt-get install -y mono-complete
echo "Setup successful, you should now be able to run the profiler with cd SnapdragonProfiler and ./run_sdp.sh"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
RUBYOPT="-W0" irqtop -d1 -R
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
import argparse
import zstandard as zstd
from collections import defaultdict
import matplotlib.pyplot as plt
from openpilot.cereal.services import SERVICE_LIST
from openpilot.common.utils import LOG_COMPRESSION_LEVEL
from openpilot.tools.lib.logreader import LogReader
from tqdm import tqdm
MIN_SIZE = 0.5 # Percent size of total to show as separate entry
def make_pie(msgs, typ):
msgs_by_type = defaultdict(list)
for m in msgs:
msgs_by_type[m.which()].append(m.as_builder().to_bytes())
total = len(zstd.compress(b"".join([m.as_builder().to_bytes() for m in msgs]), LOG_COMPRESSION_LEVEL))
uncompressed_total = len(b"".join([m.as_builder().to_bytes() for m in msgs]))
length_by_type = {k: len(b"".join(v)) for k, v in msgs_by_type.items()}
# calculate compressed size by calculating diff when removed from the segment
compressed_length_by_type = {}
for k in tqdm(msgs_by_type.keys(), desc="Compressing"):
compressed_length_by_type[k] = total - len(zstd.compress(b"".join([m.as_builder().to_bytes() for m in msgs if m.which() != k]), LOG_COMPRESSION_LEVEL))
sizes = sorted(compressed_length_by_type.items(), key=lambda kv: kv[1])
print("name - comp. size (uncomp. size)")
for (name, sz) in sizes:
print(f"{name:<22} - {sz / 1024:.2f} kB ({length_by_type[name] / 1024:.2f} kB)")
print()
print(f"{typ} - Real total {total / 1024:.2f} kB")
print(f"{typ} - Breakdown total {sum(compressed_length_by_type.values()) / 1024:.2f} kB")
print(f"{typ} - Uncompressed total {uncompressed_total / 1024 / 1024:.2f} MB")
sizes_large = [(k, sz) for (k, sz) in sizes if sz >= total * MIN_SIZE / 100]
sizes_large += [('other', sum(sz for (_, sz) in sizes if sz < total * MIN_SIZE / 100))]
labels, sizes = zip(*sizes_large, strict=True)
plt.figure()
plt.title(f"{typ}")
plt.pie(sizes, labels=labels, autopct='%1.1f%%')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View log size breakdown by message type')
parser.add_argument('route', help='route to use')
parser.add_argument('--as-qlog', action='store_true', help='decimate rlog using latest decimation factors')
args = parser.parse_args()
msgs = list(LogReader(args.route))
if args.as_qlog:
new_msgs = []
msg_cnts: dict[str, int] = defaultdict(int)
for msg in msgs:
msg_which = msg.which()
if msg.which() in ("initData", "sentinel"):
new_msgs.append(msg)
continue
if msg_which not in SERVICE_LIST:
continue
decimation = SERVICE_LIST[msg_which].decimation
if decimation is not None and msg_cnts[msg_which] % decimation == 0:
new_msgs.append(msg)
msg_cnts[msg_which] += 1
msgs = new_msgs
make_pie(msgs, 'qlog')
plt.show()
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import argparse
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process
from openpilot.selfdrive.test.process_replay.test_processes import EXCLUDED_PROCS
from openpilot.tools.lib.logreader import LogReader, save_log
ALLOW_PROCS = {c.proc_name for c in CONFIGS}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run process on route and create new logs",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("route", help="The route name to use")
parser.add_argument("--fingerprint", help="The fingerprint to use")
parser.add_argument("--whitelist-procs", nargs='*', default=ALLOW_PROCS, help="Whitelist given processes (e.g. controlsd)")
parser.add_argument("--blacklist-procs", nargs='*', default=EXCLUDED_PROCS, help="Blacklist given processes (e.g. controlsd)")
args = parser.parse_args()
allowed_procs = set(args.whitelist_procs) - set(args.blacklist_procs)
cfgs = [c for c in CONFIGS if c.proc_name in allowed_procs]
inputs = list(LogReader(args.route))
outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint)
# Remove message generated by the process under test and merge in the new messages
produces = {o.which() for o in outputs}
inputs = [i for i in inputs if i.which() not in produces]
outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime)
fn = f"{args.route.replace('/', '_')}_{'_'.join(allowed_procs)}.zst"
print(f"Saving log to {fn}")
save_log(fn, outputs)
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
while true; do
if ls /dev/serial/by-id/usb-FTDI_FT230X* 2> /dev/null; then
sudo screen /dev/serial/by-id/usb-FTDI_FT230X* 115200
fi
sleep 0.005
done
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import sys
from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__":
CP = None
if len(sys.argv) > 1:
r = Route(sys.argv[1])
cps = [m for m in LogReader(r.qlog_paths()[0]) if m.which() == 'carParams']
CP = cps[0].carParams.as_builder()
else:
CP = car.CarParams.new_message()
CP.openpilotLongitudinalControl = True
CP.alphaLongitudinalAvailable = False
cp_bytes = CP.to_bytes()
for p in ("CarParams", "CarParamsCache", "CarParamsPersistent"):
Params().put(p, cp_bytes, block=True)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
import requests
from openpilot.common.params import Params
import sys
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"{sys.argv[0]} <github username>")
exit(1)
username = sys.argv[1]
keys = requests.get(f"https://github.com/{username}.keys", timeout=10)
if keys.status_code == 200:
params = Params()
params.put_bool("SshEnabled", True, block=True)
params.put("GithubSshKeys", keys.text, block=True)
params.put("GithubUsername", username, block=True)
print("Set up ssh keys successfully")
else:
print("Error getting public keys from github")
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import re
from openpilot.common.basedir import BASEDIR
from openpilot.tools.lib.auth_config import get_token
from openpilot.tools.lib.api import CommaApi
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="A helper for connecting to devices over the comma prime SSH proxy.\
Adding your SSH key to your SSH config is recommended for more convenient use; see https://docs.comma.ai/how-to/connect-to-comma/.")
parser.add_argument("device", help="device name or dongle id")
parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai")
parser.add_argument("--port", help="ssh jump server port", default=22, type=int)
parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "openpilot/common/hardware/comma/id_rsa"))
parser.add_argument("--debug", help="enable debug output", action="store_true")
args = parser.parse_args()
r = CommaApi(get_token()).get("v1/me/devices")
devices = {x['dongle_id']: x['alias'] for x in r}
if not re.match("[0-9a-zA-Z]{16}", args.device):
user_input = args.device.replace(" ", "").lower()
matches = { k: v for k, v in devices.items() if isinstance(v, str) and user_input in v.replace(" ", "").lower() }
if len(matches) == 1:
dongle_id = list(matches.keys())[0]
else:
print(f"failed to look up dongle id for \"{args.device}\"", file=sys.stderr)
if len(matches) > 1:
print("found multiple matches:", file=sys.stderr)
for k, v in matches.items():
print(f" \"{v}\" ({k})", file=sys.stderr)
exit(1)
else:
dongle_id = args.device
name = dongle_id
if dongle_id in devices:
name = f"{devices[dongle_id]} ({dongle_id})"
print(f"connecting to {name} through {args.host}:{args.port} ...")
command = [
"ssh",
"-i", args.key,
"-o", f"ProxyCommand=ssh -i {args.key} -W %h:%p -p %p %h@{args.host}",
"-p", str(args.port),
]
if args.debug:
command += ["-v"]
command += [
f"comma@comma-{dongle_id}",
]
if args.debug:
print(" ".join([f"'{c}'" if " " in c else c for c in command]))
os.execvp(command[0], command)
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
from collections import defaultdict
import argparse
import os
import traceback
from tqdm import tqdm
from opendbc.car.car_helpers import interface_names
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.fw_versions import VERSIONS, match_fw_to_car
from openpilot.tools.lib.logreader import LogReader, ReadMode
from openpilot.tools.lib.route import SegmentRange
SUPPORTED_BRANDS = VERSIONS.keys()
SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]]
UNKNOWN_BRAND = "unknown"
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Run FW fingerprint on Qlog of route or list of routes')
parser.add_argument('route', help='Route or file with list of routes')
parser.add_argument('--car', help='Force comparison fingerprint to known car')
args = parser.parse_args()
if os.path.exists(args.route):
routes = list(open(args.route))
else:
routes = [args.route]
mismatches = defaultdict(list)
not_fingerprinted = 0
solved_by_fuzzy = 0
good_exact = 0
wrong_fuzzy = 0
good_fuzzy = 0
dongles = []
for route in tqdm(routes):
sr = SegmentRange(route)
dongle_id = sr.dongle_id
if dongle_id in dongles:
continue
if sr.slice == '' and sr.selector is None:
route += '/0'
lr = LogReader(route, default_mode=ReadMode.QLOG)
try:
dongles.append(dongle_id)
CP = None
for msg in lr:
if msg.which() == "pandaStates":
if msg.pandaStates[0].pandaType in ('unknown', 'whitePanda', 'greyPanda', 'pedal'):
print("wrong panda type")
break
elif msg.which() == "carParams":
CP = msg.carParams
car_fw = [fw for fw in CP.carFw if not fw.logging]
if len(car_fw) == 0:
print("WARNING: no fw")
live_fingerprint = CP.carFingerprint
live_fingerprint = MIGRATION.get(live_fingerprint, live_fingerprint)
if args.car is not None:
live_fingerprint = args.car
if live_fingerprint not in SUPPORTED_CARS:
print("not in supported cars")
break
_, exact_matches = match_fw_to_car(car_fw, CP.carVin, allow_exact=True, allow_fuzzy=False)
_, fuzzy_matches = match_fw_to_car(car_fw, CP.carVin, allow_exact=False, allow_fuzzy=True)
if (len(exact_matches) == 1) and (list(exact_matches)[0] == live_fingerprint):
good_exact += 1
print(f"Correct! Live: {live_fingerprint} - Fuzzy: {fuzzy_matches}")
# Check if fuzzy match was correct
if len(fuzzy_matches) == 1:
if list(fuzzy_matches)[0] != live_fingerprint:
wrong_fuzzy += 1
print("Fuzzy match wrong! Fuzzy:", fuzzy_matches, "Live:", live_fingerprint)
else:
good_fuzzy += 1
break
print("Old style:", live_fingerprint, "Vin", CP.carVin)
print("New style (exact):", exact_matches)
print("New style (fuzzy):", fuzzy_matches)
padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw] + [0])
for version in sorted(car_fw, key=lambda fw: fw.brand):
subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}}, bus: {version.bus} - " +
f"(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],")
print("Mismatches")
found = False
for brand in SUPPORTED_BRANDS:
car_fws = VERSIONS[brand]
if live_fingerprint in car_fws:
found = True
expected = car_fws[live_fingerprint]
for (_, expected_addr, expected_sub_addr), v in expected.items():
for version in car_fw:
if version.brand != brand and len(version.brand):
continue
sub_addr = None if version.subAddress == 0 else version.subAddress
addr = version.address
if (addr, sub_addr) == (expected_addr, expected_sub_addr):
if version.fwVersion not in v:
print(f"({hex(addr)}, {'None' if sub_addr is None else hex(sub_addr)}) - {version.fwVersion}")
# Add to global list of mismatches
mismatch = (addr, sub_addr, version.fwVersion)
if mismatch not in mismatches[live_fingerprint]:
mismatches[live_fingerprint].append(mismatch)
# No FW versions for this car yet, add them all to mismatch list
if not found:
for version in car_fw:
sub_addr = None if version.subAddress == 0 else version.subAddress
addr = version.address
mismatch = (addr, sub_addr, version.fwVersion)
if mismatch not in mismatches[live_fingerprint]:
mismatches[live_fingerprint].append(mismatch)
print()
not_fingerprinted += 1
if len(fuzzy_matches) == 1:
if list(fuzzy_matches)[0] == live_fingerprint:
solved_by_fuzzy += 1
else:
wrong_fuzzy += 1
print("Fuzzy match wrong! Fuzzy:", fuzzy_matches, "Live:", live_fingerprint)
break
if CP is None:
print("no CarParams in logs")
except Exception:
traceback.print_exc()
except KeyboardInterrupt:
break
print()
# Print FW versions that need to be added separated out by car and address
for car, m in sorted(mismatches.items()):
print(car)
addrs = defaultdict(list)
for (addr, sub_addr, version) in m:
addrs[(addr, sub_addr)].append(version)
for (addr, sub_addr), versions in addrs.items():
print(f" ({hex(addr)}, {'None' if sub_addr is None else hex(sub_addr)}): [")
for v in versions:
print(f" {v},")
print(" ]")
print()
print()
print(f"Number of dongle ids checked: {len(dongles)}")
print(f"Fingerprinted: {good_exact}")
print(f"Not fingerprinted: {not_fingerprinted}")
print(f" of which had a fuzzy match: {solved_by_fuzzy}")
print()
print(f"Correct fuzzy matches: {good_fuzzy}")
print(f"Wrong fuzzy matches: {wrong_fuzzy}")
print()
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import time
from openpilot.cereal import log, messaging
from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes, is_tinygrad_model, is_stock_model
from openpilot.common.hardware import HARDWARE
if __name__ == "__main__":
CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10)
params = Params()
params.put("CarParams", CP.to_bytes(), block=True)
if use_tinygrad_modeld := is_tinygrad_model(False, params, CP):
print("Using TinyGrad modeld")
if use_stock_modeld := is_stock_model(False, params, CP):
print("Using stock modeld")
HARDWARE.set_power_save(False)
procs = ['camerad', 'ui', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd']
procs += ["modeld_tinygrad" if use_tinygrad_modeld else "modeld"]
for p in procs:
managed_processes[p].start()
pm = messaging.PubMaster(['controlsState', 'deviceState', 'pandaStates', 'carParams'])
msgs = {s: messaging.new_message(s) for s in ['controlsState', 'deviceState', 'carParams']}
msgs['deviceState'].deviceState.started = True
msgs['deviceState'].deviceState.deviceType = HARDWARE.get_device_type()
msgs['carParams'].carParams.openpilotLongitudinalControl = True
msgs['pandaStates'] = messaging.new_message('pandaStates', 1)
msgs['pandaStates'].pandaStates[0].ignitionLine = True
msgs['pandaStates'].pandaStates[0].pandaType = log.PandaState.PandaType.uno
try:
while True:
time.sleep(1 / 100) # continually send, rate doesn't matter
for s in msgs:
pm.send(s, msgs[s])
except KeyboardInterrupt:
for p in procs:
managed_processes[p].stop()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import argparse
import datetime
import time
from collections import deque
from dataclasses import dataclass, field
import numpy as np
import openpilot.cereal.messaging as messaging
from openpilot.cereal.services import SERVICE_LIST
@dataclass
class ServiceTiming:
times: list[float] = field(default_factory=list)
window: deque[float] = field(default_factory=lambda: deque(maxlen=100))
valids: deque[bool] = field(default_factory=lambda: deque(maxlen=100))
lag_events: list[tuple[float, float]] = field(default_factory=list)
def add(self, mono_time: float, valid: bool, expected_interval: float | None, lag_threshold: float) -> None:
if self.times:
dt = mono_time - self.times[-1]
self.window.append(dt)
if expected_interval is not None and dt > lag_threshold * expected_interval:
self.lag_events.append((mono_time, dt))
self.times.append(mono_time)
self.valids.append(valid)
def intervals(self, latest_only: bool) -> np.ndarray:
if latest_only:
return np.array(self.window)
return np.diff(self.times)
def format_row(name: str, timing: ServiceTiming, latest_only: bool) -> str:
dts = timing.intervals(latest_only)
if len(dts) == 0:
return f"{name:25} waiting for messages"
mean = np.mean(dts)
hz = 1.0 / mean if mean > 0 else 0.0
valid = all(timing.valids) if timing.valids else False
return f"{name:25} {hz:8.2f}Hz {mean * 1e3:8.2f}ms {np.std(dts) * 1e3:8.2f}ms {np.max(dts) * 1e3:8.2f}ms {np.min(dts) * 1e3:8.2f}ms valid={valid}"
def print_lag_events(name: str, timing: ServiceTiming, printed_lags: dict[str, int]) -> None:
start = printed_lags.get(name, 0)
for mono_time, dt in timing.lag_events[start:]:
print(f"{mono_time:.3f} {name} lag {dt:.3f}s", flush=True)
printed_lags[name] = len(timing.lag_events)
def monitor_services(socket_names: list[str], print_interval: float, lag_threshold: float, lag_only: bool) -> None:
sockets = {name: messaging.sub_sock(name, conflate=False) for name in socket_names}
timings = {name: ServiceTiming() for name in socket_names}
printed_lags: dict[str, int] = {}
start_time = time.monotonic()
last_print = start_time
try:
while True:
for name, sock in sockets.items():
for msg in messaging.drain_sock(sock):
expected_interval = 1.0 / SERVICE_LIST[name].frequency if name in SERVICE_LIST else None
timings[name].add(msg.logMonoTime / 1e9, msg.valid, expected_interval, lag_threshold)
now = time.monotonic()
if now - last_print < print_interval:
time.sleep(0.01)
continue
if not lag_only:
print(flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=True), flush=True)
for name in socket_names:
print_lag_events(name, timings[name], printed_lags)
last_print = now
except KeyboardInterrupt:
print("\n", flush=True)
print("=" * 5, "timing summary", "=" * 5, flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=False), flush=True)
print("=" * 5, datetime.timedelta(seconds=time.monotonic() - start_time), "=" * 5, flush=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check live service timing, frequency, validity, and lag")
parser.add_argument("socket", nargs="*", default=["carState"], help="service/socket name")
parser.add_argument("--lag-threshold", type=float, default=10.0, help="report intervals above this multiple of the expected service interval")
parser.add_argument("--lag-only", action="store_true", help="only print lag events")
parser.add_argument("--print-interval", type=float, default=1.0, help="seconds between table updates")
args = parser.parse_args()
monitor_services(args.socket, args.print_interval, args.lag_threshold, args.lag_only)
Executable
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
BOLD='\033[1m'
NC='\033[0m'
if [ -z "$OPENPILOT_ROOT" ]; then
# default to current directory for installation
OPENPILOT_ROOT="$(pwd)/openpilot"
fi
function show_motd() {
cat << 'EOF'
.~ssos+.
+8888888888i,
{888888888888o.
h8888888888888k
t888888888s888k
`t88888d/ h88k
``` h88l
,88k`
.d8h`
+d8h
_+d8h`
;y8h+`
|-`
openpilot installer
EOF
}
function check_platform() {
if [[ -f /AGNOS ]]; then
echo -e "[${RED}${NC}] This installer is for PCs only. The environment is pre-configured in AGNOS."
return 1
fi
}
function check_stdin() {
if [ -t 0 ]; then
INTERACTIVE=1
else
echo "Checking for valid invocation..."
echo -e " ↳ [${RED}${NC}] stdin not found! Running in non-interactive mode."
echo -e " Run ${BOLD}'bash <(curl -fsSL openpilot.comma.ai)'${NC} to run in interactive mode.\n"
fi
}
function ask_dir() {
echo -n "Enter directory in which to install openpilot (default $OPENPILOT_ROOT): "
if [[ -z $INTERACTIVE ]]; then
echo -e "\nBecause your are running in non-interactive mode, the installation"
echo -e "will default to $OPENPILOT_ROOT\n"
return 0
fi
read
if [[ ! -z "$REPLY" ]]; then
mkdir -p $REPLY
OPENPILOT_ROOT="$(realpath $REPLY)/openpilot"
fi
}
function check_dir() {
echo "Checking for installation directory..."
if [ -d "$OPENPILOT_ROOT" ]; then
echo -e " ↳ [${RED}${NC}] Installation destination $OPENPILOT_ROOT already exists!"
# not a valid clone, can't continue
if [[ ! -z "$(ls -A $OPENPILOT_ROOT)" && ! -f "$OPENPILOT_ROOT/launch_openpilot.sh" ]]; then
echo -e " $OPENPILOT_ROOT already contains files but does not seems"
echo -e " to be a valid openpilot git clone. Choose another location for"
echo -e " installing openpilot!\n"
return 1
fi
# already a "valid" openpilot clone, skip cloning again
if [[ ! -z "$(ls -A $OPENPILOT_ROOT)" ]]; then
SKIP_GIT_CLONE=1
fi
# by default, don't try installing in already existing directory
if [[ -z $INTERACTIVE ]]; then
return 0
fi
read -p " Would you like to attempt installation anyway? [Y/n] " -n 1 -r
echo -e "\n"
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return 1
fi
return 0
fi
echo -e " ↳ [${GREEN}${NC}] Successfully chosen $OPENPILOT_ROOT as installation directory\n"
}
function check_git() {
echo "Checking for git..."
if ! command -v "git" > /dev/null 2>&1; then
echo -e " ↳ [${RED}${NC}] git not found on your system, can't continue!"
return 1
else
echo -e " ↳ [${GREEN}${NC}] git found.\n"
fi
}
function git_clone() {
st="$(date +%s)"
echo "Cloning openpilot..."
if $(git clone --filter=blob:none https://github.com/commaai/openpilot.git "$OPENPILOT_ROOT"); then
if [[ -f $OPENPILOT_ROOT/launch_openpilot.sh ]]; then
et="$(date +%s)"
echo -e " ↳ [${GREEN}${NC}] Successfully cloned openpilot in $((et - st)) seconds.\n"
return 0
fi
fi
echo -e " ↳ [${RED}${NC}] failed to clone openpilot!"
return 1
}
function install_with_op() {
cd $OPENPILOT_ROOT
$OPENPILOT_ROOT/tools/op.sh post-commit
if ! $OPENPILOT_ROOT/tools/op.sh setup; then
echo -e "\n[${RED}${NC}] failed to install openpilot!"
return 1
fi
echo -e "\n----------------------------------------------------------------------"
echo -e "[${GREEN}${NC}] openpilot was successfully installed into ${BOLD}$OPENPILOT_ROOT${NC}"
echo -e "Checkout the docs at https://docs.comma.ai"
echo -e "Checkout how to contribute at https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md"
}
show_motd
check_platform
check_stdin
ask_dir
check_dir
check_git
[ -z $SKIP_GIT_CLONE ] && git_clone
install_with_op
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
ROOT="$(git -C "$DIR" rev-parse --show-toplevel)"
function retry() {
local attempts=$1
shift
for i in $(seq 1 "$attempts"); do
if "$@"; then
return 0
fi
if [ "$i" -lt "$attempts" ]; then
echo " Attempt $i/$attempts failed, retrying in 5s..."
sleep 5
fi
done
return 1
}
function install_linux_deps() {
SUDO=""
if [[ ! $(id -u) -eq 0 ]]; then
if [[ -z $(which sudo) ]]; then
echo "Please install sudo or run as root"
exit 1
fi
SUDO="sudo"
fi
local missing_linux_deps=0
for cmd in gcc g++ make curl curl-config git; do
if ! command -v "$cmd" > /dev/null 2>&1; then
missing_linux_deps=1
break
fi
done
# ------------------------------------------------
# dependencies should never be added to this list.
# these are only for inflating bare docker images
# to their desktop equivalents.
# ------------------------------------------------
if [[ "$missing_linux_deps" -eq 0 ]]; then
# the native package managers are slow, so skip if we can
echo "[ ] system packages already installed t=$SECONDS"
elif command -v apt-get > /dev/null 2>&1; then
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git xclip wl-clipboard
elif command -v dnf > /dev/null 2>&1; then
$SUDO dnf install -y ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-langpack-en git
elif command -v yum > /dev/null 2>&1; then
$SUDO yum install -y ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-langpack-en git
elif command -v pacman > /dev/null 2>&1; then
$SUDO pacman -Syu --noconfirm --needed base-devel ca-certificates curl git
elif command -v zypper > /dev/null 2>&1; then
$SUDO zypper --non-interactive refresh
$SUDO zypper --non-interactive install ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-locale git
elif command -v apk > /dev/null 2>&1; then
$SUDO apk add --no-cache ca-certificates build-base curl curl-dev musl-locales git
elif command -v xbps-install > /dev/null 2>&1; then
$SUDO xbps-install -Syu base-devel ca-certificates curl git libcurl-devel glibc-locales
else
echo "Unsupported Linux distribution. Supported package managers: apt-get, dnf, yum, pacman, zypper, apk, xbps-install."
exit 1
fi
if [[ -d "/etc/udev/rules.d/" ]]; then
$SUDO tee /etc/udev/rules.d/11-openpilot.rules > /dev/null <<-EOF
# Panda Jungle devices
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcf", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddef", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddcf", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddef", MODE="0666"
# Panda devices
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcc", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddee", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddcc", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddee", MODE="0666"
# comma devices over ADB
SUBSYSTEM=="usb", ATTR{idVendor}=="04d8", ATTR{idProduct}=="1234", ENV{adb_user}="yes"
EOF
# delete the old ones
$SUDO rm -f /etc/udev/rules.d/11-panda.rules /etc/udev/rules.d/12-panda_jungle.rules /etc/udev/rules.d/50-comma-adb.rules
$SUDO udevadm control --reload-rules && $SUDO udevadm trigger || true
fi
}
function install_python_deps() {
# Increase the pip timeout to handle TimeoutError
export PIP_DEFAULT_TIMEOUT=200
cd "$ROOT"
if ! command -v "uv" > /dev/null 2>&1; then
echo "installing uv..."
# TODO: outer retry can be removed once https://github.com/axodotdev/cargo-dist/pull/2311 is merged
retry 3 sh -c 'curl --retry 5 --retry-delay 5 --retry-all-errors -LsSf https://astral.sh/uv/install.sh | UV_GITHUB_TOKEN="${GITHUB_TOKEN:-}" sh'
UV_BIN="$HOME/.local/bin"
PATH="$UV_BIN:$PATH"
fi
echo "updating uv..."
# ok to fail, can also fail due to installing with brew
uv self update || true
echo "installing python packages..."
uv sync --frozen --all-extras
source .venv/bin/activate
}
# --- Main ---
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
install_linux_deps
echo "[ ] installed system dependencies t=$SECONDS"
elif [[ "$OSTYPE" == "darwin"* ]]; then
if [[ $SHELL == "/bin/zsh" ]]; then
RC_FILE="$HOME/.zshrc"
elif [[ $SHELL == "/bin/bash" ]]; then
RC_FILE="$HOME/.bash_profile"
fi
fi
if [ -f "$ROOT/pyproject.toml" ]; then
install_python_deps
echo "[ ] installed python dependencies t=$SECONDS"
fi
if [[ "$OSTYPE" == "darwin"* ]] && [[ -n "${RC_FILE:-}" ]]; then
echo
echo "---- OPENPILOT SETUP DONE ----"
echo "Open a new shell or configure your active shell env by running:"
echo "source $RC_FILE"
fi
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
import argparse
from collections import Counter
from concurrent.futures import as_completed, ProcessPoolExecutor
from itertools import batched
import math
import os
from pathlib import Path
import sys
import tempfile
import time
import traceback
import unittest
import warnings
ROOT = Path(__file__).resolve().parents[1]
IGNORED = (
ROOT / "openpilot/selfdrive/test/process_replay/test_processes.py",
ROOT / "openpilot/tools/sim",
)
FAILURES = {"failed", "error", "xpassed"}
STATUS_MARKS = {
"passed": (".", 32),
"skipped": ("s", 33),
"xfailed": ("x", 36),
"failed": ("F", 31),
"error": ("E", 31),
"xpassed": ("X", 31),
}
def paint(text, code):
if sys.stdout.isatty() and "NO_COLOR" not in os.environ:
return f"\033[{code}m{text}\033[0m"
return text
class Capture:
def __init__(self, enabled):
self.enabled = enabled
def start(self):
if not self.enabled:
return
for stream in (sys.stdout, sys.stderr):
stream.flush()
self.files = [tempfile.TemporaryFile() for _ in range(2)]
self.saved = [os.dup(fd) for fd in (1, 2)]
for fd, file in enumerate(self.files, 1):
os.dup2(file.fileno(), fd)
def stop(self, keep=True):
if not self.enabled:
return "", ""
for stream in (sys.stdout, sys.stderr):
stream.flush()
for fd, saved in enumerate(self.saved, 1):
os.dup2(saved, fd)
os.close(saved)
output = []
for file in self.files:
if keep:
file.seek(0)
output.append(file.read().decode(errors="replace"))
file.close()
return output if keep else ("", "")
def make_record(test_id, status="passed", detail=""):
return {"id": test_id, "status": status, "detail": detail, "time": 0.0, "stdout": "", "stderr": ""}
class Result(unittest.TestResult):
def __init__(self, capture_output):
super().__init__()
self.records = []
self.current = None
self.capture = Capture(capture_output)
def startTest(self, test):
self.current = make_record(test.id())
self.started = time.monotonic()
self.capture.start()
def stopTest(self, test):
self.current["time"] = time.monotonic() - self.started
keep = self.current["status"] in FAILURES
stdout, stderr = self.capture.stop(keep)
if keep:
self.current["stdout"] = stdout
self.current["stderr"] = stderr
self.records.append(self.current)
self.current = None
def mark(self, test, status, detail=""):
if self.current is None: # setUpClass/setUpModule can fail before a test starts
self.records.append(make_record(test.id(), status, detail))
return
if self.current["status"] not in FAILURES or status == "error":
self.current["status"] = status
if detail:
self.current["detail"] += ("\n\n" if self.current["detail"] else "") + detail
def addFailure(self, test, err):
self.mark(test, "failed", self._exc_info_to_string(err, test))
def addError(self, test, err):
self.mark(test, "error", self._exc_info_to_string(err, test))
def addSkip(self, test, reason):
self.mark(test, "skipped")
def addExpectedFailure(self, test, err):
self.mark(test, "xfailed")
def addUnexpectedSuccess(self, test):
self.mark(test, "xpassed", "Test was expected to fail, but passed.")
def addSubTest(self, test, subtest, err):
if err:
status = "failed" if issubclass(err[0], test.failureException) else "error"
self.mark(test, status, f"{subtest}\n{self._exc_info_to_string(err, test)}")
def flatten(suite):
for test in suite:
if isinstance(test, unittest.TestSuite):
yield from flatten(test)
else:
yield test
def module_name(path):
return ".".join(path.resolve().relative_to(ROOT).with_suffix("").parts)
def collect(targets, keyword):
use_ignores = not targets
targets = targets or ["openpilot"]
loader = unittest.TestLoader()
tests = []
errors = []
names = []
for target in targets:
path_text, *nodes = target.split("::")
path = Path(path_text)
try:
if path.is_dir():
files = sorted(path.rglob("test_*.py"))
if use_ignores:
files = [f for f in files if not any(f.resolve().is_relative_to(i) for i in IGNORED)]
names.extend(module_name(file) for file in files)
elif path.is_file():
names.append(".".join((module_name(path), *nodes)))
elif "/" in path_text or path_text.endswith(".py"):
errors.append(f"{target}: file or directory not found")
else:
names.append(target.replace("::", "."))
except (OSError, ValueError) as e:
errors.append(str(e))
for name in dict.fromkeys(names):
before = len(loader.errors)
try:
suite = loader.loadTestsFromName(name)
except Exception:
errors.append(f"Failed to collect {name}\n{traceback.format_exc()}")
continue
errors.extend(loader.errors[before:])
for test in flatten(suite):
cls = type(test)
if cls.__name__ == "_FailedTest":
continue
if getattr(cls, "__unittest_skip_why__", "") == "parameterized base class":
continue
if not keyword or keyword.lower() in test.id().lower():
tests.append(test)
return list({test.id(): test for test in tests}.values()), errors
def make_batches(tests, workers):
fixture_groups = {}
parallel = []
for test in tests:
cls = type(test)
module = sys.modules[cls.__module__]
if hasattr(module, "setUpModule") or hasattr(module, "tearDownModule"):
key = cls.__module__
elif "setUpClass" in cls.__dict__ or "tearDownClass" in cls.__dict__:
key = f"{cls.__module__}.{cls.__qualname__}"
else:
parallel.append(test.id())
continue
fixture_groups.setdefault(key, []).append(test.id())
size = max(1, math.ceil(len(tests) / (workers * 4)))
batches = list(fixture_groups.values())
batches.extend(list(batch) for batch in batched(parallel, size))
return sorted(batches, key=len, reverse=True)
def run_batch(test_ids, capture_output):
result = Result(capture_output)
outside = Capture(capture_output)
os.chdir(ROOT)
outside.start()
try:
unittest.TestLoader().loadTestsFromNames(test_ids).run(result)
finally:
stdout, stderr = outside.stop()
failures = [item for item in result.records if item["status"] in FAILURES]
if failures: # attach class/module fixture output to the first related failure
failures[0]["stdout"] = stdout + failures[0]["stdout"]
failures[0]["stderr"] = stderr + failures[0]["stderr"]
return result.records
def run_parallel(batches, workers, warning_action, capture_output):
with ProcessPoolExecutor(max_workers=workers, initializer=warnings.simplefilter, initargs=(warning_action,)) as pool:
futures = {pool.submit(run_batch, batch, capture_output): batch for batch in batches}
for future in as_completed(futures):
try:
yield future.result()
except Exception:
yield [make_record(futures[future][0], "error", traceback.format_exc())]
def report(records, errors, duration_count, elapsed):
width = min(100, os.get_terminal_size().columns if sys.stdout.isatty() else 80)
for index, error in enumerate(errors, 1):
print(paint(f"\n{'=' * 8} COLLECTION ERROR {index} {'=' * 8}", 31))
print(error.rstrip())
for item in sorted((r for r in records if r["status"] in FAILURES), key=lambda r: r["id"]):
heading = f" {item['status'].upper()} {item['id']} "
print(paint(f"\n{heading:=^{width}}", 31))
if item["detail"]:
print(item["detail"].rstrip())
for stream in ("stdout", "stderr"):
if item[stream]:
print(paint(f"\n--- captured {stream} ---", 33))
print(item[stream].rstrip())
timed = sorted((r for r in records if r["time"]), key=lambda r: r["time"], reverse=True)
if duration_count:
timed = timed[:duration_count]
if timed:
print(paint("\nslowest tests", 36))
for item in timed:
print(f"{item['time']:8.2f}s {item['id']}")
counts = Counter(item["status"] for item in records)
parts = [f"{counts[name]} {name}" for name in STATUS_MARKS if counts[name]]
if errors:
parts.append(f"{len(errors)} collection error{'s' if len(errors) != 1 else ''}")
failed = bool(errors) or any(counts[name] for name in FAILURES)
print(paint(f"\n{', '.join(parts) or 'no tests ran'} in {elapsed:.2f}s", 31 if failed else 32))
if failed:
return 1
if records:
return 0
return 5
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("targets", nargs="*", help="files, directories, dotted IDs, or path.py::Class::test")
parser.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 1, help="workers (default: available CPUs)")
parser.add_argument("-k", metavar="TEXT", help="only run test IDs containing TEXT")
parser.add_argument("-s", "--no-capture", action="store_true", help="show test output live")
parser.add_argument("-v", "--verbose", action="store_true", help="show every test")
parser.add_argument("--durations", type=int, default=10, metavar="N", help="show N slowest tests; 0 shows all")
parser.add_argument("-W", "--warnings", choices=("error", "default", "always", "ignore"), default="error")
args = parser.parse_args()
capture_output = not args.no_capture
os.chdir(ROOT)
warnings.simplefilter(args.warnings)
started = time.monotonic()
tests, errors = collect(args.targets, args.k)
batches = make_batches(tests, args.jobs)
workers = min(args.jobs, len(batches))
summary = f"collected {len(tests)} test{'s' if len(tests) != 1 else ''} in {time.monotonic() - started:.2f}s "
summary += f"{workers} worker{'s' if workers != 1 else ''}"
print(summary)
records = []
column = 0
try:
if workers < 2:
streams = (run_batch(batch, capture_output) for batch in batches)
else:
streams = run_parallel(batches, workers, args.warnings, capture_output)
for batch in streams:
records.extend(batch)
for item in batch:
mark, code = STATUS_MARKS[item["status"]]
if args.verbose:
print(f"{paint(mark, code)} {item['id']} {item['time']:.2f}s")
else:
print(paint(mark, code), end="", flush=True)
column += 1
if column == 80:
print()
column = 0
except KeyboardInterrupt:
print(paint("\ninterrupted", 31))
return 2
if column:
print()
return report(records, errors, args.durations, time.monotonic() - started)
if __name__ == "__main__":
raise SystemExit(main())