diff --git a/.gitignore b/.gitignore index 71a9908..a498208 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ venv/ .hypothesis/ *.log *.bin +# Profile board-variant SPL blobs are tracked; the global *.bin rule +# above otherwise hides them. +!src/defib/profiles/data/*.bin diff --git a/src/defib/cli/app.py b/src/defib/cli/app.py index b6044ce..bf50c05 100644 --- a/src/defib/cli/app.py +++ b/src/defib/cli/app.py @@ -1046,11 +1046,19 @@ async def _agent_upload_async(chip: str, port: str, output: str) -> None: # accordingly. Pre-truncating to profile.spl_max_size hides the real # boundary on chips where the OpenIPC SPL is larger than the HiTool # reference (e.g. hi3516av200's SVB-enabled SPL is 0x6800, not 0x4F00). - spl_data = cached_fw.read_bytes() + # If the profile (or board variant) ships a pre-built SPL blob, use + # that instead — needed on boards where the OpenIPC SPL doesn't bring + # DDR up (e.g. eMMC-equipped hi3516av300 cameras). + if profile.spl_data is not None: + spl_data = profile.spl_data + spl_source = f"profile SPL_BLOB ({len(spl_data)} bytes)" + else: + spl_data = cached_fw.read_bytes() + spl_source = f"full U-Boot ({len(spl_data)} bytes — boundary auto-detected)" if output == "human": console.print(f"Agent: [cyan]{agent_path.name}[/cyan] ({len(agent_data)} bytes)") - console.print(f"SPL: full U-Boot ({len(spl_data)} bytes — boundary auto-detected)") + console.print(f"SPL: {spl_source}") console.print("\n[yellow]Power-cycle the camera now![/yellow]\n") transport = await create_transport(normalize_port_name(port)) @@ -1196,7 +1204,13 @@ async def _agent_flash_async( # Pass full u-boot; _send_spl detects the actual SPL boundary so chips # where OpenIPC SPL is larger than HiTool's profile_max (e.g. av200's # SVB-enabled SPL = 0x6800 vs profile_max = 0x4F00) get the right size. - spl_data = cached_fw.read_bytes() + # If the profile (or board variant) ships a pre-built SPL blob, use + # that instead — needed on boards where the OpenIPC SPL doesn't bring + # DDR up (e.g. eMMC-equipped hi3516av300 cameras). + if profile.spl_data is not None: + spl_data = profile.spl_data + else: + spl_data = cached_fw.read_bytes() if output == "human": console.print(f"Firmware: [cyan]{fw_path.name}[/cyan] ({len(firmware)} bytes, CRC {fw_crc:#010x})") diff --git a/src/defib/profiles/data/hi3516av300-emmc-spl.bin b/src/defib/profiles/data/hi3516av300-emmc-spl.bin new file mode 100644 index 0000000..4b4db77 Binary files /dev/null and b/src/defib/profiles/data/hi3516av300-emmc-spl.bin differ diff --git a/src/defib/profiles/data/hi3516av300.json b/src/defib/profiles/data/hi3516av300.json index e64c197..11662dc 100644 --- a/src/defib/profiles/data/hi3516av300.json +++ b/src/defib/profiles/data/hi3516av300.json @@ -1 +1,13 @@ -{"name": "hi3516av300", "PRESTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 2, 18, 78, 87, 79, 68, 64, 1, 2, 18, 117, 106, 105, 122], "DDRSTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 2, 18, 120, 86, 52, 18, 64, 1, 2, 18, 117, 106, 105, 122], "ADDRESS": ["0x04017000", "0x04010500", "0x81000000"], "FILELEN": ["0x0040", "0x6000"], "STEPLEN": ["0x0040", "0x0070"]} +{ + "name": "hi3516av300", + "PRESTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 2, 18, 78, 87, 79, 68, 64, 1, 2, 18, 117, 106, 105, 122], + "DDRSTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 2, 18, 120, 86, 52, 18, 64, 1, 2, 18, 117, 106, 105, 122], + "ADDRESS": ["0x04017000", "0x04010500", "0x81000000"], + "FILELEN": ["0x0040", "0x6000"], + "STEPLEN": ["0x0040", "0x0070"], + "variants": { + "emmc": { + "SPL_BLOB": "hi3516av300-emmc-spl.bin" + } + } +} diff --git a/src/defib/profiles/loader.py b/src/defib/profiles/loader.py index a7db8ac..eb90ba1 100644 --- a/src/defib/profiles/loader.py +++ b/src/defib/profiles/loader.py @@ -90,7 +90,19 @@ def load_profile(chip_name: str, profiles_dir: Path | None = None) -> SoCProfile ) # Variant entries override matching top-level keys data.update(variants[variant]) - return SoCProfile.model_validate(data) + profile = SoCProfile.model_validate(data) + + # Resolve SPL_BLOB if declared. Path is relative to the profile JSON's + # directory. Done here (not in pydantic) so the schema stays I/O-free. + if profile.spl_blob: + blob_path = profile_path.parent / profile.spl_blob + if not blob_path.exists(): + raise FileNotFoundError( + f"SPL_BLOB '{profile.spl_blob}' for chip '{current}' not " + f"found at {blob_path}" + ) + profile._spl_data = blob_path.read_bytes() + return profile raise ValueError(f"Alias chain too deep for chip: {chip_name}") diff --git a/src/defib/profiles/schema.py b/src/defib/profiles/schema.py index 9f5558f..8fff775 100644 --- a/src/defib/profiles/schema.py +++ b/src/defib/profiles/schema.py @@ -2,7 +2,7 @@ from __future__ import annotations -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, PrivateAttr class SoCProfile(BaseModel): @@ -44,6 +44,26 @@ class SoCProfile(BaseModel): "the chip's actual SRAM ceiling." ), ) + spl_blob: str | None = Field( + default=None, alias="SPL_BLOB", + description=( + "Optional filename (resolved relative to the profile JSON's " + "directory) of a pre-built SPL binary to upload as the SPL stage " + "instead of slicing it from the downloaded U-Boot. Used by board " + "variants where the OpenIPC SPL doesn't bring DDR up correctly — " + "e.g. eMMC-equipped hi3516av300 boards. The loader reads the file " + "and stores its bytes on `_spl_data`; callers access them via the " + "`spl_data` property." + ), + ) + # Bytes of the SPL_BLOB, populated by the loader (not in JSON, not + # validated by pydantic). None if `spl_blob` is unset. + _spl_data: bytes | None = PrivateAttr(default=None) + + @property + def spl_data(self) -> bytes | None: + """Pre-built SPL bytes if the profile declares an `SPL_BLOB`.""" + return self._spl_data @property def ddr_step_address(self) -> int: diff --git a/tests/test_cli.py b/tests/test_cli.py index 72aa90e..2f01dd5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -194,19 +194,27 @@ class TestAgentNotRespondingDiagnostic: def test_mentions_ddr_init_root_cause(self): from defib.cli.app import _agent_not_responding_message - msg = _agent_not_responding_message("hi3516av300", 0x81000000) + msg = _agent_not_responding_message("hi3516ev300", 0x41000000) assert "DDR" in msg # Surfaces the actual address so the user can match it against # what they see on the wire. - assert "0x81000000" in msg + assert "0x41000000" in msg def test_no_variants_path(self): from defib.cli.app import _agent_not_responding_message - msg = _agent_not_responding_message("hi3516av300", 0x81000000) - # Shipped profile has no variants today; message should say so + # hi3516ev300 ships with no variants — message should say so # rather than pretending one exists. + msg = _agent_not_responding_message("hi3516ev300", 0x41000000) assert "No board variants declared" in msg + def test_when_variants_exist_for_real_shipped_chip(self): + # hi3516av300 ships with the `emmc` variant. + from defib.cli.app import _agent_not_responding_message + msg = _agent_not_responding_message("hi3516av300", 0x81000000) + assert "emmc" in msg + # And a concrete next-command nudge + assert "defib agent upload -c hi3516av300:emmc" in msg + def test_mentions_vendor_uboot_loadx_fallback(self): from defib.cli.app import _agent_not_responding_message msg = _agent_not_responding_message("hi3516av300", 0x81000000) @@ -214,7 +222,8 @@ def test_mentions_vendor_uboot_loadx_fallback(self): assert "go 0x81000000" in msg def test_when_variants_exist_lists_them(self, monkeypatch): - # Pretend the chip ships with two variants + # Pretend a chip ships with two variants; this exercises the + # multi-variant formatting independently of what's shipped. import defib.cli.app as cli_app from defib.profiles import loader monkeypatch.setattr( diff --git a/tests/test_profiles.py b/tests/test_profiles.py index c210daf..77cddc9 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -198,11 +198,80 @@ def test_variants_follow_alias_chain(self, tmp_path: Path): assert p.ddr_step_data == bytes([9] * 64) assert list_variants("dv300_alias", tmp_path) == ["emmc"] - def test_real_av300_profile_still_loads(self): - """Smoke test: real shipped profile (no variants today) keeps working.""" + def test_real_av300_base_still_loads(self): + """Smoke test: real shipped profile loads without a variant.""" p = load_profile("hi3516av300", PROFILES_DIR) assert p.name == "hi3516av300" assert p.uboot_address == 0x81000000 + # Base profile has no SPL_BLOB — that's variant-specific + assert p.spl_data is None + + def test_real_av300_emmc_variant_loads_spl_blob(self): + """The shipped hi3516av300:emmc variant carries a vendor SPL blob + extracted from a working eMMC board. Validates that the variant + merging + SPL_BLOB resolution actually loads bytes from disk.""" + p = load_profile("hi3516av300:emmc", PROFILES_DIR) + assert p.name == "hi3516av300" + assert p.spl_blob == "hi3516av300-emmc-spl.bin" + assert p.spl_data is not None + # Vendor SPL starts with an ARM `b` instruction at offset 0 + # (same as any reasonable boot ROM payload). + assert p.spl_data[:4] == bytes.fromhex("150800ea") + # SPL extracted at 0x5000 (above the gzip boundary at ~0x5220) + assert len(p.spl_data) == 0x5000 + + +class TestSplBlob: + """SPL_BLOB lets a profile (typically a board variant) ship pre-built + SPL bytes instead of relying on the OpenIPC firmware download.""" + + @pytest.fixture + def chip_with_spl_blob(self, tmp_path: Path) -> Path: + (tmp_path / "test-spl.bin").write_bytes(b"\xAA" * 1024) + profile = { + "name": "blobchip", + "DDRSTEP0": [0] * 64, + "ADDRESS": ["0x0", "0x0", "0x0"], + "FILELEN": ["0x0", "0x0"], + "STEPLEN": ["0x0", "0x0"], + "SPL_BLOB": "test-spl.bin", + } + (tmp_path / "blobchip.json").write_text(json.dumps(profile)) + return tmp_path + + def test_loader_reads_blob_bytes(self, chip_with_spl_blob: Path): + p = load_profile("blobchip", chip_with_spl_blob) + assert p.spl_data == b"\xAA" * 1024 + + def test_missing_blob_raises(self, tmp_path: Path): + profile = { + "name": "badblob", + "DDRSTEP0": [0] * 64, + "ADDRESS": ["0x0", "0x0", "0x0"], + "FILELEN": ["0x0", "0x0"], + "STEPLEN": ["0x0", "0x0"], + "SPL_BLOB": "does-not-exist.bin", + } + (tmp_path / "badblob.json").write_text(json.dumps(profile)) + with pytest.raises(FileNotFoundError, match="SPL_BLOB"): + load_profile("badblob", tmp_path) + + def test_blob_via_variant(self, tmp_path: Path): + """Most realistic case: SPL_BLOB declared inside a variant block.""" + (tmp_path / "v-spl.bin").write_bytes(b"\x11" * 4096) + profile = { + "name": "chip", + "DDRSTEP0": [0] * 64, + "ADDRESS": ["0x0", "0x0", "0x0"], + "FILELEN": ["0x0", "0x0"], + "STEPLEN": ["0x0", "0x0"], + "variants": {"emmc": {"SPL_BLOB": "v-spl.bin"}}, + } + (tmp_path / "chip.json").write_text(json.dumps(profile)) + base = load_profile("chip", tmp_path) + assert base.spl_data is None + variant = load_profile("chip:emmc", tmp_path) + assert variant.spl_data == b"\x11" * 4096 class TestVariantStrippingInLookups: