-
Notifications
You must be signed in to change notification settings - Fork 1.9k
body v1 updater #3289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
body v1 updater #3289
Changes from 20 commits
5a38900
13b280a
c6df953
bc20c8a
f200718
45cee7b
a08cea6
5c1162f
eb80c88
4235675
9670bdc
1534420
5b6f70e
10f1551
ceb84dc
25cd6cf
a004871
2c0b932
dcf614c
d42142d
ac8d600
8955dd8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import os | ||
| import time | ||
| import struct | ||
| import subprocess | ||
| from itertools import accumulate | ||
|
|
||
| from opendbc.car.uds import CanClient, IsoTpMessage, MessageTimeoutError | ||
|
|
||
| REQUEST_IN = 0xC0 | ||
| REQUEST_OUT = 0x40 | ||
| DEFAULT_ISOTP_TIMEOUT = 2 | ||
|
|
||
| F4Config = { | ||
| "sector_sizes": [0x4000 for _ in range(4)] + [0x10000] + [0x20000 for _ in range(11)], | ||
| } | ||
|
|
||
| class CanHandle: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should live in panda |
||
| def __init__(self, can_send, can_recv, bus): | ||
| self.client = CanClient(can_send, can_recv, tx_addr=1, rx_addr=2, bus=bus) | ||
|
|
||
| def transact(self, dat, timeout=DEFAULT_ISOTP_TIMEOUT, expect_disconnect=False): | ||
| try: | ||
| msg = IsoTpMessage(self.client, timeout=timeout) | ||
| msg.send(dat) | ||
| if expect_disconnect: | ||
| deadline = time.monotonic() + timeout | ||
| while not msg.tx_done: | ||
| msg.recv(timeout=0) | ||
| if not msg.tx_done and time.monotonic() > deadline: | ||
| raise MessageTimeoutError("timeout waiting for flow control") | ||
| time.sleep(0.01) | ||
| return b"" | ||
| ret, _ = msg.recv() | ||
| return ret | ||
| except MessageTimeoutError as e: | ||
| raise TimeoutError from e | ||
|
|
||
| def controlWrite(self, request_type, request, value, index, data, timeout=DEFAULT_ISOTP_TIMEOUT, expect_disconnect=False): | ||
| dat = struct.pack("HHBBHHH", 0, 0, request_type, request, value, index, 0) | ||
| return self.transact(dat, timeout=timeout, expect_disconnect=expect_disconnect) | ||
|
|
||
| def controlRead(self, request_type, request, value, index, length, timeout=DEFAULT_ISOTP_TIMEOUT): | ||
| dat = struct.pack("HHBBHHH", 0, 0, request_type, request, value, index, length) | ||
| return self.transact(dat, timeout=timeout) | ||
|
|
||
| def bulkWrite(self, endpoint, data, timeout=DEFAULT_ISOTP_TIMEOUT): | ||
| dat = struct.pack("HH", endpoint, len(data)) + data | ||
| return self.transact(dat, timeout=timeout) | ||
|
|
||
| def bulkRead(self, endpoint, timeout=DEFAULT_ISOTP_TIMEOUT): | ||
| dat = struct.pack("HH", endpoint, 0) | ||
| return self.transact(dat, timeout=timeout) | ||
|
|
||
|
|
||
| def flush_recv_buffer(can_recv): | ||
| while (1): | ||
| if len(can_recv()) == 0: | ||
| break | ||
|
|
||
|
|
||
| def fetch_bin(bin_path, update_url): | ||
| os.makedirs(os.path.dirname(bin_path), exist_ok=True) | ||
| result = subprocess.run( | ||
| ["curl", "-L", "-o", bin_path, "-w", "%{http_code}", "--silent", update_url], | ||
| capture_output=True, text=True | ||
| ) | ||
| status = result.stdout.strip() | ||
| if status == "200": | ||
| print("downloaded latest body firmware binary") | ||
| else: | ||
| raise RuntimeError(f"download failed with HTTP {status}") | ||
|
Comment on lines
+61
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. openpilot will pass this in from the panda build obj dir |
||
|
|
||
|
|
||
| def flash_can(handle, code, mcu_config): | ||
| assert mcu_config is not None, "must set valid mcu_type to flash" | ||
|
|
||
| # confirm flasher is present | ||
| fr = handle.controlRead(REQUEST_IN, 0xb0, 0, 0, 0xc) | ||
| assert fr[4:8] == b"\xde\xad\xd0\x0d" | ||
|
|
||
| apps_sectors_cumsum = accumulate(mcu_config["sector_sizes"][1:]) | ||
| last_sector = next((i + 1 for i, v in enumerate(apps_sectors_cumsum) if v > len(code)), -1) | ||
| assert last_sector >= 1, "Binary too small? No sector to erase." | ||
| assert last_sector < 7, "Binary too large! Risk of overwriting provisioning chunk." | ||
|
|
||
| print("flash: unlocking") | ||
| handle.controlWrite(REQUEST_IN, 0xb1, 0, 0, b'') | ||
|
|
||
| print(f"flash: erasing sectors 1 - {last_sector}") | ||
| for i in range(1, last_sector + 1): | ||
| handle.controlWrite(REQUEST_IN, 0xb2, i, 0, b'') | ||
|
|
||
| STEP = 0x10 | ||
| print("flash: flashing") | ||
| for i in range(0, len(code), STEP): | ||
| handle.bulkWrite(2, code[i:i + STEP]) | ||
|
|
||
|
|
||
| def reset_body(handle): | ||
| print("flash: resetting") | ||
| handle.controlWrite(REQUEST_IN, 0xd8, 0, 0, b'', expect_disconnect=True) | ||
|
|
||
|
|
||
| def update(can_send, can_recv, addr, bus, file, update_url, current_signature=None): | ||
| if not os.path.exists(file): | ||
| print("local bin is not up-to-date, fetching latest") | ||
| fetch_bin(file, update_url) | ||
|
|
||
| if current_signature is not None: | ||
| print("checking body firmware signature") | ||
| with open(file, "rb") as f: | ||
| expected_signature = f.read()[-128:] | ||
|
|
||
| print(f"expected body signature: {expected_signature.hex()}") | ||
| print(f"current body signature: {current_signature.hex()}") | ||
|
|
||
| if current_signature is None or current_signature != expected_signature: | ||
| print("flashing motherboard") | ||
| can_send(addr, b"\xce\xfa\xad\xde\x1e\x0b\xb0\x0a", bus) | ||
| time.sleep(0.1) | ||
|
|
||
| print("flashing", file) | ||
| flush_recv_buffer(can_recv) | ||
| with open(file, "rb") as f: | ||
| code = f.read() | ||
| handle = CanHandle(can_send, can_recv, bus) | ||
| retries = 3 | ||
| for i in range(retries): | ||
| try: | ||
| flash_can(handle, code, F4Config) | ||
| reset_body(handle) | ||
| except (TimeoutError, RuntimeError) as e: | ||
| print(f"flash failed (attempt {i + 1}/{retries}): {e}, trying again...") | ||
| else: | ||
| print("successfully flashed") | ||
|
|
||
| # Clear cached CarParams so FW queries run fresh after flash | ||
| car_params_cache = "/data/params/d/CarParamsCache" | ||
| if os.path.isfile(car_params_cache): | ||
| os.remove(car_params_cache) | ||
| print("cleared CarParamsCache") | ||
| return | ||
|
|
||
| # on fail: attempt to exit bootloader | ||
| reset_body(handle) | ||
| raise RuntimeError(f"flash failed after {retries} attempts") | ||
| else: | ||
| print("body firmware is up to date") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import os | ||
| from opendbc.car import Bus, CarSpecs, PlatformConfig, Platforms | ||
| from opendbc.car.structs import CarParams | ||
| from opendbc.car.docs_definitions import CarDocs | ||
|
|
@@ -7,6 +8,15 @@ | |
|
|
||
| SPEED_FROM_RPM = 0.008587 | ||
|
|
||
| FIRMWARE_VERSION = "v0.3.1" | ||
| BIN_URL = f"https://github.com/commaai/body/releases/download/{FIRMWARE_VERSION}/body.bin.signed" | ||
| BIN_NAME = f"body-v1-{FIRMWARE_VERSION}.bin.signed" | ||
| BODY_DIR = os.path.dirname(os.path.realpath(__file__)) | ||
| _PANDA_BIN_DIR = os.path.join(BODY_DIR, "../../../../panda/board/body/v1") | ||
|
Comment on lines
+11
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's just move the v1 stuff into panda |
||
| BIN_PATH = os.path.join(_PANDA_BIN_DIR, BIN_NAME) if os.path.isdir(_PANDA_BIN_DIR) else os.path.join(BODY_DIR, BIN_NAME) | ||
| FLASH_ADDR = 0x250 | ||
| BUS = 0 | ||
|
|
||
|
|
||
| class CarControllerParams: | ||
| ANGLE_DELTA_BP = [0., 5., 15.] | ||
|
|
@@ -34,6 +44,11 @@ class CAR(Platforms): | |
| [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE], | ||
| bus=0, | ||
| ), | ||
| Request( | ||
| [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.APPLICATION_SOFTWARE_FINGERPRINT_REQUEST], | ||
| [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.APPLICATION_SOFTWARE_FINGERPRINT_RESPONSE], | ||
| bus=0, | ||
| ), | ||
|
Comment on lines
+47
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this and just pass the hash with the existing request |
||
| ], | ||
| ) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's split this out into its own PR