From b55ec8532c091c1407440b7a62b9659dfc7a549f Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Thu, 2 Jul 2026 18:03:14 +0300 Subject: [PATCH] # Phase 1 - MacOS tested --- tools/production/app/firmware_test_hab.bin | Bin 0 -> 136024 bytes tools/production/app/flash_backend.py | 378 ++++++++++++++++ tools/production/app/usb_ports.py | 85 ++++ tools/production/spike/output_macos.md | 90 ++++ .../production/spike/output_windows.md | 0 tools/production/tests/__init__.py | 0 tools/production/tests/test_flash_backend.py | 423 ++++++++++++++++++ 7 files changed, 976 insertions(+) create mode 100644 tools/production/app/firmware_test_hab.bin create mode 100644 tools/production/app/flash_backend.py create mode 100644 tools/production/app/usb_ports.py create mode 100644 tools/production/spike/output_macos.md rename output_windows.md => tools/production/spike/output_windows.md (100%) create mode 100644 tools/production/tests/__init__.py create mode 100644 tools/production/tests/test_flash_backend.py diff --git a/tools/production/app/firmware_test_hab.bin b/tools/production/app/firmware_test_hab.bin new file mode 100644 index 0000000000000000000000000000000000000000..1090ebc4927fdf240490490cab97cc5d4d08dc30 GIT binary patch literal 136024 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 Path: + """Путь к готовому FCB-блобу (tools/host/dcd/w25q128_fdcb.bin и т.п.).""" + return _HOST_DCD_DIR / fcb_filename + + +# ─── USB VID:PID (те же переменные окружения, что уже приняты в проекте) ── + +_BOOTROM_VID = int(os.environ.get("BOOTROM_VID", "0x1fc9"), 16) +_BOOTROM_PID = int(os.environ.get("BOOTROM_PID", "0x0130"), 16) +_FLASHLOADER_VID = int(os.environ.get("FLASHLOADER_VID", "0x15a2"), 16) +_FLASHLOADER_PID = int(os.environ.get("FLASHLOADER_PID", "0x0073"), 16) +_CDC_VID = int(os.environ.get("SERVICE_CDC_VID", "0x1996"), 16) +_CDC_PID = int(os.environ.get("SERVICE_CDC_PID", "0x00ad"), 16) + +_SDP_DEVICE_ID = f"0x{_BOOTROM_VID:04x}:0x{_BOOTROM_PID:04x}" +_FLASHLOADER_DEVICE_ID = f"0x{_FLASHLOADER_VID:04x}:0x{_FLASHLOADER_PID:04x}" + +# ─── Аппаратные константы (см. flash_usb.py — значения не менялись) ────── + +FLASH_BASE = 0x60000000 +HAB_OFFSET = 0x1000 +FLASHLOADER_LOAD_ADDR = 0x20001C00 + +FLEXSPI_OPTION_ADDR = 0x2000 +FLEXSPI_OPTION_VALUE = 0xC0000007 +FLEXSPI_FCB_VALUE = 0xF000000F # tag=0xF → Write FCB command (auto-config) +FLEXSPI_MEMORY_ID = 9 + +ERASE_ALL_TIMEOUT_MS = 200_000 # эквивалент blhost -t 200000 (см. Фазу 0, ⚠В2) +FLASHLOADER_WAIT_TIMEOUT_S = 10.0 + +_HAB_OPTIONS_TEMPLATE = [ + "options:", + " flags: 0x00", + " startAddress: 0x60000000", + " ivtOffset: 0x1000", + " initialLoadSize: 0x2000", + " family: mimxrt1050", +] + + +# ─── Исключения ──────────────────────────────────────────────────────────── + + +class FlashBackendError(Exception): + """Базовая ошибка flash_backend.""" + + +class DeviceNotFoundError(FlashBackendError): + """SDP-устройство не найдено при попытке загрузить Flashloader.""" + + +class FlashLoaderTimeoutError(FlashBackendError): + """Flashloader не ответил за FLASHLOADER_WAIT_TIMEOUT_S после jump_and_run.""" + + +class HabBuildError(FlashBackendError): + """Ошибка сборки HAB-образа через HabImage (см. build_custom_hab).""" + + +# ─── Detection (Р7 — spsdk API вместо pyusb) ─────────────────────────────── + + +def detect_sdp() -> bool: + """True если виден BootROM SDP (см. _SDP_DEVICE_ID).""" + return len(SdpUSBInterface.scan(device_id=_SDP_DEVICE_ID)) > 0 + + +def detect_cdc() -> bool: + """True если виден CDC firmware_test (список портов, см. usb_ports.py).""" + return resolve_serial_port(UsbId(_CDC_VID, _CDC_PID)) is not None + + +def _emit( + progress_cb: Optional[ProgressCallback], phase: str, percent: int, message: str +) -> None: + if progress_cb is not None: + progress_cb(FlashProgress(phase=phase, percent=percent, message=message)) + + +# ─── Flashloader bring-up ─────────────────────────────────────────────────── + + +def wait_for_flashloader( + timeout_s: float = FLASHLOADER_WAIT_TIMEOUT_S, +) -> MbootUSBInterface: + """Опрашивает MbootUSBInterface.scan() пока Flashloader не поднимется. + + :raises FlashLoaderTimeoutError: если не ответил за timeout_s. + """ + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + found = MbootUSBInterface.scan(device_id=_FLASHLOADER_DEVICE_ID) + if found: + return found[0] + time.sleep(0.5) + raise FlashLoaderTimeoutError( + f"Flashloader не ответил за {timeout_s:.0f}с. Проверь BOOT_MOD пины и подключение." + ) + + +def load_flashloader( + progress_cb: Optional[ProgressCallback] = None, +) -> MbootUSBInterface: + """Загружает Flashloader через SDP, если ещё не запущен. + + :raises DeviceNotFoundError: SDP-устройство не найдено (и Flashloader тоже не поднят). + :raises FlashLoaderTimeoutError: см. wait_for_flashloader(). + """ + already = MbootUSBInterface.scan(device_id=_FLASHLOADER_DEVICE_ID) + if already: + logger.info("Flashloader уже запущен — пропускаем загрузку") + return already[0] + + sdp_devices = SdpUSBInterface.scan(device_id=_SDP_DEVICE_ID) + if not sdp_devices: + raise DeviceNotFoundError( + f"SDP-устройство не найдено ({_SDP_DEVICE_ID}). Плата в BootROM-режиме?" + ) + if not FLASHLOADER_BIN.exists(): + raise FlashBackendError(f"Не найден: {FLASHLOADER_BIN}") + + _emit( + progress_cb, + "load_flashloader", + 0, + f"Загрузка Flashloader через SDP ({_SDP_DEVICE_ID})", + ) + data = FLASHLOADER_BIN.read_bytes() + with SDP(sdp_devices[0]) as sdp: + sdp.write_file(FLASHLOADER_LOAD_ADDR, data) + sdp.jump_and_run(FLASHLOADER_LOAD_ADDR) + + iface = wait_for_flashloader() + _emit(progress_cb, "load_flashloader", 100, "Flashloader готов") + return iface + + +# ─── FlexSPI / FCB ─────────────────────────────────────────────────────── + + +def configure_flexspi(mboot: McuBoot) -> None: + """Инициализирует FlexSPI NOR контроллер (см. flash_usb.py::configure_flexspi).""" + mboot.fill_memory(FLEXSPI_OPTION_ADDR, 4, FLEXSPI_OPTION_VALUE) + ok = mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID) + if not ok: + raise FlashBackendError("configure_memory (FlexSPI init) вернул False") + + +def write_fcb_auto(mboot: McuBoot) -> None: + """Auto-config FCB через magic option word (см. flash_usb.py::write_fcb). + + Надёжно проверен только для W25Q128 — см. docstring оригинала. + """ + mboot.fill_memory(FLEXSPI_OPTION_ADDR, 4, FLEXSPI_FCB_VALUE) + ok = mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID) + if not ok: + raise FlashBackendError("configure_memory (FCB write) вернул False") + + +def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None: + """Пишет буквальный FCB-блоб (512 байт) в Flash[FLASH_BASE] (custom-бинари). + + См. flash_usb.py::write_fcb_explicit — nxpimage не кладёт FCB в HAB-образ, + поэтому для произвольных чипов нужен явный блоб под конкретный memory chip. + """ + if not fcb_path.exists(): + raise FlashBackendError(f"FCB-файл не найден: {fcb_path}") + data = fcb_path.read_bytes() + ok = mboot.write_memory(FLASH_BASE, data, mem_id=0) + if not ok: + raise FlashBackendError(f"write_memory(FCB {fcb_path.name}) вернул False") + + +# ─── Прошивка / RAM-load / erase ──────────────────────────────────────────── + + +def flash( + hab_bin: Path, + *, + ram_only: bool = False, + fcb_path: Optional[Path] = None, + progress_cb: Optional[ProgressCallback] = None, +) -> None: + """Прошить HAB-образ в Flash либо загрузить в RAM (см. flash_usb.py::flash). + + :param hab_bin: Готовый HAB-образ (.bin). Для custom-бинарей — см. + build_custom_hab(). + :param ram_only: Загрузить в RAM через SDP, во Flash не писать. + :param fcb_path: Явный FCB-блоб (custom-бинари). None → auto-config + (write_fcb_auto, только для штатных firmware_test/production). + :raises FlashBackendError: и подклассы — на любой ошибке. + """ + if not hab_bin.exists(): + raise FlashBackendError(f"Файл не найден: {hab_bin}") + + if ram_only: + _emit(progress_cb, "load_flashloader", 0, f"Загрузка в RAM: {hab_bin.name}") + sdp_devices = SdpUSBInterface.scan(device_id=_SDP_DEVICE_ID) + if not sdp_devices: + raise DeviceNotFoundError(f"SDP-устройство не найдено ({_SDP_DEVICE_ID})") + addr = FLASH_BASE + HAB_OFFSET + with SDP(sdp_devices[0]) as sdp: + sdp.write_file(addr, hab_bin.read_bytes()) + sdp.jump_and_run(addr) + _emit(progress_cb, "done", 100, "Загружено в RAM") + return + + iface = load_flashloader(progress_cb) + + write_addr = FLASH_BASE + HAB_OFFSET + hab_size = hab_bin.stat().st_size + erase_size = ((HAB_OFFSET + hab_size + 0xFFF) // 0x1000) * 0x1000 + + with McuBoot(iface) as mboot: + _emit(progress_cb, "configure", 0, "Конфигурация FlexSPI NOR") + configure_flexspi(mboot) + + _emit( + progress_cb, "erase", 0, f"Стирание 0x{FLASH_BASE:08X} + {erase_size} байт" + ) + ok = mboot.flash_erase_region(FLASH_BASE, erase_size, mem_id=0) + if not ok: + raise FlashBackendError("flash_erase_region вернул False") + + _emit(progress_cb, "fcb", 0, "Запись FCB") + if fcb_path is not None: + write_fcb_explicit(mboot, fcb_path) + else: + write_fcb_auto(mboot) + + _emit(progress_cb, "write", 0, f"Запись {hab_bin.name} ({hab_size} байт)") + + def _on_progress(current: int, total: int) -> None: + percent = int(current * 100 / total) if total else 0 + _emit(progress_cb, "write", percent, f"{current}/{total} байт") + + data = hab_bin.read_bytes() + ok = mboot.write_memory( + write_addr, data, mem_id=0, progress_callback=_on_progress + ) + if not ok: + raise FlashBackendError("write_memory (HAB-образ) вернул False") + + _emit(progress_cb, "reset", 0, "Reset") + mboot.reset(reopen=False) + + _emit(progress_cb, "done", 100, "Прошивка завершена успешно") + + +def erase_chip(progress_cb: Optional[ProgressCallback] = None) -> None: + """Полная очистка Flash (см. flash_usb.py::erase_chip). После erase FCB + тоже стёрт — плата не загрузится до следующей прошивки.""" + iface = load_flashloader(progress_cb) + + with McuBoot(iface) as mboot: + _emit(progress_cb, "configure", 0, "Конфигурация FlexSPI NOR") + configure_flexspi(mboot) + + _emit(progress_cb, "erase", 0, "Полная очистка Flash (~30с)") + iface.device.timeout = ERASE_ALL_TIMEOUT_MS # см. Фазу 0, ⚠В2 + ok = mboot.flash_erase_all(mem_id=FLEXSPI_MEMORY_ID) + if not ok: + raise FlashBackendError("flash_erase_all вернул False") + + _emit(progress_cb, "reset", 0, "Reset") + mboot.reset(reopen=False) + + _emit(progress_cb, "done", 100, "Chip erase завершён") + + +# ─── HAB-сборка для custom-бинарей (Фаза 0, ⚠В1 — HabImage вместо nxpimage) ─ + + +def _make_hab_config(input_bin: Path, dcd_bin: Optional[Path], work_dir: Path) -> Path: + """YAML-конфиг nxpimage hab с АБСОЛЮТНЫМИ путями (Р6 — временный файл + может лежать где угодно, не обязательно рядом с реальными hab/*.yaml).""" + lines = list(_HAB_OPTIONS_TEMPLATE) + if dcd_bin is not None: + lines.append(f" DCDFilePath: {dcd_bin.resolve().as_posix()}") + lines.append(f'inputImageFile: "{input_bin.resolve().as_posix()}"') + lines.append("sections: []") + + yaml_path = ( + work_dir / f"hab_{hashlib.sha1(input_bin.name.encode()).hexdigest()[:8]}.yaml" + ) + yaml_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return yaml_path + + +def build_custom_hab( + raw_bin: Path, + use_dcd: bool, + progress_cb: Optional[ProgressCallback] = None, +) -> Path: + """Собрать HAB-образ из сырого бинарника через HabImage (spsdk API). + + Заменяет flasher.py::_build_custom_hab (subprocess nxpimage). Логика + сборки байт-в-байт идентична nxpimage CLI — см. Фазу 0 (golden-тест). + + :return: путь к собранному *.hab.bin во временной директории + (tempfile.gettempdir(), Р6). Вызывающий код отвечает за unlink + после использования — так же, как сейчас делает Flasher. + :raises HabBuildError: при ошибке валидации/сборки. + """ + _emit(progress_cb, "hab_build", 0, "Сборка HAB-образа (HabImage)") + + dcd_bin = REAL_DCD_BIN if use_dcd else None + if use_dcd and not dcd_bin.exists(): + raise HabBuildError(f"DCD запрошен, но файл не найден: {dcd_bin}") + + work_dir = Path(tempfile.mkdtemp(prefix="tui_hab_")) + try: + yaml_path = _make_hab_config(raw_bin, dcd_bin, work_dir) + cfg = Config.create_from_file(str(yaml_path)) + schemas = HabImage.get_validation_schemas_from_cfg(cfg) + cfg.check(schemas, check_unknown_props=True) + hab = HabImage.load_from_config(cfg) + hab.post_export(cfg.config_dir) + data = hab.export() + except HabBuildError: + raise + except Exception as exc: # noqa: BLE001 — оборачиваем весь spsdk-зоопарк исключений + raise HabBuildError(f"Сборка HAB не удалась: {exc}") from exc + + out_path = work_dir / f"{raw_bin.stem}_hab.bin" + out_path.write_bytes(data) + _emit(progress_cb, "hab_build", 100, f"HAB-образ собран: {len(data)} байт") + return out_path diff --git a/tools/production/app/usb_ports.py b/tools/production/app/usb_ports.py new file mode 100644 index 0000000..ed78b4d --- /dev/null +++ b/tools/production/app/usb_ports.py @@ -0,0 +1,85 @@ +""" +usb_ports.py — резолвер serial-портов по VID:PID (Р8). + +Принцип (см. MONOLITH_APP_PLAN.md §Р8): имя порта не переносимо даже в +пределах одной ОС (перевтыкание в другой физический разъём меняет имя: +cu.usbmodemXXXX / COMn / ttyACMn), поэтому единственный стабильный +идентификатор устройства — VID:PID. Порядок резолва: + + 1. Явный override через переменную окружения (escape hatch). + 2. Иначе — поиск по VID:PID среди serial.tools.list_ports.comports(). + 3. Одно совпадение → его имя порта. + 4. Ноль совпадений → None (штатное состояние: устройство не подключено, + для TUI это WaitingScreen, а не ошибка). + 5. Несколько совпадений → первое + warning в лог (дизамбигуация по + serial_number — задел на будущее, см. MONOLITH_APP_PLAN.md §О3). + +Модуль намеренно не хранит собственных VID/PID-констант — они остаются +там же, где были (firmware_client.py, m5_client.py, flash_backend.py), +каждый со своим fallback-дефолтом, как уже принято в проекте. +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass +from typing import Optional + +import serial.tools.list_ports + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class UsbId: + """VID:PID пара — единственный стабильный идентификатор USB-устройства.""" + + vid: int + pid: int + + def __str__(self) -> str: + return f"{self.vid:04X}:{self.pid:04X}" + + +def resolve_serial_port(usb_id: UsbId, env_var: Optional[str] = None) -> Optional[str]: + """ + Найти serial-порт устройства по VID:PID. + + :param usb_id: Идентичность устройства (VID:PID). + :param env_var: Имя переменной окружения-override (escape hatch, п.1 + алгоритма). Если задана и непуста — возвращается + as-is без обращения к list_ports; значение не + валидируется (ответственность вызывающего кода). + :return: Имя порта (например, "/dev/cu.usbmodem1101" или "COM8"), + либо None, если устройство не найдено. + """ + if env_var: + override = os.environ.get(env_var, "").strip() + if override: + logger.debug( + "resolve_serial_port(%s): override из %s = %s", + usb_id, + env_var, + override, + ) + return override + + matches = [ + info.device + for info in serial.tools.list_ports.comports() + if info.vid == usb_id.vid and info.pid == usb_id.pid + ] + + if not matches: + return None + + if len(matches) > 1: + logger.warning( + "Несколько устройств %s: %s — используем первое (%s). ", + usb_id, + matches, + matches[0], + ) + + return matches[0] diff --git a/tools/production/spike/output_macos.md b/tools/production/spike/output_macos.md new file mode 100644 index 0000000..52d4934 --- /dev/null +++ b/tools/production/spike/output_macos.md @@ -0,0 +1,90 @@ +# Windows Host Results + +## Step 1: spike_hab.py + +```bash +production git:(feature-tui-monolith) uv run pytest spike/spike_hab.py -v -s +================================================================================================================ test session starts ================================================================================================================ +platform darwin -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0 -- /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv/bin/python3 +cachedir: .pytest_cache +rootdir: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production +configfile: pyproject.toml +collected 2 items + +spike/spike_hab.py::test_golden_hab_no_dcd PASSED +spike/spike_hab.py::test_golden_hab_with_dcd PASSED + +================================================================================================================= 2 passed in 1.07s ================================================================================================================= + +``` + +## Step 2: spike_flash.py --ports-only + +```bash +# подключены M5StamPLC + плата с firmware_test + +➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py --ports-only +=== Serial-порты (serial.tools.list_ports.comports) === + /dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None + /dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None + /dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000' + /dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ' + /dev/cu.usbmodem11301 VID:PID=1996:00AD 'MU LLC' serial=None + + Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5). +``` + +## Step 3: spike_flash.py + +```bash +# подключены M5StamPLC + плата в режиме SDP +➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py +=== Serial-порты (serial.tools.list_ports.comports) === + /dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None + /dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None + /dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000' + /dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ' + + Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5). + +=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) === + Загрузка Flashloader через SDP (0x1fc9:0x0130) + Ожидание Flashloader (до 10с).... OK + +✅ get_property(CURRENT_VERSION) = [1258424320] + +=== configure_memory (FlexSPI NOR) === +configure_memory: OK + +✅ Гейт 0 (SDP/Flashloader): пройден +``` + +## Step 4: spike_flash.py --erase-all + +```bash +# Подключены M5StamPLC + плата в режиме SDP + +➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py --erase-all +=== Serial-порты (serial.tools.list_ports.comports) === + /dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None + /dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None + /dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000' + /dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ' + + Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5). + +=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) === + Flashloader уже запущен — пропускаем загрузку + +✅ get_property(CURRENT_VERSION) = [1258424320] + +=== configure_memory (FlexSPI NOR) === +configure_memory: OK + +⚠️ Chip erase сотрёт FCB — плата не загрузится до повторной прошивки. +Наберите ERASE для подтверждения: ERASE + Выставляю timeout=200000мс (эквивалент blhost -t 200000) +flash_erase_all: OK + +✅ Гейт 0 (SDP/Flashloader): пройден +``` diff --git a/output_windows.md b/tools/production/spike/output_windows.md similarity index 100% rename from output_windows.md rename to tools/production/spike/output_windows.md diff --git a/tools/production/tests/__init__.py b/tools/production/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/production/tests/test_flash_backend.py b/tools/production/tests/test_flash_backend.py new file mode 100644 index 0000000..5021296 --- /dev/null +++ b/tools/production/tests/test_flash_backend.py @@ -0,0 +1,423 @@ +""" +test_flash_backend.py — юнит-тесты app/flash_backend.py. + +Два уровня: + 1. SDP/McuBoot/detect — моки (unittest.mock через monkeypatch). Железо не + нужно, спсdk-транспорт не трогаем — проверяем только нашу логику + (последовательность вызовов, обработку ошибок, форму FlashProgress). + 2. build_custom_hab() — НЕ мокается. Использует настоящий HabImage на + детерминированном dummy-бинаре, как spike_hab.py в Фазе 0 (там же + доказана byte-exact идентичность nxpimage). Мокать spsdk здесь было бы + ложной уверенностью — сама суть проверки в том, что реальный HabImage + не падает и не расходится по конфигу. +""" + +from __future__ import annotations + +import hashlib +from pathlib import Path +from typing import List +from unittest.mock import MagicMock, Mock + +import pytest + +from app import flash_backend as fb +from app.models import FlashProgress + + +# ─── Общие фикстуры ────────────────────────────────────────────────────── + + +@pytest.fixture +def events() -> List[FlashProgress]: + return [] + + +def _collector(events: List[FlashProgress]): + return events.append + + +def _phases(events: List[FlashProgress]) -> list[str]: + return [e.phase for e in events] + + +# ─── detect_sdp / detect_cdc ──────────────────────────────────────────── + + +def test_detect_sdp_found(monkeypatch): + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[Mock()])) + assert fb.detect_sdp() is True + + +def test_detect_sdp_not_found(monkeypatch): + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[])) + assert fb.detect_sdp() is False + + +def test_detect_cdc_found(monkeypatch): + monkeypatch.setattr( + fb, "resolve_serial_port", Mock(return_value="/dev/cu.usbmodem1") + ) + assert fb.detect_cdc() is True + + +def test_detect_cdc_not_found(monkeypatch): + monkeypatch.setattr(fb, "resolve_serial_port", Mock(return_value=None)) + assert fb.detect_cdc() is False + + +# ─── wait_for_flashloader ──────────────────────────────────────────────── + + +def test_wait_for_flashloader_immediate(monkeypatch): + iface = Mock() + monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[iface])) + assert fb.wait_for_flashloader(timeout_s=1.0) is iface + + +def test_wait_for_flashloader_timeout(monkeypatch): + monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[])) + monkeypatch.setattr(fb.time, "sleep", Mock()) # не ждать реально + with pytest.raises(fb.FlashLoaderTimeoutError): + fb.wait_for_flashloader(timeout_s=0.001) + + +# ─── load_flashloader ──────────────────────────────────────────────────── + + +def test_load_flashloader_already_running(monkeypatch): + iface = Mock() + scan_mboot = Mock(return_value=[iface]) + scan_sdp = Mock() # не должен вызываться + monkeypatch.setattr(fb.MbootUSBInterface, "scan", scan_mboot) + monkeypatch.setattr(fb.SdpUSBInterface, "scan", scan_sdp) + + result = fb.load_flashloader() + + assert result is iface + scan_sdp.assert_not_called() + + +def test_load_flashloader_no_sdp_device(monkeypatch): + monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[])) + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[])) + + with pytest.raises(fb.DeviceNotFoundError): + fb.load_flashloader() + + +def test_load_flashloader_missing_bin(monkeypatch): + monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[])) + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[Mock()])) + monkeypatch.setattr(fb, "FLASHLOADER_BIN", Mock(exists=Mock(return_value=False))) + + with pytest.raises(fb.FlashBackendError, match="Не найден"): + fb.load_flashloader() + + +def test_load_flashloader_happy_path(monkeypatch, events): + sdp_iface = Mock() + flashloader_iface = Mock() + + monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[])) + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[sdp_iface])) + + fake_bin = Mock( + exists=Mock(return_value=True), read_bytes=Mock(return_value=b"\x00" * 16) + ) + monkeypatch.setattr(fb, "FLASHLOADER_BIN", fake_bin) + + mock_sdp_ctx = MagicMock() + mock_sdp_ctx.__enter__.return_value = mock_sdp_ctx + monkeypatch.setattr(fb, "SDP", Mock(return_value=mock_sdp_ctx)) + + monkeypatch.setattr( + fb, "wait_for_flashloader", Mock(return_value=flashloader_iface) + ) + + result = fb.load_flashloader(progress_cb=_collector(events)) + + assert result is flashloader_iface + mock_sdp_ctx.write_file.assert_called_once_with( + fb.FLASHLOADER_LOAD_ADDR, b"\x00" * 16 + ) + mock_sdp_ctx.jump_and_run.assert_called_once_with(fb.FLASHLOADER_LOAD_ADDR) + assert _phases(events) == ["load_flashloader", "load_flashloader"] + assert events[-1].percent == 100 + + +# ─── configure_flexspi / write_fcb_* ───────────────────────────────────── + + +def test_configure_flexspi_ok(): + mboot = Mock(configure_memory=Mock(return_value=True)) + fb.configure_flexspi(mboot) + mboot.fill_memory.assert_called_once_with( + fb.FLEXSPI_OPTION_ADDR, 4, fb.FLEXSPI_OPTION_VALUE + ) + mboot.configure_memory.assert_called_once_with( + fb.FLEXSPI_OPTION_ADDR, fb.FLEXSPI_MEMORY_ID + ) + + +def test_configure_flexspi_fail(): + mboot = Mock(configure_memory=Mock(return_value=False)) + with pytest.raises(fb.FlashBackendError): + fb.configure_flexspi(mboot) + + +def test_write_fcb_auto_uses_magic_word(): + mboot = Mock(configure_memory=Mock(return_value=True)) + fb.write_fcb_auto(mboot) + mboot.fill_memory.assert_called_once_with( + fb.FLEXSPI_OPTION_ADDR, 4, fb.FLEXSPI_FCB_VALUE + ) + + +def test_write_fcb_explicit_missing_file(tmp_path): + mboot = Mock() + with pytest.raises(fb.FlashBackendError, match="не найден"): + fb.write_fcb_explicit(mboot, tmp_path / "nope.bin") + + +def test_write_fcb_explicit_ok(tmp_path): + fcb = tmp_path / "w25q128_fdcb.bin" + fcb.write_bytes(b"\xab" * 512) + mboot = Mock(write_memory=Mock(return_value=True)) + + fb.write_fcb_explicit(mboot, fcb) + + mboot.write_memory.assert_called_once_with(fb.FLASH_BASE, b"\xab" * 512, mem_id=0) + + +def test_write_fcb_explicit_write_fails(tmp_path): + fcb = tmp_path / "w25q128_fdcb.bin" + fcb.write_bytes(b"\xab" * 512) + mboot = Mock(write_memory=Mock(return_value=False)) + + with pytest.raises(fb.FlashBackendError): + fb.write_fcb_explicit(mboot, fcb) + + +# ─── flash() ────────────────────────────────────────────────────────────── + + +def _mock_mcuboot_ctx(monkeypatch, **method_returns): + """Подменяет fb.McuBoot на context-manager мок с заданными return_value + у fill_memory/configure_memory/flash_erase_region/write_memory (все True + по умолчанию, кроме явно переопределённых).""" + defaults = dict( + configure_memory=True, + flash_erase_region=True, + flash_erase_all=True, + write_memory=True, + ) + defaults.update(method_returns) + + ctx = MagicMock() + ctx.__enter__.return_value = ctx + for name, ret in defaults.items(): + getattr(ctx, name).return_value = ret + + monkeypatch.setattr(fb, "McuBoot", Mock(return_value=ctx)) + return ctx + + +def test_flash_missing_file(events, tmp_path): + with pytest.raises(fb.FlashBackendError, match="Файл не найден"): + fb.flash(tmp_path / "nope_hab.bin", progress_cb=_collector(events)) + assert events == [] + + +def test_flash_ram_only(monkeypatch, events, tmp_path): + hab_bin = tmp_path / "fw_hab.bin" + hab_bin.write_bytes(b"\xd1" + b"\x00" * 63) + + sdp_iface = Mock() + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[sdp_iface])) + mcuboot_mock = Mock() + monkeypatch.setattr(fb, "McuBoot", mcuboot_mock) + + mock_sdp_ctx = MagicMock() + mock_sdp_ctx.__enter__.return_value = mock_sdp_ctx + monkeypatch.setattr(fb, "SDP", Mock(return_value=mock_sdp_ctx)) + + fb.flash(hab_bin, ram_only=True, progress_cb=_collector(events)) + + addr = fb.FLASH_BASE + fb.HAB_OFFSET + mock_sdp_ctx.write_file.assert_called_once_with(addr, hab_bin.read_bytes()) + mock_sdp_ctx.jump_and_run.assert_called_once_with(addr) + mcuboot_mock.assert_not_called() + assert _phases(events) == ["load_flashloader", "done"] + + +def test_flash_ram_only_no_sdp(monkeypatch, events, tmp_path): + hab_bin = tmp_path / "fw_hab.bin" + hab_bin.write_bytes(b"\xd1") + monkeypatch.setattr(fb.SdpUSBInterface, "scan", Mock(return_value=[])) + + with pytest.raises(fb.DeviceNotFoundError): + fb.flash(hab_bin, ram_only=True, progress_cb=_collector(events)) + + +def test_flash_happy_path_auto_fcb(monkeypatch, events, tmp_path): + hab_bin = tmp_path / "fw_hab.bin" + hab_bin.write_bytes(b"\xd1" + b"\x00" * 127) + + flashloader_iface = Mock() + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=flashloader_iface)) + ctx = _mock_mcuboot_ctx(monkeypatch) + + fb.flash(hab_bin, progress_cb=_collector(events)) + + ctx.flash_erase_region.assert_called_once() + args, kwargs = ctx.flash_erase_region.call_args + assert args[0] == fb.FLASH_BASE + assert kwargs["mem_id"] == 0 + + ctx.write_memory.assert_called_once() + wargs, wkwargs = ctx.write_memory.call_args + assert wargs[0] == fb.FLASH_BASE + fb.HAB_OFFSET + assert wargs[1] == hab_bin.read_bytes() + assert wkwargs["mem_id"] == 0 + assert "progress_callback" in wkwargs + + ctx.reset.assert_called_once_with(reopen=False) + assert _phases(events) == ["configure", "erase", "fcb", "write", "reset", "done"] + assert events[-1].percent == 100 + + +def test_flash_happy_path_explicit_fcb(monkeypatch, events, tmp_path): + hab_bin = tmp_path / "fw_hab.bin" + hab_bin.write_bytes(b"\xd1" + b"\x00" * 15) + fcb_bin = tmp_path / "w25q512_fdcb.bin" + fcb_bin.write_bytes(b"\xab" * 512) + + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock())) + ctx = _mock_mcuboot_ctx(monkeypatch) + + fb.flash(hab_bin, fcb_path=fcb_bin, progress_cb=_collector(events)) + + # explicit FCB → write_memory вызывается ДВАЖДЫ: FCB-блоб + сам HAB-образ + assert ctx.write_memory.call_count == 2 + first_call_args = ctx.write_memory.call_args_list[0].args + assert first_call_args[0] == fb.FLASH_BASE + assert first_call_args[1] == b"\xab" * 512 + + +def test_flash_write_memory_fails(monkeypatch, events, tmp_path): + hab_bin = tmp_path / "fw_hab.bin" + hab_bin.write_bytes(b"\xd1") + + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock())) + _mock_mcuboot_ctx(monkeypatch, write_memory=False) + + with pytest.raises(fb.FlashBackendError, match="write_memory"): + fb.flash(hab_bin, progress_cb=_collector(events)) + + # reset/done не должны наступить после ошибки записи + assert "reset" not in _phases(events) + assert "done" not in _phases(events) + + +def test_flash_progress_callback_reports_bytes(monkeypatch, events, tmp_path): + """write_memory реально дёргает progress_callback(current, total) — см. + Фазу 0. Проверяем, что наш адаптер конвертирует это в FlashProgress.""" + hab_bin = tmp_path / "fw_hab.bin" + payload = b"\xd1" + b"\x00" * 999 + hab_bin.write_bytes(payload) + + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock())) + ctx = _mock_mcuboot_ctx(monkeypatch) + + def _fake_write_memory(address, data, mem_id=0, progress_callback=None): + progress_callback(len(data) // 2, len(data)) + progress_callback(len(data), len(data)) + return True + + ctx.write_memory.side_effect = _fake_write_memory + + fb.flash(hab_bin, progress_cb=_collector(events)) + + write_events = [e for e in events if e.phase == "write" and "/" in e.message] + assert len(write_events) == 2 + assert write_events[-1].percent == 100 + + +# ─── erase_chip() ───────────────────────────────────────────────────────── + + +def test_erase_chip_happy_path(monkeypatch, events): + flashloader_iface = MagicMock() + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=flashloader_iface)) + ctx = _mock_mcuboot_ctx(monkeypatch) + + fb.erase_chip(progress_cb=_collector(events)) + + assert flashloader_iface.device.timeout == fb.ERASE_ALL_TIMEOUT_MS + ctx.flash_erase_all.assert_called_once_with(mem_id=fb.FLEXSPI_MEMORY_ID) + ctx.reset.assert_called_once_with(reopen=False) + assert _phases(events) == ["configure", "erase", "reset", "done"] + + +def test_erase_chip_fails(monkeypatch, events): + monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=MagicMock())) + _mock_mcuboot_ctx(monkeypatch, flash_erase_all=False) + + with pytest.raises(fb.FlashBackendError, match="flash_erase_all"): + fb.erase_chip(progress_cb=_collector(events)) + + assert "done" not in _phases(events) + + +# ─── build_custom_hab() — НЕ мокается, реальный HabImage ───────────────── + + +def _dummy_bin(tmp_path: Path, size: int = 512) -> Path: + seed = hashlib.sha256(b"test-flash-backend-golden").digest() + data = (seed * (size // len(seed) + 1))[:size] + p = tmp_path / "dummy_app.bin" + p.write_bytes(data) + return p + + +def test_build_custom_hab_no_dcd(tmp_path, events): + raw = _dummy_bin(tmp_path) + out = fb.build_custom_hab(raw, use_dcd=False, progress_cb=_collector(events)) + + assert out.exists() + assert out.stat().st_size > 0 + assert _phases(events) == ["hab_build", "hab_build"] + assert events[-1].percent == 100 + + +def test_build_custom_hab_with_dcd(tmp_path): + if not fb.REAL_DCD_BIN.exists(): + pytest.skip(f"Реальный DCD не найден: {fb.REAL_DCD_BIN}") + raw = _dummy_bin(tmp_path) + out = fb.build_custom_hab(raw, use_dcd=True) + assert out.exists() + assert out.stat().st_size > 0 + + +def test_build_custom_hab_dcd_requested_but_missing(monkeypatch, tmp_path): + raw = _dummy_bin(tmp_path) + monkeypatch.setattr(fb, "REAL_DCD_BIN", tmp_path / "nonexistent_dcd.bin") + + with pytest.raises(fb.HabBuildError, match="DCD"): + fb.build_custom_hab(raw, use_dcd=True) + + +def test_build_custom_hab_missing_input(tmp_path): + with pytest.raises(fb.HabBuildError): + fb.build_custom_hab(tmp_path / "does_not_exist.bin", use_dcd=False) + + +def test_build_custom_hab_bytes_match_golden(tmp_path): + """Дополнительная перекрёстная проверка со spike_hab.py: два независимых + вызова build_custom_hab с одинаковым входом дают одинаковый результат + (детерминированность), а не просто 'не упало'.""" + raw = _dummy_bin(tmp_path) + out1 = fb.build_custom_hab(raw, use_dcd=False) + out2 = fb.build_custom_hab(raw, use_dcd=False) + assert out1.read_bytes() == out2.read_bytes()