Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 17 additions & 3 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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})")
Expand Down
Binary file not shown.
14 changes: 13 additions & 1 deletion src/defib/profiles/data/hi3516av300.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
14 changes: 13 additions & 1 deletion src/defib/profiles/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
22 changes: 21 additions & 1 deletion src/defib/profiles/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, PrivateAttr


class SoCProfile(BaseModel):
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 14 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,27 +194,36 @@ 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)
assert "loady" in msg
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(
Expand Down
73 changes: 71 additions & 2 deletions tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading