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
18 changes: 18 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ Requires `arm-none-eabi-gcc` (Arch: `pacman -S arm-none-eabi-gcc arm-none-eabi-n

Addresses from [qemu-hisilicon](https://github.com/OpenIPC/LoTool) hardware definitions.

### Where defib looks for a built binary

No prebuilt agent ships in the pip/uv package -- the binary is bare-metal ARM
compiled per SoC, so it has to be built here first. `defib agent` searches, in
order:

1. `$DEFIB_AGENT_DIR`
2. `defib/agent/binaries/` inside the installed package
3. the agent cache directory (`defib agent` prints the path when it finds
nothing) -- next to the downloaded-firmware cache
4. this `agent/` directory, which is what a git checkout gets for free

Both `agent-<chip>.bin` and `agent-<build>.bin` are tried in each, so a
`make SOC=gk7205v300` build is found even though that chip shares the
gk7205v200 memory map. If you installed defib rather than cloning it, build
here and copy the `.bin` into the cache directory, or point `DEFIB_AGENT_DIR`
at this one.

`hi3520dv200` is a V1-era DVR/NVR SoC: Cortex-A9 (single core), `0x2xxxxxxx`
peripheral map, and a HISFC350 SPI flash controller (NOT the FMC100 used by
all other supported SoCs). The HISFC350 driver lives in
Expand Down
128 changes: 105 additions & 23 deletions src/defib/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from __future__ import annotations

import logging
import os
import struct
import zlib
from dataclasses import dataclass, field
Expand Down Expand Up @@ -142,44 +143,125 @@ def bad_block(self) -> list[SectorResult]:
FALLBACK_BAUD = 115200 # Always works


# Which agent build a chip uses. Several chips share one because they share a
# memory map, so the build name is not the chip name. Module scope so the "no
# binary" help text lists exactly what the lookup accepts.
_CHIP_TO_AGENT = {
"hi3516ev300": "hi3516ev300",
"hi3516ev200": "hi3516ev200",
"gk7205v200": "gk7205v200",
"gk7205v300": "gk7205v200",
"gk7202v300": "gk7205v200",
"hi3516cv300": "hi3516cv300",
"hi3516cv500": "hi3516cv500",
"hi3516av300": "hi3516cv500", # cv500-family, same memory map
"hi3516dv300": "hi3516cv500", # cv500-family, same memory map
"hi3519v101": "hi3519v101",
"hi3516av200": "hi3519v101", # 3519v101 family, same memory map
"hi3516cv610": "hi3516cv610",
"hi3516cv608": "hi3516cv610", # cv6xx-family, same memory map
"hi3518ev200": "hi3518ev200",
"hi3520dv200": "hi3520dv200", # V1-era, HISFC350 SPI controller
}


def get_agent_binary(chip: str) -> Path | None:
"""Get the path to the pre-compiled agent binary for a chip.

Accepts an optional ``:variant`` suffix (e.g. ``hi3516av300:emmc``);
the variant only affects DDR init, not the agent binary, so we strip
it before the lookup.
"""
chip = chip.split(":", 1)[0]
chip_to_agent = {
"hi3516ev300": "hi3516ev300",
"gk7205v200": "gk7205v200",
"gk7205v300": "gk7205v200",
"gk7202v300": "gk7205v200",
"hi3516cv300": "hi3516cv300",
"hi3516cv500": "hi3516cv500",
"hi3516av300": "hi3516cv500", # cv500-family, same memory map
"hi3516dv300": "hi3516cv500", # cv500-family, same memory map
"hi3519v101": "hi3519v101",
"hi3516av200": "hi3519v101", # 3519v101 family, same memory map
"hi3516cv610": "hi3516cv610",
"hi3516cv608": "hi3516cv610", # cv6xx-family, same memory map
"hi3518ev200": "hi3518ev200",
"hi3520dv200": "hi3520dv200", # V1-era, HISFC350 SPI controller
}

agent_name = chip_to_agent.get(chip.lower())
agent_name = agent_binary_for(chip)
if not agent_name:
return None

candidates = [
Path(__file__).parent.parent.parent.parent / "agent" / f"agent-{agent_name}.bin",
]
for path in candidates:
for path in _agent_search_path(agent_name, chip):
if path.exists():
return path
return None


def agent_binary_for(chip: str) -> str | None:
"""The agent build this chip uses, or None if there is no agent for it.

Several chips share one binary because they share a memory map, so the
build name is not the chip name.
"""
return _CHIP_TO_AGENT.get(chip.split(":", 1)[0].lower())


def _agent_search_path(agent_name: str, chip: str = "") -> list[Path]:
"""Where an agent binary may be found, nearest first.

The only entry this used to have was the git checkout, four levels up from
this module. In an installed package that resolves inside site-packages and
can never exist, so `defib agent` was unreachable for anyone who installed
defib the documented way -- the reporter in OpenIPC/firmware#2381 hit it on
a uv tool install. The binary still has to be compiled per SoC, so the
honest fix is to look where a compiled one plausibly is and to say how to
build one when it is not there.

Both names are tried in every directory. `make` names its output after the
SOC it was given, and several chips map onto another chip's build, so
`make SOC=gk7205v300` leaves an agent-gk7205v300.bin that a search for the
mapped agent-gk7205v200.bin would walk straight past.
"""
names = [n for n in (chip.split(":", 1)[0].lower(), agent_name) if n]
filenames = list(dict.fromkeys(f"agent-{n}.bin" for n in names))

directories = []
override = os.environ.get("DEFIB_AGENT_DIR")
if override:
directories.append(Path(override))
# Shipped inside the package, for a wheel that carries prebuilt agents.
directories.append(Path(__file__).parent / "binaries")
# Built by hand and dropped in the cache next to downloaded firmware.
directories.append(get_agent_cache_dir())
# A git checkout with `make SOC=<soc>` already run in agent/.
directories.append(Path(__file__).parent.parent.parent.parent / "agent")
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

return [d / f for d in directories for f in filenames]


def get_agent_cache_dir() -> Path:
"""Where a locally built agent binary can be dropped to be found.

Names the directory without making it. Building the search path must stay
free of side effects: an unwritable cache location used to raise out of
`get_agent_binary` even when DEFIB_AGENT_DIR held the binary, and out of
the help text whose whole job is to explain that nothing was found.
"""
from defib.firmware import get_cache_dir

return get_cache_dir(create=False).parent / "agent"


def agent_binary_help(chip: str) -> str:
"""Say why there is no agent binary, and what to do about it."""
agent_name = agent_binary_for(chip)
if not agent_name:
supported = ", ".join(sorted(set(_CHIP_TO_AGENT)))
return (
f"There is no flash agent for '{chip}'. The agent supports: "
f"{supported}. Use `defib install` or `defib restore`, which drive "
f"U-Boot over TFTP instead and work on every supported chip."
)
looked_in = "\n ".join(str(p) for p in _agent_search_path(agent_name, chip))
return (
f"No agent binary for '{chip}' (it uses the {agent_name} build).\n"
f"The agent is bare-metal C compiled per SoC, and no prebuilt binary "
f"ships in the package -- build it from a defib checkout:\n"
f" git clone https://github.com/OpenIPC/defib && cd defib/agent\n"
f" make SOC={agent_name} # needs arm-none-eabi-gcc\n"
f"then either run defib from that checkout, copy agent-{agent_name}.bin "
f"into {get_agent_cache_dir()}, or point DEFIB_AGENT_DIR at it.\n"
f"Looked in:\n {looked_in}\n"
f"Or skip the agent: `defib install` and `defib restore` drive U-Boot "
f"over TFTP and need nothing compiled."
)


class FlashAgentClient:
"""Client for the bare-metal flash agent.

Expand Down
26 changes: 18 additions & 8 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ async def _network_async(

from rich.console import Console

from defib.network.ip_manager import list_interfaces, temporary_ip
from defib.network.ip_manager import list_interfaces_async, temporary_ip
from defib.network.tftp_server import start_tftp_server

console = Console()
Expand All @@ -851,7 +851,7 @@ async def _network_async(

# Determine network interface
if not nic:
interfaces = list_interfaces()
interfaces = await list_interfaces_async()
if interfaces:
nic = interfaces[0]
if output == "human":
Expand Down Expand Up @@ -1036,7 +1036,9 @@ async def _agent_upload_async(

from rich.console import Console

from defib.agent.client import FlashAgentClient, get_agent_binary
from defib.agent.client import (
FlashAgentClient, agent_binary_help, get_agent_binary,
)
from defib.firmware import get_cached_path
from defib.profiles.loader import load_profile
from defib.protocol.hisilicon_cv6xx import HiSiliconCV6xx
Expand All @@ -1052,7 +1054,10 @@ async def _agent_upload_async(
# Find agent binary
agent_path = get_agent_binary(chip)
if not agent_path:
msg = f"No agent binary for '{chip}'"
# A bare "No agent binary for '<chip>'" reads as an unsupported chip,
# which sent the reporter in OpenIPC/firmware#2381 looking for another
# tool when the answer was that nothing had compiled it yet.
msg = agent_binary_help(chip)
if output == "json":
print(json_mod.dumps({"event": "error", "message": msg}))
else:
Expand Down Expand Up @@ -1348,7 +1353,9 @@ async def _agent_flash_async(

from rich.console import Console

from defib.agent.client import FlashAgentClient, get_agent_binary
from defib.agent.client import (
FlashAgentClient, agent_binary_help, get_agent_binary,
)
from defib.firmware import get_cached_path
from defib.profiles.loader import load_profile
from defib.protocol.hisilicon_standard import HiSiliconStandard
Expand All @@ -1375,7 +1382,10 @@ async def _agent_flash_async(
# --- Find agent binary ---
agent_path = get_agent_binary(chip)
if not agent_path:
msg = f"No agent binary for '{chip}'"
# A bare "No agent binary for '<chip>'" reads as an unsupported chip,
# which sent the reporter in OpenIPC/firmware#2381 looking for another
# tool when the answer was that nothing had compiled it yet.
msg = agent_binary_help(chip)
if output == "json":
print(json_mod.dumps({"event": "error", "message": msg}))
else:
Expand Down Expand Up @@ -2217,7 +2227,7 @@ async def _install_async(
has_firmware,
pad_to_size,
)
from defib.network.ip_manager import list_interfaces, temporary_ip
from defib.network.ip_manager import list_interfaces_async, temporary_ip
from defib.network.tftp_server import start_tftp_server
from defib.recovery.events import LogEvent, ProgressEvent
from defib.recovery.session import RecoverySession
Expand Down Expand Up @@ -2590,7 +2600,7 @@ async def _cmd(cmd: str, timeout: float = 60.0, **kw: object) -> str:
if not use_pod_tftp:
# Host TFTP needs a NIC + host_ip; pod path needs neither.
if not nic:
interfaces = list_interfaces()
interfaces = await list_interfaces_async()
if interfaces:
nic = interfaces[0]
else:
Expand Down
12 changes: 9 additions & 3 deletions src/defib/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,22 @@
}


def get_cache_dir() -> Path:
"""Get platform-appropriate cache directory for downloaded firmware."""
def get_cache_dir(create: bool = True) -> Path:
"""Get platform-appropriate cache directory for downloaded firmware.

`create=False` just names the directory. Callers that are only searching
should use it: an unwritable cache location must not stop a lookup that
would have found the file somewhere else entirely.
"""
if sys.platform == "darwin":
base = Path.home() / "Library" / "Caches"
elif sys.platform == "win32":
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
else:
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
cache_dir = base / "defib" / "firmware"
cache_dir.mkdir(parents=True, exist_ok=True)
if create:
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir


Expand Down
Loading
Loading