openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
5.0 KiB

#!/usr/bin/env python3
import json
import lzma
import hashlib
import requests
import struct
import subprocess
import os
from typing import Generator
from common.spinner import Spinner
class StreamingDecompressor:
def __init__(self, url: str) -> None:
self.buf = b""
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None})
self.it = self.req.iter_content(chunk_size=1024 * 1024)
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
self.eof = False
self.sha256 = hashlib.sha256()
def read(self, length: int) -> bytes:
while len(self.buf) < length:
self.req.raise_for_status()
try:
compressed = next(self.it)
except StopIteration:
self.eof = True
break
out = self.decompressor.decompress(compressed)
self.buf += out
result = self.buf[:length]
self.buf = self.buf[length:]
self.sha256.update(result)
return result
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
# https://source.android.com/devices/bootloader/images#sparse-format
magic = struct.unpack("I", f.read(4))[0]
assert(magic == 0xed26ff3a)
# Version
major = struct.unpack("H", f.read(2))[0]
minor = struct.unpack("H", f.read(2))[0]
assert(major == 1 and minor == 0)
f.read(2) # file header size
f.read(2) # chunk header size
block_sz = struct.unpack("I", f.read(4))[0]
f.read(4) # total blocks
num_chunks = struct.unpack("I", f.read(4))[0]
f.read(4) # crc checksum
for _ in range(num_chunks):
chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12))
if chunk_type == 0xcac1: # Raw
# TODO: yield in smaller chunks. Yielding only block_sz is too slow. Largest observed data chunk is 252 MB.
yield f.read(out_blocks * block_sz)
elif chunk_type == 0xcac2: # Fill
filler = f.read(4) * (block_sz // 4)
for _ in range(out_blocks):
yield filler
elif chunk_type == 0xcac3: # Don't care
yield b""
else:
raise Exception("Unhandled sparse chunk type")
def flash_partition(cloudlog, spinner, target_slot, partition):
cloudlog.info(f"Downloading and writing {partition['name']}")
downloader = StreamingDecompressor(partition['url'])
with open(f"/dev/disk/by-partlabel/{partition['name']}{target_slot}", 'wb+') as out:
partition_size = partition['size']
# Check if partition is already flashed
out.seek(partition_size)
if out.read(64) == partition['hash_raw'].lower().encode():
cloudlog.info(f"Already flashed {partition['name']}")
return
# Clear hash before flashing
out.seek(partition_size)
out.write(b"\x00" * 64)
out.seek(0)
os.sync()
# Flash partition
if partition['sparse']:
raw_hash = hashlib.sha256()
for chunk in unsparsify(downloader):
raw_hash.update(chunk)
out.write(chunk)
if spinner is not None:
spinner.update_progress(out.tell(), partition_size)
if raw_hash.hexdigest().lower() != partition['hash_raw'].lower():
raise Exception(f"Unsparse hash mismatch '{raw_hash.hexdigest().lower()}'")
else:
while not downloader.eof:
out.write(downloader.read(1024 * 1024))
if spinner is not None:
spinner.update_progress(out.tell(), partition_size)
if downloader.sha256.hexdigest().lower() != partition['hash'].lower():
raise Exception("Uncompressed hash mismatch")
if out.tell() != partition['size']:
raise Exception("Uncompressed size mismatch")
# Write hash after successfull flash
os.sync()
out.write(partition['hash_raw'].lower().encode())
def flash_agnos_update(manifest_path, cloudlog, spinner=None):
update = json.load(open(manifest_path))
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
target_slot = "_b" if current_slot == "_a" else "_a"
target_slot_number = "0" if target_slot == "_a" else "1"
cloudlog.info(f"Current slot {current_slot}, target slot {target_slot}")
# set target slot as unbootable
os.system(f"abctl --set_unbootable {target_slot_number}")
for partition in update:
success = False
for retries in range(10):
try:
flash_partition(cloudlog, spinner, target_slot, partition)
success = True
break
except requests.exceptions.RequestException:
cloudlog.exception("Failed")
spinner.update("Waiting for internet...")
cloudlog.info(f"Failed to download {partition['name']}, retrying ({retries})")
time.sleep(10)
if not success:
cloudlog.info(f"Failed to flash {partition['name']}, aborting")
raise Exception("Maximum retries exceeded")
cloudlog.info(f"AGNOS ready on slot {target_slot}")
if __name__ == "__main__":
import logging
import time
import sys
if len(sys.argv) != 2:
print("Usage: ./agnos.py <manifest.json>")
exit(1)
spinner = Spinner()
spinner.update("Updating AGNOS")
time.sleep(5)
logging.basicConfig(level=logging.INFO)
flash_agnos_update(sys.argv[1], logging, spinner)