# flasher.py - tested on MacOS
This commit is contained in:
parent
b55ec8532c
commit
ad77c897e3
1 changed files with 186 additions and 291 deletions
|
|
@ -1,10 +1,26 @@
|
||||||
"""
|
"""
|
||||||
flasher.py — обёртка над tools/host/flash_usb.py для TUI.
|
flasher.py — async-обёртка над app/flash_backend.py для TUI.
|
||||||
|
|
||||||
Запускает flash_usb.py как subprocess, парсит stdout для прогресса,
|
Фаза 2: внутренности переведены с subprocess (flash_usb.py) на прямые
|
||||||
пробрасывает события через asyncio.Queue в TUI.
|
вызовы синхронного app/flash_backend.py через asyncio.to_thread. Публичный
|
||||||
|
API класса Flasher не менялся с Фазы 0 — сигнатуры те же, что в исходной
|
||||||
|
subprocess-версии.
|
||||||
|
|
||||||
Не дублирует spsdk-окружение — переиспользует tools/host/ uv-проект.
|
Мост sync → async для прогресса: flash_backend отдаёт события через
|
||||||
|
синхронный callback (вызывается из worker-потока asyncio.to_thread), здесь
|
||||||
|
он оборачивается в asyncio.run_coroutine_threadsafe(...).result() —
|
||||||
|
согласовано с автором проекта (Фаза 2, вопрос 1): блокирующий .result()
|
||||||
|
внутри worker-потока гарантирует, что события прогресса приходят в TUI
|
||||||
|
строго по порядку.
|
||||||
|
|
||||||
|
Отмена прошивки посреди операции НЕ поддерживается и не должна
|
||||||
|
поддерживаться (согласовано, Фаза 2, вопрос 2) — если стирание/запись уже
|
||||||
|
начались, они обязаны докрутиться до конца (успешно или с ошибкой).
|
||||||
|
Специального кода для этого не потребовалось: Python-поток, запущенный
|
||||||
|
через asyncio.to_thread, нельзя прервать снаружи — если ожидающая
|
||||||
|
корутина в TUI получит CancelledError, сам поток всё равно продолжит
|
||||||
|
работу в фоне до естественного завершения flash_backend.flash()/
|
||||||
|
erase_chip(). Это и есть требуемое поведение, а не обходной путь.
|
||||||
|
|
||||||
Публичный API:
|
Публичный API:
|
||||||
Flasher.flash(target, bin_path, progress_cb) — async, прогресс через callback
|
Flasher.flash(target, bin_path, progress_cb) — async, прогресс через callback
|
||||||
|
|
@ -18,31 +34,30 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Awaitable, Callable, Optional
|
from typing import Awaitable, Callable, Optional
|
||||||
|
|
||||||
|
from . import flash_backend
|
||||||
from .models import FcbVariant, FlashProgress, FlashTarget
|
from .models import FcbVariant, FlashProgress, FlashTarget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# VID/PID констант — читаются из env, fallback на известные значения
|
|
||||||
_BOOTROM_VID = int(os.environ.get("BOOTROM_VID", "0x1fc9"), 16)
|
|
||||||
_BOOTROM_PID = int(os.environ.get("BOOTROM_PID", "0x0130"), 16)
|
|
||||||
_CDC_VID = int(os.environ.get("SERVICE_CDC_VID", "0x1996"), 16)
|
|
||||||
_CDC_PID = int(os.environ.get("SERVICE_CDC_PID", "0x00ad"), 16)
|
|
||||||
|
|
||||||
# Тип сборки firmware_test для прошивки (Debug | Release).
|
# Тип сборки firmware_test для прошивки (Debug | Release).
|
||||||
# Release временно нестабилен (см. отчёт о тестировании) — по умолчанию Debug.
|
# Release временно нестабилен (см. отчёт о тестировании) — по умолчанию Debug.
|
||||||
_FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug")
|
_FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug")
|
||||||
|
|
||||||
# Путь до flash_usb.py относительно корня репозитория
|
# tools/host/build (или переопределено через BUILD_DIR) — та же логика,
|
||||||
_FLASH_USB_SCRIPT = Path(__file__).parents[3] / "tools" / "host" / "flash_usb.py"
|
# что раньше жила внутри flash_usb.py::main() и была ему видна только
|
||||||
_HOST_TOOLS_DIR = _FLASH_USB_SCRIPT.parent
|
# через CLI-аргументы --firmware/--build-type. Теперь резолвим сами.
|
||||||
_HAB_DIR = _HOST_TOOLS_DIR / "hab"
|
_BUILD_DIR = Path(os.environ.get("BUILD_DIR", str(flash_backend.REPO_ROOT / "build")))
|
||||||
_DCD_DIR = _HOST_TOOLS_DIR / "dcd"
|
|
||||||
|
|
||||||
|
def _firmware_hab_path(firmware: str) -> Path:
|
||||||
|
"""Путь к готовому HAB-образу штатной прошивки (см. build.just — тот же
|
||||||
|
файл, что собирает `nxpimage hab export` при обычной сборке)."""
|
||||||
|
return _BUILD_DIR / _FIRMWARE_BUILD_TYPE / f"{firmware}_hab.bin"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_custom_binaries_dir() -> Path:
|
def _resolve_custom_binaries_dir() -> Path:
|
||||||
|
|
@ -67,103 +82,114 @@ def _resolve_custom_binaries_dir() -> Path:
|
||||||
|
|
||||||
CUSTOM_BINARIES_DIR = _resolve_custom_binaries_dir()
|
CUSTOM_BINARIES_DIR = _resolve_custom_binaries_dir()
|
||||||
|
|
||||||
# Паттерны stdout flash_usb.py для извлечения прогресса
|
|
||||||
_RE_PERCENT = re.compile(r"(\d{1,3})\s*%")
|
|
||||||
_RE_PHASE = re.compile(r"(sdphost|blhost|Writing|Erasing|Verifying)", re.IGNORECASE)
|
|
||||||
|
|
||||||
ProgressCallback = Callable[[FlashProgress], Awaitable[None]]
|
ProgressCallback = Callable[[FlashProgress], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
def _detect_usb(vid: int, pid: int) -> bool:
|
def _make_sync_progress_cb(
|
||||||
|
progress_cb: Optional[ProgressCallback],
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
) -> Optional[flash_backend.ProgressCallback]:
|
||||||
|
"""Мост sync (flash_backend, вызывается из worker-потока) → async (TUI).
|
||||||
|
|
||||||
|
.result() — намеренно блокирующий вызов внутри worker-потока: гарантирует
|
||||||
|
доставку событий прогресса в порядке их возникновения (см. docstring
|
||||||
|
модуля). Исключения из progress_cb (например, если экран уже закрыт)
|
||||||
|
логируются и не прерывают саму операцию прошивки — см. договорённость
|
||||||
|
по отмене (Фаза 2, вопрос 2): прошивка должна докрутиться до конца
|
||||||
|
независимо от состояния UI.
|
||||||
"""
|
"""
|
||||||
Проверить наличие USB-устройства по VID/PID (синхронно).
|
if progress_cb is None:
|
||||||
|
return None
|
||||||
|
|
||||||
Два метода детекта:
|
def _sync_cb(progress: FlashProgress) -> None:
|
||||||
1. pyusb (usb.core) — видит все USB-устройства включая SDP bulk/HID.
|
future = asyncio.run_coroutine_threadsafe(progress_cb(progress), loop)
|
||||||
На macOS SDP-устройство (1FC9:0130) не создаёт serial-порт
|
try:
|
||||||
и невидимо через serial.tools.list_ports.
|
future.result()
|
||||||
2. serial.tools.list_ports — fallback для CDC ACM устройств.
|
except Exception:
|
||||||
"""
|
logger.exception("progress_cb выбросил исключение из TUI-потока")
|
||||||
# Метод 1: pyusb — работает для SDP и CDC
|
|
||||||
try:
|
|
||||||
import usb.core
|
|
||||||
|
|
||||||
dev = usb.core.find(idVendor=vid, idProduct=pid)
|
return _sync_cb
|
||||||
if dev is not None:
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Метод 2: serial list_ports — fallback для CDC ACM
|
|
||||||
try:
|
|
||||||
import serial.tools.list_ports
|
|
||||||
|
|
||||||
for info in serial.tools.list_ports.comports():
|
|
||||||
if info.vid == vid and info.pid == pid:
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Flasher:
|
class Flasher:
|
||||||
"""
|
"""
|
||||||
Async-обёртка над flash_usb.py.
|
Async-обёртка над app/flash_backend.py.
|
||||||
|
|
||||||
Пример::
|
Пример::
|
||||||
|
|
||||||
flasher = Flasher()
|
flasher = Flasher()
|
||||||
await flasher.flash(
|
|
||||||
target=FlashTarget.FIRMWARE_TEST,
|
async def on_progress(p: FlashProgress) -> None:
|
||||||
progress_cb=lambda p: print(p.message),
|
print(p.message)
|
||||||
)
|
|
||||||
|
await flasher.flash(target=FlashTarget.FIRMWARE_TEST, progress_cb=on_progress)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
# ── Detection (делегируется в flash_backend, см. Р7) ──────────────────
|
||||||
self._proc: Optional[asyncio.subprocess.Process] = None
|
|
||||||
|
|
||||||
# ── Detection ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def detect_sdp() -> bool:
|
def detect_sdp() -> bool:
|
||||||
"""True если виден BootROM SDP (1FC9:0130)."""
|
"""True если виден BootROM SDP."""
|
||||||
return _detect_usb(_BOOTROM_VID, _BOOTROM_PID)
|
return flash_backend.detect_sdp()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def detect_cdc() -> bool:
|
def detect_cdc() -> bool:
|
||||||
"""True если виден CDC firmware_test (1996:00AD)."""
|
"""True если виден CDC firmware_test."""
|
||||||
return _detect_usb(_CDC_VID, _CDC_PID)
|
return flash_backend.detect_cdc()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_custom_binaries() -> list[Path]:
|
def list_custom_binaries() -> list[Path]:
|
||||||
"""Отсканировать custom_binaries/ на *.bin, отсортировано по имени."""
|
"""Отсканировать custom_binaries/ на *.bin, отсортировано по имени."""
|
||||||
return sorted(CUSTOM_BINARIES_DIR.glob("*.bin"))
|
return sorted(CUSTOM_BINARIES_DIR.glob("*.bin"))
|
||||||
|
|
||||||
# ── Erase ────────────────────────────────────────────────────────────────
|
# ── Общий раннер: flash_backend-операция в потоке + перевод ошибок ────
|
||||||
|
|
||||||
|
async def _run_flash_op(
|
||||||
|
self,
|
||||||
|
func: Callable[..., None],
|
||||||
|
*args: object,
|
||||||
|
async_progress_cb: Optional[ProgressCallback],
|
||||||
|
sync_progress_cb: Optional[flash_backend.ProgressCallback],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
"""Выполнить flash_backend.flash()/erase_chip() в потоке.
|
||||||
|
|
||||||
|
flash_backend поднимает FlashBackendError вместо возврата False —
|
||||||
|
здесь это конвертируется обратно в контракт Flasher (bool + событие
|
||||||
|
phase="error"), который был у subprocess-версии (ненулевой returncode).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(func, *args, progress_cb=sync_progress_cb, **kwargs)
|
||||||
|
return True
|
||||||
|
except flash_backend.FlashBackendError as exc:
|
||||||
|
logger.error("%s: %s", getattr(func, "__name__", func), exc)
|
||||||
|
if async_progress_cb is not None:
|
||||||
|
await async_progress_cb(
|
||||||
|
FlashProgress(phase="error", percent=0, message=str(exc))
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── Erase ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def erase_chip(
|
async def erase_chip(
|
||||||
self,
|
self,
|
||||||
progress_cb: Optional[ProgressCallback] = None,
|
progress_cb: Optional[ProgressCallback] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Chip erase Flash через USB SDP (flash_usb.py --erase-chip).
|
Chip erase Flash через USB SDP (flash_backend.erase_chip).
|
||||||
Занимает ~30 с для W25Q128. FCB будет стёрт.
|
Занимает ~30 с для W25Q128. FCB будет стёрт.
|
||||||
|
|
||||||
:return: True при успехе.
|
:return: True при успехе.
|
||||||
"""
|
"""
|
||||||
cmd = [
|
loop = asyncio.get_running_loop()
|
||||||
"uv",
|
sync_cb = _make_sync_progress_cb(progress_cb, loop)
|
||||||
"run",
|
return await self._run_flash_op(
|
||||||
"--directory",
|
flash_backend.erase_chip,
|
||||||
str(_HOST_TOOLS_DIR),
|
async_progress_cb=progress_cb,
|
||||||
"python",
|
sync_progress_cb=sync_cb,
|
||||||
str(_FLASH_USB_SCRIPT),
|
)
|
||||||
"--erase-chip",
|
|
||||||
]
|
|
||||||
return await self._run_cmd(cmd, "erase", progress_cb)
|
|
||||||
|
|
||||||
# ── Flash ────────────────────────────────────────────────────────────────
|
# ── Flash ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def flash(
|
async def flash(
|
||||||
self,
|
self,
|
||||||
|
|
@ -174,257 +200,126 @@ class Flasher:
|
||||||
fcb_variant: FcbVariant = FcbVariant.W25Q128,
|
fcb_variant: FcbVariant = FcbVariant.W25Q128,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Запустить прошивку через flash_usb.py.
|
Запустить прошивку через flash_backend.
|
||||||
|
|
||||||
:param target: Что прошиваем (firmware_test, production или custom).
|
:param target: Что прошиваем (firmware_test, production или custom).
|
||||||
:param progress_cb: Async callback с FlashProgress (может быть None).
|
:param progress_cb: Async callback с FlashProgress (может быть None).
|
||||||
:param bin_path: Путь к бинарю (обязателен для CUSTOM).
|
:param bin_path: Путь к бинарю (обязателен для CUSTOM; для
|
||||||
|
FIRMWARE_TEST — опционален, уже готовый HAB-образ
|
||||||
|
вместо штатной сборки из BUILD_DIR).
|
||||||
:param use_dcd: Только для CUSTOM — включить DCDFilePath (SDRAM-init)
|
:param use_dcd: Только для CUSTOM — включить DCDFilePath (SDRAM-init)
|
||||||
при сборке HAB-образа через nxpimage.
|
при сборке HAB-образа через HabImage.
|
||||||
:param fcb_variant: Только для CUSTOM — какой явный FCB-блоб (dcd/*_fdcb.bin)
|
:param fcb_variant: Только для CUSTOM — какой явный FCB-блоб (dcd/*_fdcb.bin)
|
||||||
записать в Flash[0x60000000] вместо auto-config.
|
записать в Flash[0x60000000] вместо auto-config.
|
||||||
:return: True при успехе.
|
:return: True при успехе.
|
||||||
"""
|
"""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
sync_cb = _make_sync_progress_cb(progress_cb, loop)
|
||||||
|
|
||||||
if target == FlashTarget.FIRMWARE_TEST:
|
if target == FlashTarget.FIRMWARE_TEST:
|
||||||
if bin_path is not None:
|
hab_bin = (
|
||||||
return await self._run_flash_bin(bin_path, progress_cb)
|
bin_path.resolve()
|
||||||
return await self._run_flash("firmware_test", progress_cb)
|
if bin_path is not None
|
||||||
|
else _firmware_hab_path("firmware_test")
|
||||||
|
)
|
||||||
|
return await self._run_flash_op(
|
||||||
|
flash_backend.flash,
|
||||||
|
hab_bin,
|
||||||
|
async_progress_cb=progress_cb,
|
||||||
|
sync_progress_cb=sync_cb,
|
||||||
|
)
|
||||||
|
|
||||||
elif target == FlashTarget.PRODUCTION:
|
elif target == FlashTarget.PRODUCTION:
|
||||||
ok = await self._run_flash("bootloader", progress_cb)
|
ok = await self._run_flash_op(
|
||||||
|
flash_backend.flash,
|
||||||
|
_firmware_hab_path("bootloader"),
|
||||||
|
async_progress_cb=progress_cb,
|
||||||
|
sync_progress_cb=sync_cb,
|
||||||
|
)
|
||||||
if ok:
|
if ok:
|
||||||
ok = await self._run_flash("app", progress_cb)
|
ok = await self._run_flash_op(
|
||||||
|
flash_backend.flash,
|
||||||
|
_firmware_hab_path("app"),
|
||||||
|
async_progress_cb=progress_cb,
|
||||||
|
sync_progress_cb=sync_cb,
|
||||||
|
)
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
elif target == FlashTarget.CUSTOM:
|
elif target == FlashTarget.CUSTOM:
|
||||||
if bin_path is None:
|
if bin_path is None:
|
||||||
raise ValueError("FlashTarget.CUSTOM требует bin_path")
|
raise ValueError("FlashTarget.CUSTOM требует bin_path")
|
||||||
return await self._run_flash_custom(
|
return await self._flash_custom(
|
||||||
bin_path, use_dcd, fcb_variant, progress_cb
|
bin_path, use_dcd, fcb_variant, progress_cb, sync_cb
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _run_flash_custom(
|
async def _flash_custom(
|
||||||
self,
|
self,
|
||||||
raw_bin_path: Path,
|
raw_bin_path: Path,
|
||||||
use_dcd: bool,
|
use_dcd: bool,
|
||||||
fcb_variant: FcbVariant,
|
fcb_variant: FcbVariant,
|
||||||
progress_cb: Optional[ProgressCallback],
|
progress_cb: Optional[ProgressCallback],
|
||||||
|
sync_cb: Optional[flash_backend.ProgressCallback],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Прошить «сырой» (не-HAB) кастомный бинарник из custom_binaries/.
|
Прошить «сырой» (не-HAB) кастомный бинарник из custom_binaries/.
|
||||||
|
|
||||||
Два шага:
|
Два шага:
|
||||||
1. Собрать HAB-образ (IVT + опционально DCD, БЕЗ FCB) через
|
1. Собрать HAB-образ (IVT + опционально DCD, БЕЗ FCB) через
|
||||||
nxpimage — так же, как just build::hab-* собирает штатные
|
flash_backend.build_custom_hab() (HabImage, см. Фазу 0) — сам
|
||||||
прошивки, только конфиг генерируется на лету под выбранный файл.
|
эмитит phase="hab_build" 0%/100% через sync_cb, отдельно
|
||||||
2. Прошить получившийся HAB-образ через flash_usb.py --bin-path,
|
дублировать здесь не нужно.
|
||||||
подставив явный FCB-блоб (--fcb-path) под выбранный тип памяти —
|
2. Прошить получившийся HAB-образ через flash_backend.flash(),
|
||||||
см. FcbVariant.
|
подставив явный FCB-блоб под выбранный тип памяти (FcbVariant).
|
||||||
"""
|
|
||||||
if progress_cb is not None:
|
|
||||||
await progress_cb(
|
|
||||||
FlashProgress(
|
|
||||||
phase="nxpimage", percent=0, message="Сборка HAB-образа..."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
hab_bin = await self._build_custom_hab(raw_bin_path, use_dcd, progress_cb)
|
Временная директория build_custom_hab() (tempfile.mkdtemp) целиком
|
||||||
|
удаляется в finally — не только *.bin, как было в subprocess-версии.
|
||||||
|
"""
|
||||||
|
hab_bin = await self._build_custom_hab(
|
||||||
|
raw_bin_path, use_dcd, progress_cb, sync_cb
|
||||||
|
)
|
||||||
if hab_bin is None:
|
if hab_bin is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
fcb_path = flash_backend.fcb_blob_path(fcb_variant.fcb_filename)
|
||||||
|
try:
|
||||||
|
return await self._run_flash_op(
|
||||||
|
flash_backend.flash,
|
||||||
|
hab_bin,
|
||||||
|
fcb_path=fcb_path,
|
||||||
|
async_progress_cb=progress_cb,
|
||||||
|
sync_progress_cb=sync_cb,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(hab_bin.parent, ignore_errors=True)
|
||||||
|
|
||||||
|
async def _build_custom_hab(
|
||||||
|
self,
|
||||||
|
raw_bin_path: Path,
|
||||||
|
use_dcd: bool,
|
||||||
|
progress_cb: Optional[ProgressCallback],
|
||||||
|
sync_cb: Optional[flash_backend.ProgressCallback],
|
||||||
|
) -> Optional[Path]:
|
||||||
|
"""Обёртка над flash_backend.build_custom_hab() в потоке.
|
||||||
|
|
||||||
|
:return: путь к собранному *.hab.bin, либо None при ошибке.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
flash_backend.build_custom_hab,
|
||||||
|
raw_bin_path,
|
||||||
|
use_dcd,
|
||||||
|
progress_cb=sync_cb,
|
||||||
|
)
|
||||||
|
except flash_backend.FlashBackendError as exc:
|
||||||
|
logger.error("build_custom_hab: %s", exc)
|
||||||
if progress_cb is not None:
|
if progress_cb is not None:
|
||||||
await progress_cb(
|
await progress_cb(
|
||||||
FlashProgress(
|
FlashProgress(
|
||||||
phase="error",
|
phase="error",
|
||||||
percent=0,
|
percent=0,
|
||||||
message="Ошибка сборки HAB-образа (nxpimage)",
|
message=f"Ошибка сборки HAB-образа: {exc}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
return None
|
||||||
|
|
||||||
fcb_path = _DCD_DIR / fcb_variant.fcb_filename
|
|
||||||
try:
|
|
||||||
cmd = [
|
|
||||||
"uv",
|
|
||||||
"run",
|
|
||||||
"--directory",
|
|
||||||
str(_HOST_TOOLS_DIR),
|
|
||||||
"python",
|
|
||||||
str(_FLASH_USB_SCRIPT),
|
|
||||||
"--bin-path",
|
|
||||||
str(hab_bin),
|
|
||||||
"--fcb-path",
|
|
||||||
str(fcb_path),
|
|
||||||
]
|
|
||||||
return await self._run_cmd(cmd, raw_bin_path.stem, progress_cb)
|
|
||||||
finally:
|
|
||||||
hab_bin.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
async def _build_custom_hab(
|
|
||||||
self,
|
|
||||||
raw_bin: Path,
|
|
||||||
use_dcd: bool,
|
|
||||||
progress_cb: Optional[ProgressCallback] = None,
|
|
||||||
) -> Optional[Path]:
|
|
||||||
"""
|
|
||||||
Собрать HAB-образ из сырого бинарника через nxpimage.
|
|
||||||
|
|
||||||
Временный YAML пишется прямо в tools/host/hab/ (как и штатные
|
|
||||||
hab_*.yaml) и nxpimage запускается с cwd=tools/host/hab/ — это
|
|
||||||
обязательно: относительный DCDFilePath ("../dcd/dcd.bin") в
|
|
||||||
существующих конфигах резолвится именно так (см. build.just,
|
|
||||||
группа hab_image_gen — `cd tools/host/hab && uv run nxpimage ...`).
|
|
||||||
Отходить от этой схемы рискованно — nxpimage не документирует
|
|
||||||
явно, от чего резолвит относительные пути.
|
|
||||||
|
|
||||||
:return: путь к собранному *.hab.bin, либо None при ошибке nxpimage.
|
|
||||||
"""
|
|
||||||
yaml_f = tempfile.NamedTemporaryFile(
|
|
||||||
dir=_HAB_DIR, suffix=".yaml", prefix="_tui_custom_", delete=False
|
|
||||||
)
|
|
||||||
yaml_path = Path(yaml_f.name)
|
|
||||||
out_path = yaml_path.with_suffix(".hab.bin")
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
"options:",
|
|
||||||
" flags: 0x00",
|
|
||||||
" startAddress: 0x60000000",
|
|
||||||
" ivtOffset: 0x1000",
|
|
||||||
" initialLoadSize: 0x2000",
|
|
||||||
" family: mimxrt1050",
|
|
||||||
]
|
|
||||||
if use_dcd:
|
|
||||||
lines.append(" DCDFilePath: ../dcd/dcd.bin")
|
|
||||||
lines.append(f'inputImageFile: "{raw_bin.resolve()}"')
|
|
||||||
lines.append("sections: []")
|
|
||||||
|
|
||||||
try:
|
|
||||||
yaml_f.write("\n".join(lines).encode("utf-8"))
|
|
||||||
yaml_f.close()
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
"uv",
|
|
||||||
"run",
|
|
||||||
"nxpimage",
|
|
||||||
"hab",
|
|
||||||
"export",
|
|
||||||
"--force",
|
|
||||||
"-c",
|
|
||||||
str(yaml_path),
|
|
||||||
"-o",
|
|
||||||
str(out_path),
|
|
||||||
]
|
|
||||||
logger.info("Building custom HAB image: %s", " ".join(cmd))
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
|
||||||
cwd=str(_HAB_DIR),
|
|
||||||
)
|
|
||||||
assert proc.stdout is not None
|
|
||||||
async for raw_line in proc.stdout:
|
|
||||||
line = raw_line.decode("utf-8", errors="replace").rstrip()
|
|
||||||
logger.debug("nxpimage: %s", line)
|
|
||||||
if progress_cb is not None and line:
|
|
||||||
await progress_cb(
|
|
||||||
FlashProgress(phase="nxpimage", percent=0, message=line)
|
|
||||||
)
|
|
||||||
await proc.wait()
|
|
||||||
|
|
||||||
if proc.returncode != 0 or not out_path.exists():
|
|
||||||
logger.error("nxpimage hab export failed (rc=%s)", proc.returncode)
|
|
||||||
return None
|
|
||||||
return out_path
|
|
||||||
finally:
|
|
||||||
yaml_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
async def _run_flash(
|
|
||||||
self,
|
|
||||||
firmware: str,
|
|
||||||
progress_cb: Optional[ProgressCallback],
|
|
||||||
) -> bool:
|
|
||||||
"""Запустить flash_usb.py для одного бинаря (стандартный firmware)."""
|
|
||||||
cmd = [
|
|
||||||
"uv",
|
|
||||||
"run",
|
|
||||||
"--directory",
|
|
||||||
str(_HOST_TOOLS_DIR),
|
|
||||||
"python",
|
|
||||||
str(_FLASH_USB_SCRIPT),
|
|
||||||
"--firmware",
|
|
||||||
firmware,
|
|
||||||
"--build-type",
|
|
||||||
_FIRMWARE_BUILD_TYPE,
|
|
||||||
]
|
|
||||||
return await self._run_cmd(cmd, firmware, progress_cb)
|
|
||||||
|
|
||||||
async def _run_flash_bin(
|
|
||||||
self,
|
|
||||||
bin_path: Path,
|
|
||||||
progress_cb: Optional[ProgressCallback],
|
|
||||||
) -> bool:
|
|
||||||
"""Запустить flash_usb.py для произвольного бинаря."""
|
|
||||||
cmd = [
|
|
||||||
"uv",
|
|
||||||
"run",
|
|
||||||
"--directory",
|
|
||||||
str(_HOST_TOOLS_DIR),
|
|
||||||
"python",
|
|
||||||
str(_FLASH_USB_SCRIPT),
|
|
||||||
"--bin-path",
|
|
||||||
str(bin_path.resolve()),
|
|
||||||
]
|
|
||||||
return await self._run_cmd(cmd, bin_path.stem, progress_cb)
|
|
||||||
|
|
||||||
async def _run_cmd(
|
|
||||||
self,
|
|
||||||
cmd: list[str],
|
|
||||||
label: str,
|
|
||||||
progress_cb: Optional[ProgressCallback],
|
|
||||||
) -> bool:
|
|
||||||
"""Общий subprocess runner для всех операций flash_usb.py."""
|
|
||||||
logger.info("Running: %s", " ".join(cmd))
|
|
||||||
try:
|
|
||||||
self._proc = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
|
||||||
cwd=str(_HOST_TOOLS_DIR),
|
|
||||||
)
|
|
||||||
assert self._proc.stdout is not None
|
|
||||||
async for raw_line in self._proc.stdout:
|
|
||||||
line = raw_line.decode("utf-8", errors="replace").rstrip()
|
|
||||||
logger.debug("flash_usb [%s]: %s", label, line)
|
|
||||||
if progress_cb is not None:
|
|
||||||
progress = _parse_progress(line)
|
|
||||||
if progress is not None:
|
|
||||||
await progress_cb(progress)
|
|
||||||
await self._proc.wait()
|
|
||||||
success = self._proc.returncode == 0
|
|
||||||
if progress_cb is not None:
|
|
||||||
phase = "done" if success else "error"
|
|
||||||
msg = "Завершено" if success else "Ошибка"
|
|
||||||
await progress_cb(FlashProgress(phase=phase, percent=100, message=msg))
|
|
||||||
return success
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error("Subprocess error [%s]: %s", label, exc)
|
|
||||||
if progress_cb is not None:
|
|
||||||
await progress_cb(
|
|
||||||
FlashProgress(phase="error", percent=0, message=str(exc))
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
self._proc = None
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_progress(line: str) -> Optional[FlashProgress]:
|
|
||||||
"""
|
|
||||||
Извлечь прогресс из строки stdout flash_usb.py.
|
|
||||||
Возвращает None если строка не несёт прогресс-информации.
|
|
||||||
"""
|
|
||||||
percent_m = _RE_PERCENT.search(line)
|
|
||||||
phase_m = _RE_PHASE.search(line)
|
|
||||||
|
|
||||||
if percent_m is None and phase_m is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
percent = int(percent_m.group(1)) if percent_m else 0
|
|
||||||
phase = phase_m.group(1).lower() if phase_m else "flash"
|
|
||||||
return FlashProgress(phase=phase, percent=percent, message=line.strip())
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue