Omnioculars V1

This commit is contained in:
firestar5683
2026-07-20 14:07:12 -05:00
parent 8b7c2b5f36
commit a51b1fd78f
16 changed files with 325 additions and 23 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ fi
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.8.27"
export AGNOS_VERSION="12.8.28"
fi
if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then
+1 -1
View File
@@ -26,7 +26,7 @@ DEFAULT_SPEED_VALUES = (15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75)
# Values the review and dataset tooling can accept. Keep DEFAULT_SPEED_VALUES
# aligned with the currently deployed classifier until an expanded model is
# promoted; adding a class changes every output index after it.
SUPPORTED_SPEED_VALUES = (10, *DEFAULT_SPEED_VALUES, 80, 90, 100)
SUPPORTED_SPEED_VALUES = (5, 10, *DEFAULT_SPEED_VALUES, 80, 90, 100)
EXTENDED_CLASSIFIER_SPEED_VALUES = tuple(sorted(SUPPORTED_SPEED_VALUES, key=str))
DETECTOR_EXPORT_NAME = "speed_limit_us_detector.onnx"
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from collections import Counter
from pathlib import Path
IMAGE_SUFFIXES = frozenset((".jpg", ".jpeg", ".png", ".bmp", ".webp"))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Evaluate a YOLO classifier dataset with the runtime confidence threshold.")
parser.add_argument("--model", type=Path, required=True)
parser.add_argument("--data", type=Path, required=True, help="Classifier dataset root containing train/ and val/.")
parser.add_argument("--split", choices=("train", "val"), default="val")
parser.add_argument("--imgsz", type=int, default=128)
parser.add_argument("--batch", type=int, default=64)
parser.add_argument("--device", default="cpu")
parser.add_argument("--min-confidence", type=float, default=0.60)
parser.add_argument("--output-json", type=Path)
return parser.parse_args()
def main() -> int:
args = parse_args()
split_root = args.data.expanduser().resolve() / args.split
if not split_root.is_dir():
raise FileNotFoundError(split_root)
samples: list[tuple[Path, str]] = []
for class_dir in sorted(split_root.iterdir()):
if not class_dir.is_dir() or class_dir.name.startswith("._"):
continue
for image_path in sorted(class_dir.iterdir()):
if image_path.name.startswith("._") or image_path.suffix.lower() not in IMAGE_SUFFIXES or not image_path.is_file():
continue
samples.append((image_path, class_dir.name))
if not samples:
raise RuntimeError(f"No classifier images found under {split_root}")
try:
from ultralytics import YOLO
except Exception as exc:
raise SystemExit("Ultralytics is required to evaluate classifier checkpoints.") from exc
model = YOLO(str(args.model.expanduser().resolve()))
predictions = model.predict(
source=[str(path) for path, _ in samples],
imgsz=args.imgsz,
batch=args.batch,
device=args.device,
verbose=False,
stream=True,
)
class_counts: dict[str, Counter[str]] = {}
total_counts: Counter[str] = Counter()
for (_, expected), result in zip(samples, predictions, strict=True):
probabilities = result.probs
if probabilities is None:
raise RuntimeError("Classifier prediction did not include probabilities")
predicted = str(result.names[int(probabilities.top1)])
confidence = float(probabilities.top1conf)
counts = class_counts.setdefault(expected, Counter())
counts["total"] += 1
total_counts["total"] += 1
if predicted == expected:
counts["top1_exact"] += 1
total_counts["top1_exact"] += 1
if confidence < args.min_confidence:
counts["rejected"] += 1
total_counts["rejected"] += 1
continue
counts["accepted"] += 1
total_counts["accepted"] += 1
if predicted == expected:
counts["accepted_exact"] += 1
total_counts["accepted_exact"] += 1
else:
counts["accepted_wrong"] += 1
total_counts["accepted_wrong"] += 1
def summarize(counts: Counter[str]) -> dict[str, float | int]:
total = counts["total"]
accepted = counts["accepted"]
return {
"total": total,
"top1_exact": counts["top1_exact"],
"top1_exact_rate": round(counts["top1_exact"] / total, 6) if total else 0.0,
"accepted": accepted,
"accepted_coverage": round(accepted / total, 6) if total else 0.0,
"accepted_exact": counts["accepted_exact"],
"accepted_wrong": counts["accepted_wrong"],
"accepted_precision": round(counts["accepted_exact"] / accepted, 6) if accepted else 0.0,
"rejected": counts["rejected"],
}
summary = {
"model": str(args.model.expanduser().resolve()),
"data": str(split_root),
"minimum_confidence": args.min_confidence,
"overall": summarize(total_counts),
"classes": {name: summarize(class_counts[name]) for name in sorted(class_counts)},
}
encoded = json.dumps(summary, indent=2, sort_keys=True) + "\n"
print(encoded, end="")
if args.output_json:
output_path = args.output_json.expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(encoded, encoding="ascii")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -59,6 +59,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--track-max-age", type=float, help="Override the maximum proposal track lifetime.")
parser.add_argument("--crop-ocr", action="store_true", help="Evaluate with crop OCR confirmation enabled.")
parser.add_argument("--classifier-min-confidence", type=float, help="Override the value classifier confidence threshold.")
parser.add_argument("--classifier-speed-values", help="Comma-separated classifier classes for a candidate model.")
parser.add_argument("--extended-classifier-min-confidence", type=float, help="Override confidence for 5/10/80/90/100.")
parser.add_argument("--trusted-model-min-confidence", type=float, help="Override tiny-box trusted model confidence.")
parser.add_argument("--classifier-expansion-limit", type=int, help="Evaluate only the first N detector crop expansions.")
parser.add_argument("--classifier-expansion-indices", help="Comma-separated detector crop expansion indices to evaluate.")
@@ -232,6 +234,10 @@ def main() -> int:
slv.DETECTOR_CLASSIFIER_CROP_OCR_ENABLED = args.crop_ocr
if args.classifier_min_confidence is not None:
slv.US_CLASSIFIER_MIN_CONFIDENCE = args.classifier_min_confidence
if args.classifier_speed_values:
slv.US_CLASSIFIER_SPEED_VALUES = tuple(int(value) for value in args.classifier_speed_values.split(","))
if args.extended_classifier_min_confidence is not None:
slv.EXTENDED_CLASSIFIER_MIN_CONFIDENCE = args.extended_classifier_min_confidence
if args.trusted_model_min_confidence is not None:
slv.DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_READ_CONFIDENCE = args.trusted_model_min_confidence
if args.classifier_expansion_indices:
@@ -34,7 +34,9 @@ def link_or_copy(source: Path, destination: Path) -> None:
try:
os.link(source, destination)
except OSError:
shutil.copy2(source, destination)
# copy2 preserves macOS metadata as AppleDouble `._` files on exFAT. Image
# loaders then mistake those sidecars for corrupt training images.
shutil.copyfile(source, destination)
def main() -> int:
@@ -130,7 +130,8 @@ HTML = r"""<!doctype html>
</aside>
</main>
<script>
const speeds = [10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,90,100];
const speeds = [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,90,100];
const shortcutSpeeds = speeds.filter((speed) => speed !== 5 && speed !== 100);
let rows = [];
let index = 0;
let current = null;
@@ -339,7 +340,7 @@ function handleDigitShortcut(digit) {
}
const speed = Number(speedBuffer);
if (speeds.includes(speed)) {
if (shortcutSpeeds.includes(speed)) {
clearSpeedBuffer();
setSpeed(speed, true);
return;
@@ -34,11 +34,11 @@ def test_raw_comma_camera_uses_real_frame_rate():
def test_extended_classifier_order_matches_lexical_dataset_classes():
assert common.SUPPORTED_SPEED_VALUES == (10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 90, 100)
assert common.EXTENDED_CLASSIFIER_SPEED_VALUES == (10, 100, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 90)
assert common.SUPPORTED_SPEED_VALUES == (5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 90, 100)
assert common.EXTENDED_CLASSIFIER_SPEED_VALUES == (10, 100, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70, 75, 80, 90)
@pytest.mark.parametrize("speed", (10, 80, 90, 100))
@pytest.mark.parametrize("speed", (5, 10, 80, 90, 100))
def test_manual_import_accepts_extended_speed_values(speed):
assert import_queue.parse_speed(str(speed)) == speed
+8 -2
View File
@@ -169,9 +169,11 @@ US_DETECTOR_CLASSES = {
2: "school_zone_speed_limit",
}
US_CLASSIFIER_SPEED_VALUES = (15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75)
EXTENDED_CLASSIFIER_SPEED_VALUES = frozenset((5, 10, 80, 90, 100))
SCHOOL_ZONE_SPEED_VALUES = frozenset((15, 20, 25))
US_DETECTOR_MIN_CONFIDENCE = 0.06
US_CLASSIFIER_MIN_CONFIDENCE = 0.60
EXTENDED_CLASSIFIER_MIN_CONFIDENCE = 0.90
US_CLASSIFIER_REJECT_MIN_CONFIDENCE = 0.85
SEPARATE_REJECT_CLASSIFIER_ENABLED = False
US_REJECT_CLASSIFIER_MIN_CONFIDENCE = 0.85
@@ -1614,12 +1616,16 @@ class SpeedLimitVisionDaemon:
speed_probabilities = probabilities[:speed_class_count]
class_index = int(np.argmax(speed_probabilities))
confidence = float(speed_probabilities[class_index])
speed_limit = US_CLASSIFIER_SPEED_VALUES[class_index]
if has_reject_class and float(probabilities[speed_class_count]) >= max(confidence, US_CLASSIFIER_REJECT_MIN_CONFIDENCE):
return None
if confidence < US_CLASSIFIER_MIN_CONFIDENCE:
minimum_confidence = (
EXTENDED_CLASSIFIER_MIN_CONFIDENCE if speed_limit in EXTENDED_CLASSIFIER_SPEED_VALUES else US_CLASSIFIER_MIN_CONFIDENCE
)
if confidence < minimum_confidence:
return None
return US_CLASSIFIER_SPEED_VALUES[class_index], confidence
return speed_limit, confidence
def _detect_sign_from_detector_classifier(self, frame_bgr):
frame_height, frame_width = frame_bgr.shape[:2]
@@ -15,6 +15,17 @@ class MemoryParams:
self.values[key] = value
class StaticClassifierNet:
def __init__(self, probabilities):
self.probabilities = np.array(probabilities, dtype=np.float32)
def setInput(self, _blob):
pass
def forward(self):
return self.probabilities
def daemon_with_history(current_speed, entries):
daemon = SpeedLimitVisionDaemon.__new__(SpeedLimitVisionDaemon)
daemon.published_speed_limit_mph = current_speed
@@ -65,6 +76,32 @@ def test_published_sign_value_uses_configured_units():
assert metric_daemon.published_status == "Vision 50 km/h (95%)"
@pytest.mark.parametrize(("confidence", "expected"), ((0.89, None), (0.91, (80, 0.91))))
def test_extended_classifier_values_require_high_confidence(monkeypatch, confidence, expected):
speed_values = (10, 100, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 90)
probabilities = np.zeros(len(speed_values) + 1, dtype=np.float32)
probabilities[speed_values.index(80)] = confidence
probabilities[-1] = 1.0 - confidence
method_globals = slv.SpeedLimitVisionDaemon._classify_speed_limit_from_model.__globals__
monkeypatch.setitem(method_globals, "US_CLASSIFIER_SPEED_VALUES", speed_values)
monkeypatch.setitem(method_globals, "EXTENDED_CLASSIFIER_SPEED_VALUES", frozenset((5, 10, 80, 90, 100)))
monkeypatch.setitem(method_globals, "EXTENDED_CLASSIFIER_MIN_CONFIDENCE", 0.90)
daemon = slv.SpeedLimitVisionDaemon.__new__(slv.SpeedLimitVisionDaemon)
daemon.classifier_net = StaticClassifierNet(probabilities)
daemon.reject_classifier_net = None
daemon.classifier_input_size = 128
daemon.last_classifier_forward_count = 0
daemon.last_classifier_forward_duration_s = 0.0
result = daemon._classify_speed_limit_from_model(np.ones((64, 48, 3), dtype=np.uint8))
if expected is None:
assert result is None
else:
assert result == pytest.approx(expected)
def test_speed_change_requires_two_matching_reads_below_single_read_threshold():
daemon = daemon_with_history(40, [(55, 0.82)])
assert daemon._confirm_detection() is None
+3 -3
View File
@@ -67,9 +67,9 @@
},
{
"name": "system",
"url": "https://www.dropbox.com/scl/fi/kf8be07fgdjgcv9ic482y/system7.img.xz?rlkey=0e6jwij46tndpaeu33jx65tma&st=g9g0ph65&dl=1",
"hash": "1b71fd1835610e46c9d3f2d13e389108df4777308a6128e5b794b6875c91012e",
"hash_raw": "1b71fd1835610e46c9d3f2d13e389108df4777308a6128e5b794b6875c91012e",
"url": "https://www.dropbox.com/scl/fi/8fd3w7hbhsyq146kqqdab/system8.img.xz?rlkey=5zt15mtuehdlj8ncj6pwn7zjy&st=j6tr4d47&dl=1",
"hash": "4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793",
"hash_raw": "4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793",
"size": 5368709120,
"sparse": false,
"full_check": false,
+24 -1
View File
@@ -1,6 +1,13 @@
import hashlib
from pathlib import Path
import tempfile
import unittest
from unittest import mock
import numpy as np
from tinygrad.helpers import polyN, is_numpy_ndarray
import zstandard
import tinygrad.helpers
from tinygrad.helpers import _decompress_zstd, fetch_fw, polyN, is_numpy_ndarray
from tinygrad.tensor import Tensor
class TestPolyN(unittest.TestCase):
@@ -11,5 +18,21 @@ class TestIsNumpyNdarray(unittest.TestCase):
def test_tensor_numpy(self):
self.assertTrue(is_numpy_ndarray(Tensor([1, 2, 3]).numpy()))
class TestZstd(unittest.TestCase):
def test_decompress(self):
payload = b"local firmware payload"
compressed = zstandard.ZstdCompressor().compress(payload)
self.assertEqual(_decompress_zstd(compressed), payload)
def test_fetch_fw_uses_local_zstd(self):
payload = b"local firmware payload"
with tempfile.TemporaryDirectory() as tmp:
firmware = Path(tmp) / "firmware.bin.zst"
firmware.write_bytes(zstandard.ZstdCompressor().compress(payload))
with mock.patch.object(tinygrad.helpers.pathlib, "Path", return_value=firmware), \
mock.patch.object(tinygrad.helpers, "fetch") as remote_fetch:
self.assertEqual(fetch_fw("amdgpu", "firmware.bin", hashlib.sha256(payload).hexdigest()), payload)
remote_fetch.assert_not_called()
if __name__ == '__main__':
unittest.main()
+9 -3
View File
@@ -480,10 +480,16 @@ def fetch(url:str, name:pathlib.Path|str|None=None, subdir:str|None=None, gunzip
if length and (file_size:=os.stat(fp).st_size) < length: raise RuntimeError(f"fetch size incomplete, {file_size} < {length}")
return fp
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
if sys.version_info >= (3,14) and (p:=pathlib.Path(f"/lib/firmware/{path}/{name}.zst")).is_file():
def _decompress_zstd(data:bytes) -> bytes:
if sys.version_info >= (3,14):
from compression.zstd import decompress
if hashlib.sha256(b:=decompress(p.read_bytes())).hexdigest() == sha256: return b
return decompress(data)
from zstandard import ZstdDecompressor
return ZstdDecompressor().decompress(data)
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
if (p:=pathlib.Path(f"/lib/firmware/{path}/{name}.zst")).is_file():
if hashlib.sha256(b:=_decompress_zstd(p.read_bytes())).hexdigest() == sha256: return b
return fetch(f"https://gitlab.com/kernel-firmware/linux-firmware/-/raw/1e2c15348485939baf1b6d1f5a7a3b799d80703d/{path}/{name}",
subdir="fw", sha256=sha256).read_bytes()
+3 -3
View File
@@ -2,7 +2,7 @@
set -euo pipefail
HOST="${1:-comma@192.168.3.110}"
IMAGE="${2:-/Users/dominickthompson/Desktop/system7.img.xz}"
IMAGE="${2:-/Users/dominickthompson/Desktop/system8.img.xz}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
@@ -16,8 +16,8 @@ REMOTE_RUNNER="${REMOTE_DIR}/run_flash.sh"
REMOTE_AGNOS="${REMOTE_DIR}/agnos.py"
PORT="8989"
EXPECTED_VERSION="12.8.27"
RAW_HASH="1b71fd1835610e46c9d3f2d13e389108df4777308a6128e5b794b6875c91012e"
EXPECTED_VERSION="12.8.28"
RAW_HASH="4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793"
RAW_SIZE="5368709120"
if [[ ! -f "$IMAGE" ]]; then
+71 -3
View File
@@ -32,6 +32,7 @@ MICI_SETUP_ENTRY_IN_SETUP_ZIPAPP = "openpilot/system/ui/mici_setup.py"
UPDATER_ENTRY_IN_ZIPAPP = "openpilot/system/ui/updater.py"
VERSION_PATH_IN_IMAGE = "/VERSION"
PYTHON_SITE_PACKAGES_PATH_IN_IMAGE = "/usr/local/venv/lib/python3.12/site-packages"
AMDGPU_FIRMWARE_PATH_IN_IMAGE = "/lib/firmware/amdgpu"
PATCH_MARKER = "STARPILOT_C4_RESET_LAYOUT_V1"
APP_PATCH_MARKER = "STARPILOT_C4_RESET_APP_DIMENSIONS_V1"
SETUP_WIFI_PATCH_MARKER = "JEEPNY_AVAILABLE = True"
@@ -51,6 +52,17 @@ CHUNK_TYPE_DONT_CARE = 0xCAC3
CHUNK_TYPE_CRC32 = 0xCAC4
XZ_MAGIC = b"\xFD7zXZ\x00"
AMDGPU_FIRMWARE_SHA256 = {
"gc_12_0_0_imu.bin.zst": "aa15e5b3156bffc45e0c50bccbcd364fbd3f958531b695b7487a803d780b8328",
"gc_12_0_0_me.bin.zst": "d7eba5197f2580f32b8256b1d9cb68e723e9e644293a34446a7913e3c093cba5",
"gc_12_0_0_mec.bin.zst": "1931593440b8f9423580d9e2cdc5b34e7c682cdffe1ca4b74b0c2f6a0420236d",
"gc_12_0_0_pfp.bin.zst": "16bfd64c10fe73b5e760055069a60e5841dba16c0ed4edb56c20d675e23901f6",
"gc_12_0_0_rlc.bin.zst": "6436b582734a413456fff3d3c7195e71cc9e78a7ed31ee21c83ffd6fae1ad186",
"psp_14_0_2_sos.bin.zst": "7b538448b57d4f9dd06b2eea90d4f86a16e65e3027cdecee8db71c2c5f1fa243",
"sdma_7_0_0.bin.zst": "beaafb53993a106edd392392d5896245ae2a957c6d0f495d0002eec72ad8ad38",
"smu_14_0_2.bin.zst": "6951995d1d606f4dc60c895f19d34ed18aa40e62129f83d8510c45e8aa9ae2fc",
}
DEFAULT_SYNC_COMMA_FILES = [
"/usr/comma/bg.jpg",
"/usr/comma/comma.sh",
@@ -366,7 +378,7 @@ def patch_reset_script() -> bytes:
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Patch AGNOS system image with minimal C4 reset/setup/updater fixes")
p = argparse.ArgumentParser(description="Patch AGNOS system image with StarPilot reset and hardware support")
p.add_argument("--manifest", default="system/hardware/tici/agnos.json", help="Path to AGNOS manifest JSON")
p.add_argument("--work-dir", default=".cache/agnos_reset_patch", help="Working directory")
p.add_argument("--source-url", default=None, help="Override source raw system image URL")
@@ -378,6 +390,8 @@ def parse_args() -> argparse.Namespace:
help="Comma-separated file list to sync from reference image (e.g. /usr/comma/installer,/usr/comma/setup)")
p.add_argument("--disable-comma-file-sync", action="store_true",
help="Disable syncing /usr/comma files from a reference image")
p.add_argument("--disable-usbgpu-firmware", action="store_true",
help="Do not install the AMD firmware required by the external GPU")
p.add_argument("--output-xz", default=None, help="Output .img.xz path")
p.add_argument("--new-url", default=None, help="Hosted URL for patched image; used for manifest output")
p.add_argument("--manifest-out", default=None, help="Write updated manifest JSON here")
@@ -435,12 +449,13 @@ def find_default_reference_manifest(primary_manifest_path: Path) -> Path | None:
repo_root = repo_root.parent
candidates = [
repo_root.parent / "openpilot/openpilot/system/hardware/tici/agnos.json",
repo_root.parent / "openpilot/system/hardware/tici/agnos.json",
repo_root / "openpilot/system/hardware/tici/agnos.json",
]
for candidate in candidates:
if candidate.is_file():
if candidate.is_file() and candidate.resolve() != primary_manifest_path.resolve():
return candidate.resolve()
return None
@@ -1043,6 +1058,51 @@ def sha256_file(path: Path) -> str:
return h.hexdigest()
def sha256_zstd_payload(path: Path) -> str:
try:
import zstandard
except ImportError as e:
raise RuntimeError("zstandard is required to verify the external-GPU firmware") from e
digest = hashlib.sha256()
with open(path, "rb") as compressed:
with zstandard.ZstdDecompressor().stream_reader(compressed) as source:
while chunk := source.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def install_amdgpu_firmware_from_reference(debugfs: str, reference_img: Path, patched_img: Path, work_dir: Path) -> None:
ensure_directory_in_image(debugfs, patched_img, AMDGPU_FIRMWARE_PATH_IN_IMAGE, "040755", 0, 0)
firmware_dir = work_dir / "amdgpu_firmware"
firmware_dir.mkdir(parents=True, exist_ok=True)
for filename, expected_payload_hash in AMDGPU_FIRMWARE_SHA256.items():
image_path = f"{AMDGPU_FIRMWARE_PATH_IN_IMAGE}/{filename}"
local_file = firmware_dir / filename
verify_file = firmware_dir / f"{filename}.verify"
local_file.unlink(missing_ok=True)
verify_file.unlink(missing_ok=True)
stat_out = run_debugfs(debugfs, reference_img, f"stat {image_path}", write=False)
file_type, perms_octal, uid, gid = parse_debugfs_stat(stat_out)
if file_type != "regular":
raise RuntimeError(f"Reference firmware {image_path} is {file_type}, expected regular")
run_debugfs(debugfs, reference_img, f"dump -p {image_path} {local_file}", write=False)
if sha256_zstd_payload(local_file) != expected_payload_hash:
raise RuntimeError(f"Reference firmware payload hash mismatch for {filename}")
mode_octal = inode_mode_from_type_and_perms(file_type, perms_octal)
write_regular_file_to_image(debugfs, patched_img, image_path, local_file, mode_octal, uid, gid)
run_debugfs(debugfs, patched_img, f"dump -p {image_path} {verify_file}", write=False)
if sha256_file(local_file) != sha256_file(verify_file):
raise RuntimeError(f"Compressed firmware verification failed for {filename}")
if sha256_zstd_payload(verify_file) != expected_payload_hash:
raise RuntimeError(f"Installed firmware payload hash mismatch for {filename}")
print(f"Installed and verified {len(AMDGPU_FIRMWARE_SHA256)} AMD firmware files", flush=True)
def compress_xz(src: Path, dst: Path) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)
tmp = dst.with_suffix(dst.suffix + ".part")
@@ -1149,15 +1209,23 @@ def main() -> int:
shutil.copy2(raw_img, patched_img)
sync_paths = [] if args.disable_comma_file_sync else parse_sync_file_list(args.sync_comma_files)
if sync_paths:
reference_raw = None
if sync_paths or not args.disable_usbgpu_firmware:
reference_source_img = resolve_reference_source_image(args, manifest_path, work_dir)
reference_raw = work_dir / "reference_system.ext4.img"
materialize_ext4_image(reference_source_img, reference_raw, work_dir, "reference_system", force=args.force_download)
if sync_paths:
assert reference_raw is not None
print(f"Syncing /usr/comma payload files from reference image: {reference_raw}", flush=True)
synced_files = sync_files_from_reference_image(debugfs, reference_raw, patched_img, sync_paths, work_dir)
print(f"Synced {len(synced_files)} /usr/comma files from reference image", flush=True)
if not args.disable_usbgpu_firmware:
assert reference_raw is not None
print(f"Installing external-GPU firmware from reference image: {reference_raw}", flush=True)
install_amdgpu_firmware_from_reference(debugfs, reference_raw, patched_img, work_dir)
preserved_paths = {
RESET_PATH_IN_IMAGE: "comma_reset",
SETUP_PATH_IN_IMAGE: "comma_setup",
@@ -1,10 +1,16 @@
from pathlib import Path
import runpy
import pytest
from tools.agnos.patch_system_reset_image import (
AMDGPU_FIRMWARE_SHA256,
COMMA_SH_DISPLAY_WAIT_PATCH_MARKER,
comma_sh_has_expected_display_wait,
find_default_reference_manifest,
format_debugfs_mode,
patch_comma_sh_display_wait,
sha256_zstd_payload,
)
@@ -52,3 +58,32 @@ def test_patch_comma_sh_display_wait_rejects_unknown_layout():
])
def test_format_debugfs_mode(mode, expected):
assert format_debugfs_mode(mode) == expected
def test_external_gpu_firmware_matches_tinygrad_requirements():
firmware_metadata = Path(__file__).resolve().parents[2] / "tinygrad/runtime/autogen/am/fw.py"
hashes = runpy.run_path(firmware_metadata)["hashes"]
expected = {filename.removesuffix(".zst"): digest for filename, digest in AMDGPU_FIRMWARE_SHA256.items()}
assert all(hashes[filename] == digest for filename, digest in expected.items())
def test_zstd_payload_hash(tmp_path):
import hashlib
import zstandard
payload = b"external GPU firmware payload"
compressed = tmp_path / "firmware.bin.zst"
compressed.write_bytes(zstandard.ZstdCompressor().compress(payload))
assert sha256_zstd_payload(compressed) == hashlib.sha256(payload).hexdigest()
def test_default_reference_manifest_uses_sibling_openpilot(tmp_path):
primary = tmp_path / "starpilot/system/hardware/tici/agnos.json"
reference = tmp_path / "openpilot/openpilot/system/hardware/tici/agnos.json"
primary.parent.mkdir(parents=True)
reference.parent.mkdir(parents=True)
primary.write_text("[]")
reference.write_text("[]")
assert find_default_reference_manifest(primary) == reference.resolve()