Add Slice 4 tuner stack with AGENTS-compliant core types and HTTP API.
Introduces Si4684/ADAU1701 drivers, TunerService, FrequencyKHz, SeekDirection, /api/tuner routes without file-scope globals, host tests, and firmware blob tooling (binaries remain gitignored). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""extract_si4684_blob.py — convert Si4684 C arrays or copies to .bin files.
|
||||
|
||||
DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
|
||||
Copyright 2026 Michele Bigi
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Usage:
|
||||
# From a SigmaStudio-style C header (PE5PVB firmware.h, Si468xROM.h):
|
||||
tools/extract_si4684_blob.py --header src/firmware.h --array firmware \\
|
||||
--out Firmware/Si4684-Firmware/dab_firmware.bin
|
||||
|
||||
# Copy an existing Skyworks .bin from an eval-board folder:
|
||||
tools/extract_si4684_blob.py --copy ~/si46xx_firmware/fm_radio_5_1_0.bin \\
|
||||
--out Firmware/Si4684-Firmware/fm_firmware.bin
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_c_array(header: Path, array_name: str) -> bytes:
|
||||
text = header.read_text(encoding="utf-8", errors="replace")
|
||||
pattern = rf"{re.escape(array_name)}\s*\[\s*\]\s*=\s*\{{(.*?)\}};"
|
||||
match = re.search(pattern, text, re.S)
|
||||
if not match:
|
||||
raise SystemExit(f"array {array_name!r} not found in {header}")
|
||||
values = re.findall(r"0x[0-9a-fA-F]+", match.group(1))
|
||||
if not values:
|
||||
raise SystemExit(f"array {array_name!r} is empty in {header}")
|
||||
return bytes(int(v, 16) for v in values)
|
||||
|
||||
|
||||
def extract_flash_region(dump: Path, offset: int, size: int) -> bytes:
|
||||
data = dump.read_bytes()
|
||||
end = offset + size
|
||||
if end > len(data):
|
||||
raise SystemExit(
|
||||
f"flash dump too small: need {end} bytes, have {len(data)}"
|
||||
)
|
||||
return data[offset:end]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--out", type=Path, required=True, help="output .bin path")
|
||||
src = ap.add_mutually_exclusive_group(required=True)
|
||||
src.add_argument("--header", type=Path, help="C header with byte array")
|
||||
src.add_argument("--copy", type=Path, help="existing firmware .bin to copy")
|
||||
src.add_argument("--flash-dump", type=Path, help="full SPI flash dump")
|
||||
ap.add_argument("--array", help="array name when using --header")
|
||||
ap.add_argument(
|
||||
"--offset",
|
||||
type=lambda s: int(s, 0),
|
||||
default=0x106000,
|
||||
help="byte offset for --flash-dump (default: FM region from dirb.tech)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--size",
|
||||
type=lambda s: int(s, 0),
|
||||
default=0x81721,
|
||||
help="byte length for --flash-dump (default: FM V5.1.0 size)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.header is not None:
|
||||
if not args.array:
|
||||
ap.error("--array is required with --header")
|
||||
payload = extract_c_array(args.header, args.array)
|
||||
elif args.copy is not None:
|
||||
payload = args.copy.read_bytes()
|
||||
else:
|
||||
payload = extract_flash_region(args.flash_dump, args.offset, args.size)
|
||||
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_bytes(payload)
|
||||
print(f"wrote {args.out} ({len(payload)} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
"""extract_ugreen_radio_cli.py — pull Si468x blobs from uGreen radio_cli ELF.
|
||||
|
||||
DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
|
||||
Copyright 2026 Michele Bigi
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
uGreen embeds Skyworks firmware inside ``radio_cli`` (Files_v16.zip). This module
|
||||
locates ``fw_*`` rodata symbols and writes raw ``.bin`` payloads for DigiRadio
|
||||
HOST_LOAD boot (AN649 / PE5PVB flow).
|
||||
|
||||
Obtain Files_v16.zip from https://ugreen.eu/downloads/ (DABBoard customer pack).
|
||||
Do not redistribute extracted blobs in public repositories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# Exact HOST_LOAD sizes observed in radio_cli power_up_and_load (ARM64 builds).
|
||||
UGREEN_KNOWN_SIZES: dict[str, int] = {
|
||||
"fw_rom00_patch016bin": 5796,
|
||||
"fw_dab_radio_5_0_5bin": 521_448,
|
||||
"fw_dab_radio_6_0_6bin": 496_944,
|
||||
"fw_fmhd_radio_5_0_4bin": 530_184,
|
||||
"fw_fmhd_radio_5_1_3bin": 531_300,
|
||||
}
|
||||
|
||||
FM_SYMBOL_PRIORITY = (
|
||||
"fw_fmhd_radio_5_1_3bin",
|
||||
"fw_fmhd_radio_5_1_0bin",
|
||||
"fw_fm_radio_5_1_0bin",
|
||||
"fw_fmhd_radio_5_0_4bin",
|
||||
"fw_fm_radio_5_0_9bin",
|
||||
)
|
||||
|
||||
DAB_SYMBOL_PRIORITY = (
|
||||
"fw_dab_radio_6_0_6bin",
|
||||
"fw_dab_radio_5_0_8bin",
|
||||
"fw_dab_radio_5_0_5bin",
|
||||
"fw_dab_radio_4_0_5bif",
|
||||
)
|
||||
|
||||
PATCH_SYMBOL = "fw_rom00_patch016bin"
|
||||
|
||||
RADIO_CLI_GLOB = ("radio_cli_v*", "radio_cli")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FirmwareBlob:
|
||||
symbol: str
|
||||
offset: int
|
||||
size: int
|
||||
data: bytes
|
||||
|
||||
|
||||
def resolve_radio_cli(path: Path) -> Path:
|
||||
"""Return path to radio_cli ELF inside a file, zip, or directory."""
|
||||
path = path.expanduser().resolve()
|
||||
if path.is_file() and zipfile.is_zipfile(path):
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
names = [
|
||||
n
|
||||
for n in zf.namelist()
|
||||
if "radio_cli" in Path(n).name and not n.endswith("/")
|
||||
]
|
||||
if not names:
|
||||
raise SystemExit(f"No radio_cli binary found inside {path}")
|
||||
# Prefer 64-bit builds (same blobs, easier to test on desktop nm).
|
||||
names.sort(key=lambda n: ("64-bit" not in n, n))
|
||||
chosen = names[0]
|
||||
tmp_dir = Path(path.parent, f".ugreen-extract-{path.stem}")
|
||||
tmp_dir.mkdir(exist_ok=True)
|
||||
out = tmp_dir / Path(chosen).name
|
||||
if not out.exists() or out.stat().st_mtime < path.stat().st_mtime:
|
||||
out.write_bytes(zf.read(chosen))
|
||||
return out
|
||||
|
||||
if path.is_dir():
|
||||
for pattern in ("**/bin/64-bit/radio_cli*", "**/bin/32-bit/radio_cli*"):
|
||||
matches = sorted(path.glob(pattern))
|
||||
if matches:
|
||||
return matches[0]
|
||||
for pattern in RADIO_CLI_GLOB:
|
||||
matches = sorted(path.rglob(pattern))
|
||||
if matches:
|
||||
return matches[0]
|
||||
raise SystemExit(f"No radio_cli binary under {path}")
|
||||
|
||||
if path.is_file():
|
||||
return path
|
||||
|
||||
raise SystemExit(f"Path not found: {path}")
|
||||
|
||||
|
||||
def _run_nm(elf: Path) -> str:
|
||||
for cmd in (["llvm-nm", str(elf)], ["nm", str(elf)]):
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, check=False
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if proc.returncode == 0 and proc.stdout.strip():
|
||||
return proc.stdout
|
||||
raise SystemExit(
|
||||
f"Could not read symbols from {elf} (install nm or llvm-nm)"
|
||||
)
|
||||
|
||||
|
||||
def _parse_nm_output(text: str) -> dict[str, int]:
|
||||
symbols: dict[str, int] = {}
|
||||
for line in text.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
addr_s, sym_type, name = parts[0], parts[1], parts[2]
|
||||
if sym_type not in {"R", "D", "r"}:
|
||||
continue
|
||||
if not name.startswith("fw_"):
|
||||
continue
|
||||
try:
|
||||
symbols[name] = int(addr_s, 16)
|
||||
except ValueError:
|
||||
continue
|
||||
return symbols
|
||||
|
||||
|
||||
def _elf_vma_to_file_offset(elf: Path, vma: int) -> int:
|
||||
data = elf.read_bytes()
|
||||
if data[:4] != b"\x7fELF":
|
||||
raise SystemExit(f"Not an ELF file: {elf}")
|
||||
|
||||
ei_class = data[4]
|
||||
if ei_class == 1:
|
||||
e_shoff = struct.unpack_from("<I", data, 0x20)[0]
|
||||
e_shentsize = struct.unpack_from("<H", data, 0x2E)[0]
|
||||
e_shnum = struct.unpack_from("<H", data, 0x30)[0]
|
||||
e_shstrndx = struct.unpack_from("<H", data, 0x32)[0]
|
||||
sh_fmt = "<IIIIIIII"
|
||||
elif ei_class == 2:
|
||||
e_shoff = struct.unpack_from("<Q", data, 0x28)[0]
|
||||
e_shentsize = struct.unpack_from("<H", data, 0x3A)[0]
|
||||
e_shnum = struct.unpack_from("<H", data, 0x3C)[0]
|
||||
e_shstrndx = struct.unpack_from("<H", data, 0x3E)[0]
|
||||
sh_fmt = "<IIQQQQIIQQ"
|
||||
else:
|
||||
raise SystemExit(f"Unsupported ELF class in {elf}")
|
||||
|
||||
if e_shnum == 0:
|
||||
return vma
|
||||
|
||||
def read_section(index: int) -> tuple[int, int, int]:
|
||||
off = e_shoff + index * e_shentsize
|
||||
fields = struct.unpack_from(sh_fmt, data, off)
|
||||
if ei_class == 1:
|
||||
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size = fields[:6]
|
||||
else:
|
||||
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size = fields[:6]
|
||||
return sh_addr, sh_offset, sh_size
|
||||
|
||||
shstr_off = read_section(e_shstrndx)[1]
|
||||
|
||||
for index in range(e_shnum):
|
||||
sh_addr, sh_offset, sh_size = read_section(index)
|
||||
if sh_size == 0:
|
||||
continue
|
||||
if sh_addr <= vma < sh_addr + sh_size:
|
||||
return sh_offset + (vma - sh_addr)
|
||||
|
||||
return vma
|
||||
|
||||
|
||||
def _pick_symbol(symbols: dict[str, int], priority: tuple[str, ...]) -> str | None:
|
||||
for name in priority:
|
||||
if name in symbols:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def _blob_size(
|
||||
symbol: str,
|
||||
vma: int,
|
||||
ordered: list[tuple[str, int]],
|
||||
file_size: int,
|
||||
file_offset: int,
|
||||
) -> int:
|
||||
if symbol in UGREEN_KNOWN_SIZES:
|
||||
return UGREEN_KNOWN_SIZES[symbol]
|
||||
|
||||
for name, addr in ordered:
|
||||
if addr > vma:
|
||||
return addr - vma
|
||||
|
||||
return file_size - file_offset
|
||||
|
||||
|
||||
def extract_blob(elf: Path, symbol: str, symbols: dict[str, int]) -> FirmwareBlob:
|
||||
if symbol not in symbols:
|
||||
raise SystemExit(f"Symbol {symbol!r} not found in {elf}")
|
||||
|
||||
vma = symbols[symbol]
|
||||
file_offset = _elf_vma_to_file_offset(elf, vma)
|
||||
ordered = sorted(symbols.items(), key=lambda item: item[1])
|
||||
size = _blob_size(
|
||||
symbol, vma, ordered, elf.stat().st_size, file_offset
|
||||
)
|
||||
|
||||
data = elf.read_bytes()
|
||||
end = file_offset + size
|
||||
if end > len(data):
|
||||
raise SystemExit(
|
||||
f"{symbol}: need {size} bytes at file offset 0x{file_offset:x}, "
|
||||
f"ELF size is {len(data)}"
|
||||
)
|
||||
|
||||
payload = data[file_offset:end]
|
||||
if len(payload) != size:
|
||||
raise SystemExit(f"{symbol}: short read ({len(payload)} != {size})")
|
||||
|
||||
return FirmwareBlob(symbol, file_offset, size, payload)
|
||||
|
||||
|
||||
def extract_images(
|
||||
source: Path,
|
||||
*,
|
||||
want_fm: bool = True,
|
||||
want_dab: bool = False,
|
||||
want_patch: bool = False,
|
||||
) -> dict[str, FirmwareBlob]:
|
||||
elf = resolve_radio_cli(source)
|
||||
symbols = _parse_nm_output(_run_nm(elf))
|
||||
if not symbols:
|
||||
raise SystemExit(f"No fw_* symbols in {elf}")
|
||||
|
||||
out: dict[str, FirmwareBlob] = {}
|
||||
|
||||
if want_patch:
|
||||
out["patch"] = extract_blob(elf, PATCH_SYMBOL, symbols)
|
||||
|
||||
if want_dab:
|
||||
dab_sym = _pick_symbol(symbols, DAB_SYMBOL_PRIORITY)
|
||||
if dab_sym is None:
|
||||
raise SystemExit(f"No DAB fw_* symbol in {elf}")
|
||||
out["dab"] = extract_blob(elf, dab_sym, symbols)
|
||||
|
||||
if want_fm:
|
||||
fm_sym = _pick_symbol(symbols, FM_SYMBOL_PRIORITY)
|
||||
if fm_sym is None:
|
||||
raise SystemExit(f"No FM fw_* symbol in {elf}")
|
||||
out["fm"] = extract_blob(elf, fm_sym, symbols)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def write_blobs(blobs: dict[str, FirmwareBlob], out_dir: Path) -> None:
|
||||
mapping = {
|
||||
"patch": "rom_patch_016.bin",
|
||||
"dab": "dab_firmware.bin",
|
||||
"fm": "fm_firmware.bin",
|
||||
}
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for key, blob in blobs.items():
|
||||
dest = out_dir / mapping[key]
|
||||
dest.write_bytes(blob.data)
|
||||
print(
|
||||
f"wrote {dest} ({len(blob.data)} bytes) "
|
||||
f"from {blob.symbol} @ 0x{blob.offset:x}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"source",
|
||||
type=Path,
|
||||
help="radio_cli ELF, Files_v16.zip, or unpacked Files_v16 directory",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parents[1] / "Firmware" / "Si4684-Firmware",
|
||||
help="output directory for .bin files",
|
||||
)
|
||||
ap.add_argument("--fm", action="store_true", help="extract FM image")
|
||||
ap.add_argument("--dab", action="store_true", help="extract DAB image")
|
||||
ap.add_argument("--patch", action="store_true", help="extract ROM patch")
|
||||
ap.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="extract patch, DAB, and FM",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.all:
|
||||
want_fm = want_dab = want_patch = True
|
||||
else:
|
||||
want_fm = args.fm
|
||||
want_dab = args.dab
|
||||
want_patch = args.patch
|
||||
if not (want_fm or want_dab or want_patch):
|
||||
want_fm = True
|
||||
|
||||
blobs = extract_images(
|
||||
args.source,
|
||||
want_fm=want_fm,
|
||||
want_dab=want_dab,
|
||||
want_patch=want_patch,
|
||||
)
|
||||
write_blobs(blobs, args.out_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+232
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fetch_si4684_firmware.py — populate Firmware/Si4684-Firmware/ blobs.
|
||||
|
||||
DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
|
||||
Copyright 2026 Michele Bigi
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
DAB images are extracted from PE5PVB/SI4684-DAB-Receiver (same as Slice 3).
|
||||
FM images are proprietary Skyworks blobs — supply via --si46xx-dir (eval CD /
|
||||
dabpi si46xx_firmware folder), --flash-dump (TechniSat-style SPI readout), or
|
||||
--from-ugreen-radio-cli (uGreen DABBoard Files_v16.zip / radio_cli ELF).
|
||||
|
||||
AN649 Si4684-A10: FM application image fm_radio_5_1_0.bin (or community
|
||||
fmhd_radio_5_0_4.bin on Si4688-class boards — verify on hardware).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
_TOOLS = Path(__file__).resolve().parent
|
||||
if str(_TOOLS) not in sys.path:
|
||||
sys.path.insert(0, str(_TOOLS))
|
||||
|
||||
from extract_ugreen_radio_cli import extract_images, write_blobs # noqa: E402
|
||||
|
||||
ROOT = _TOOLS.parent
|
||||
FW_DIR = ROOT / "Firmware" / "Si4684-Firmware"
|
||||
EXTRACT = _TOOLS / "extract_si4684_blob.py"
|
||||
PE5PVB = "https://github.com/PE5PVB/SI4684-DAB-Receiver.git"
|
||||
|
||||
FM_CANDIDATE_NAMES = (
|
||||
"fm_radio_5_1_0.bin",
|
||||
"fm_radio_5_0_9.bin",
|
||||
"fmhd_radio_5_1_0.bin",
|
||||
"fmhd_radio_5_0_4.bin",
|
||||
)
|
||||
|
||||
|
||||
def run_extract(args: list[str]) -> None:
|
||||
cmd = [sys.executable, str(EXTRACT), *args]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
def clone_pe5pvb(tmp: Path) -> Path:
|
||||
dest = tmp / "SI4684-DAB-Receiver"
|
||||
if not dest.exists():
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", PE5PVB, str(dest)],
|
||||
check=True,
|
||||
)
|
||||
return dest
|
||||
|
||||
|
||||
def refresh_dab(tmp: Path) -> None:
|
||||
repo = clone_pe5pvb(tmp)
|
||||
run_extract(
|
||||
[
|
||||
"--header",
|
||||
str(repo / "src" / "Si468xROM.h"),
|
||||
"--array",
|
||||
"rom_patch_016",
|
||||
"--out",
|
||||
str(FW_DIR / "rom_patch_016.bin"),
|
||||
]
|
||||
)
|
||||
run_extract(
|
||||
[
|
||||
"--header",
|
||||
str(repo / "src" / "firmware.h"),
|
||||
"--array",
|
||||
"firmware",
|
||||
"--out",
|
||||
str(FW_DIR / "dab_firmware.bin"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def find_fm_source(si46xx_dir: Path) -> Path | None:
|
||||
for name in FM_CANDIDATE_NAMES:
|
||||
candidate = si46xx_dir / name
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def import_fm(source: Path) -> None:
|
||||
run_extract(["--copy", str(source), "--out", str(FW_DIR / "fm_firmware.bin")])
|
||||
|
||||
|
||||
def import_fm_from_flash(dump: Path, offset: int, size: int) -> None:
|
||||
run_extract(
|
||||
[
|
||||
"--flash-dump",
|
||||
str(dump),
|
||||
"--offset",
|
||||
hex(offset),
|
||||
"--size",
|
||||
hex(size),
|
||||
"--out",
|
||||
str(FW_DIR / "fm_firmware.bin"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def import_from_ugreen(
|
||||
source: Path,
|
||||
*,
|
||||
want_fm: bool,
|
||||
want_dab: bool,
|
||||
want_patch: bool,
|
||||
) -> None:
|
||||
blobs = extract_images(
|
||||
source,
|
||||
want_fm=want_fm,
|
||||
want_dab=want_dab,
|
||||
want_patch=want_patch,
|
||||
)
|
||||
write_blobs(blobs, FW_DIR)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--si46xx-dir",
|
||||
type=Path,
|
||||
help="folder with Skyworks .bin files (eval CD / dabpi si46xx_firmware)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--flash-dump",
|
||||
type=Path,
|
||||
help="full external SPI dump (TechniSat-style layout)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--fm-offset",
|
||||
type=lambda s: int(s, 0),
|
||||
default=0x106000,
|
||||
help="FM image offset inside --flash-dump",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--fm-size",
|
||||
type=lambda s: int(s, 0),
|
||||
default=0x81721,
|
||||
help="FM image size inside --flash-dump",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--dab-only",
|
||||
action="store_true",
|
||||
help="refresh DAB + patch only (skip FM)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--from-ugreen-radio-cli",
|
||||
type=Path,
|
||||
metavar="PATH",
|
||||
help="uGreen Files_v16.zip, radio_cli ELF, or unpacked Files_v16 folder",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--ugreen-all",
|
||||
action="store_true",
|
||||
help="with --from-ugreen-radio-cli: extract patch, DAB, and FM from radio_cli",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
FW_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ugreen = args.from_ugreen_radio_cli is not None
|
||||
if ugreen and args.dab_only:
|
||||
ap.error("--dab-only cannot be combined with --from-ugreen-radio-cli")
|
||||
|
||||
fm_sources = int(args.si46xx_dir is not None) + int(
|
||||
args.flash_dump is not None
|
||||
) + int(ugreen)
|
||||
if fm_sources > 1:
|
||||
ap.error(
|
||||
"choose one FM source: --si46xx-dir, --flash-dump, or "
|
||||
"--from-ugreen-radio-cli"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="digiradio-si4684-") as tmp_name:
|
||||
tmp = Path(tmp_name)
|
||||
|
||||
if args.dab_only:
|
||||
refresh_dab(tmp)
|
||||
print("DAB + patch refreshed; FM unchanged.")
|
||||
return 0
|
||||
|
||||
ugreen_full = ugreen and args.ugreen_all
|
||||
if not ugreen_full:
|
||||
refresh_dab(tmp)
|
||||
|
||||
if ugreen:
|
||||
import_from_ugreen(
|
||||
args.from_ugreen_radio_cli,
|
||||
want_fm=True,
|
||||
want_dab=ugreen_full,
|
||||
want_patch=ugreen_full,
|
||||
)
|
||||
elif args.si46xx_dir is not None:
|
||||
fm = find_fm_source(args.si46xx_dir)
|
||||
if fm is None:
|
||||
print(
|
||||
f"No FM image found in {args.si46xx_dir}; expected one of:",
|
||||
", ".join(FM_CANDIDATE_NAMES),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
import_fm(fm)
|
||||
elif args.flash_dump is not None:
|
||||
import_fm_from_flash(args.flash_dump, args.fm_offset, args.fm_size)
|
||||
else:
|
||||
print(
|
||||
"FM image not updated: pass --si46xx-dir, --flash-dump, or "
|
||||
"--from-ugreen-radio-cli.\n"
|
||||
"Skyworks fm_radio_5_1_0.bin is on the Si4684 eval CD "
|
||||
"(see AN649 table 1).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
fm_path = FW_DIR / "fm_firmware.bin"
|
||||
if fm_path.is_file():
|
||||
print(f"FM image ready: {fm_path} ({fm_path.stat().st_size} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user