# Phase 4: connection loss handling with spsdk - MacOS tested
This commit is contained in:
parent
234f1a60cf
commit
d7dd9aea4a
5 changed files with 634 additions and 22 deletions
|
|
@ -39,6 +39,7 @@ from spsdk.image.hab.hab_image import HabImage
|
|||
from spsdk.mboot import McuBoot, MbootUSBInterface
|
||||
from spsdk.sdp import SDP, SdpUSBInterface
|
||||
from spsdk.utils.config import Config
|
||||
from spsdk.utils.exceptions import SPSDKTimeoutError
|
||||
|
||||
from .models import FlashProgress
|
||||
from .usb_ports import UsbId, resolve_serial_port
|
||||
|
|
@ -155,6 +156,15 @@ class HabBuildError(FlashBackendError):
|
|||
"""Ошибка сборки HAB-образа через HabImage (см. build_custom_hab)."""
|
||||
|
||||
|
||||
# SPSDKTimeoutError НЕ наследует SPSDKConnectionError (оба — потомки SPSDKError,
|
||||
# проверено по исходникам spsdk 3.7.0), поэтому один `except SPSDKConnectionError`
|
||||
# его пропускал → safety net в Flasher показывал «Непредвиденная ошибка» вместо
|
||||
# «Соединение потеряно» (Фаза 4a). Ловим оба явным кортежем в SDP/McuBoot-обёртках:
|
||||
# read-фаза после write может отдать голый таймаут. Именованная константа вместо
|
||||
# 4× инлайн-дублей.
|
||||
_CONNECTION_LOST_EXCEPTIONS = (SPSDKConnectionError, SPSDKTimeoutError)
|
||||
|
||||
|
||||
# ─── Detection (Р7 — spsdk API вместо pyusb) ───────────────────────────────
|
||||
|
||||
|
||||
|
|
@ -168,6 +178,35 @@ def detect_cdc() -> bool:
|
|||
return resolve_serial_port(UsbId(_CDC_VID, _CDC_PID)) is not None
|
||||
|
||||
|
||||
def _sdp_still_present() -> bool:
|
||||
"""Быстрая проверка «плата ещё на шине» для error-путей (вариант B, Р10).
|
||||
|
||||
Любая ошибка самой проверки трактуется как «устройства нет»: проверка
|
||||
выполняется только ПОСЛЕ уже случившегося сбоя команды, шина в этот
|
||||
момент нестабильна, и «не смог проверить» практически всегда означает
|
||||
«плату выдернули» (согласовано, RELEASE_ROADMAP.md §B).
|
||||
"""
|
||||
try:
|
||||
return detect_sdp()
|
||||
except Exception: # noqa: BLE001 — см. docstring: любой сбой ⇒ считаем обрывом
|
||||
return False
|
||||
|
||||
|
||||
def _fail_command(message: str) -> None:
|
||||
"""Живая команда spsdk вернула False — переклассификация по варианту B (Р10).
|
||||
|
||||
Если устройство пропало с шины → обрыв (ConnectionLostError), иначе →
|
||||
честная ошибка операции (FlashBackendError с прежним текстом). detect_sdp()
|
||||
выполняется ТОЛЬКО здесь, в error-пути; happy path не затрагивается.
|
||||
|
||||
:raises ConnectionLostError: устройство исчезло с шины после сбоя команды.
|
||||
:raises FlashBackendError: устройство на месте — ошибка самой операции.
|
||||
"""
|
||||
if not _sdp_still_present():
|
||||
raise ConnectionLostError(f"{message} (устройство пропало с шины)")
|
||||
raise FlashBackendError(message)
|
||||
|
||||
|
||||
def _emit(
|
||||
progress_cb: Optional[ProgressCallback], phase: str, percent: int, message: str
|
||||
) -> None:
|
||||
|
|
@ -244,7 +283,7 @@ def load_flashloader(
|
|||
with SDP(sdp_devices[0]) as sdp:
|
||||
sdp.write_file(FLASHLOADER_LOAD_ADDR, data)
|
||||
sdp.jump_and_run(FLASHLOADER_LOAD_ADDR)
|
||||
except SPSDKConnectionError as exc:
|
||||
except _CONNECTION_LOST_EXCEPTIONS as exc:
|
||||
raise ConnectionLostError(
|
||||
f"USB-соединение потеряно при загрузке Flashloader: {exc}"
|
||||
) from exc
|
||||
|
|
@ -262,7 +301,7 @@ def configure_flexspi(mboot: McuBoot) -> None:
|
|||
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")
|
||||
_fail_command("configure_memory (FlexSPI init) вернул False")
|
||||
|
||||
|
||||
def write_fcb_auto(mboot: McuBoot) -> None:
|
||||
|
|
@ -273,7 +312,7 @@ def write_fcb_auto(mboot: McuBoot) -> None:
|
|||
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")
|
||||
_fail_command("configure_memory (FCB write) вернул False")
|
||||
|
||||
|
||||
def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
||||
|
|
@ -287,7 +326,7 @@ def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
|||
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")
|
||||
_fail_command(f"write_memory(FCB {fcb_path.name}) вернул False")
|
||||
|
||||
|
||||
# ─── Прошивка / RAM-load / erase ────────────────────────────────────────────
|
||||
|
|
@ -323,7 +362,7 @@ def flash(
|
|||
with SDP(sdp_devices[0]) as sdp:
|
||||
sdp.write_file(addr, hab_bin.read_bytes())
|
||||
sdp.jump_and_run(addr)
|
||||
except SPSDKConnectionError as exc:
|
||||
except _CONNECTION_LOST_EXCEPTIONS as exc:
|
||||
raise ConnectionLostError(
|
||||
f"USB-соединение потеряно при RAM-загрузке: {exc}"
|
||||
) from exc
|
||||
|
|
@ -352,7 +391,7 @@ def flash(
|
|||
)
|
||||
ok = mboot.flash_erase_region(FLASH_BASE, erase_size, mem_id=0)
|
||||
if not ok:
|
||||
raise FlashBackendError("flash_erase_region вернул False")
|
||||
_fail_command("flash_erase_region вернул False")
|
||||
|
||||
_emit(progress_cb, "fcb", 0, "Запись FCB")
|
||||
if fcb_path is not None:
|
||||
|
|
@ -371,11 +410,11 @@ def flash(
|
|||
write_addr, data, mem_id=0, progress_callback=_on_progress
|
||||
)
|
||||
if not ok:
|
||||
raise FlashBackendError("write_memory (HAB-образ) вернул False")
|
||||
_fail_command("write_memory (HAB-образ) вернул False")
|
||||
|
||||
_emit(progress_cb, "reset", 0, "Reset")
|
||||
mboot.reset(reopen=False)
|
||||
except SPSDKConnectionError as exc:
|
||||
except _CONNECTION_LOST_EXCEPTIONS as exc:
|
||||
raise ConnectionLostError(
|
||||
f"USB-соединение потеряно во время прошивки: {exc}"
|
||||
) from exc
|
||||
|
|
@ -402,11 +441,11 @@ def erase_chip(progress_cb: Optional[ProgressCallback] = None) -> None:
|
|||
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")
|
||||
_fail_command("flash_erase_all вернул False")
|
||||
|
||||
_emit(progress_cb, "reset", 0, "Reset")
|
||||
mboot.reset(reopen=False)
|
||||
except SPSDKConnectionError as exc:
|
||||
except _CONNECTION_LOST_EXCEPTIONS as exc:
|
||||
raise ConnectionLostError(
|
||||
f"USB-соединение потеряно во время chip erase: {exc}"
|
||||
) from exc
|
||||
|
|
|
|||
467
tools/production/docs/RELEASE_PLAN_v2.md
Normal file
467
tools/production/docs/RELEASE_PLAN_v2.md
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
# service-tui — Roadmap до релиза v1 (фазы 4a → 4b → 5 → 6)
|
||||
|
||||
> Продолжение `MONOLITH_APP_PLAN.md` (V4). Фазы 0–3 закрыты, Фаза 4
|
||||
> закрыта частично — деструктивные гейты на железе вскрыли пробел в
|
||||
> типизации обрыва USB (см. Фаза 4a). Этот документ — дорожная карта
|
||||
> оставшегося пути до релиза.
|
||||
>
|
||||
> Ветка: `feature-tui-monolith` от `dev`. Каждая фаза = коммит(ы) с
|
||||
> зелёным гейтом; откат любой фазы не ломает предыдущие.
|
||||
|
||||
---
|
||||
|
||||
## Статус на входе
|
||||
|
||||
| Фаза | Статус |
|
||||
| --- | --- |
|
||||
| 0 — Спайк / де-риск | ✅ Закрыта (⚠В1/⚠В2/Р7/О1 сняты, golden-тест byte-exact) |
|
||||
| 1 — Backend-модуль | ✅ Закрыта (`flash_backend.py`, `usb_ports.py`, 41 тест) |
|
||||
| 2 — Пересадка `Flasher` | ✅ Закрыта (async-мост, гейт на железе) |
|
||||
| 3 — Custom HAB in-process | ✅ Закрыта (де-факто вместе с фазой 1, подтверждена на железе) |
|
||||
| 4 — Обработка отвала USB | ⚠️ **Частично** — see 4a |
|
||||
| 4a — Добор типизации обрыва | ⏳ **Следующая** |
|
||||
| 4b — Сокращение логов | ⏳ |
|
||||
| 5 — Упаковка PyInstaller | ⏳ |
|
||||
| 6 — Документация / релиз | ⏳ |
|
||||
|
||||
### Почему Фаза 4 не закрыта
|
||||
|
||||
Деструктивные гейты на железе (macOS) показали: **выдёргивание USB
|
||||
проявляется тремя разными способами**, а код Фазы 4 корректно
|
||||
типизирует только один.
|
||||
|
||||
| Проявление в spsdk | Что реально прилетает | Фаза 4 (сейчас) | Пользователь видит |
|
||||
| --- | --- | --- | --- |
|
||||
| write-фаза, обрыв при записи | `SPSDKConnectionError` | обёрнут только вокруг `with SDP`/`with McuBoot`, но реальный путь `write_memory` прошёл мимо | «Непредвиденная ошибка» (safety net) |
|
||||
| read-фаза после write | `SPSDKTimeoutError` (потомок `SPSDKError`, **не** `SPSDKConnectionError`) | не ловится обёрткой обрыва | «Непредвиденная ошибка» (safety net) |
|
||||
| chip erase по таймауту | `flash_erase_all()` вернул `False` (без исключения) | ветка `if not ok: raise FlashBackendError(...)` | «flash_erase_all вернул False» |
|
||||
|
||||
План Фазы 4 буквально требовал «`McuBootConnectionError`/**таймауты** →
|
||||
`FlashBackendError(connection_lost=True)`». Таймаут (`SPSDKTimeoutError`)
|
||||
и `False`-по-таймауту не были покрыты — значит Гейт 4 по букве не пройден.
|
||||
Это добор, а не новая работа сверх плана.
|
||||
|
||||
> **Важно (UX-надёжность уже работает):** safety net (`except Exception`
|
||||
> в `flasher.py`) во всех трёх случаях корректно вернул `ok=False`,
|
||||
> разблокировал кнопки, оставил приложение живым. Проблема
|
||||
> исключительно в *формулировке* сообщения, не в устойчивости.
|
||||
|
||||
---
|
||||
|
||||
## Принятые решения этого этапа
|
||||
|
||||
| ID | Решение |
|
||||
| --- | --- |
|
||||
| **Р10** | Erase-таймаут (`False` без исключения) переклассифицируется в `ConnectionLostError` **вариантом B**: после `False` выполнить быстрый `detect_sdp()` — если устройство пропало с шины, это обрыв; если на месте — честная ошибка операции. |
|
||||
| **Р11** | Троттлинг `write`-событий в `#flash-log` — каждые **10%** (≈10 строк вместо ~135). Прогресс-бар обновляется на **каждом** событии (плавность не теряется), в лог пишется прореженно. |
|
||||
| **Р12** | Логирование: root/`spsdk`/`libusbsio` понижаются до `WARNING` по умолчанию; полный DEBUG доступен через env-переключатель (диагностика не теряется совсем). |
|
||||
| **О2 (закрыт)** | Состав `firmware/` в релизе — только `firmware_test`; тип сборки (Debug/Release) управляется через `.env` (`FIRMWARE_BUILD_TYPE`), механизм уже реализован в `flasher.py`. |
|
||||
| **POST-1** | Циклический прогон неинтерактивных тестов (SDRAM/NOR/OPTO/CAN) на `DiagScreen` — **отложен на пост-релиз**, вне `MONOLITH_APP_PLAN.md`. Зафиксирован, чтобы не потерять. |
|
||||
| **Предложение 2** | Кнопка «Выйти из приложения» на `WaitingScreen` — принято, включается в Фазу 5 (UI-полировка перед упаковкой). |
|
||||
|
||||
---
|
||||
|
||||
## Фаза 4a — Добор: корректная типизация обрыва USB
|
||||
|
||||
**Цель:** все три проявления обрыва USB дают пользователю единое
|
||||
понятное сообщение «Соединение с платой потеряно», а не «Непредвиденная
|
||||
ошибка» / «flash_erase_all вернул False».
|
||||
|
||||
### Файлы
|
||||
|
||||
| Файл | Тип правки |
|
||||
| --- | --- |
|
||||
| `app/flash_backend.py` | правки: расширить обёртку обрыва на `SPSDKTimeoutError`; реализовать вариант B для erase |
|
||||
| `tests/test_flash_backend.py` | правки: тесты на `SPSDKTimeoutError`-путь и на erase-переклассификацию |
|
||||
|
||||
### Содержание
|
||||
|
||||
1. **Обёртка обрыва расширяется** с `SPSDKConnectionError` на общий
|
||||
родительский случай, покрывающий и `SPSDKTimeoutError`. Оба —
|
||||
потомки `SPSDKError`, но `SPSDKTimeoutError` **не** наследует
|
||||
`SPSDKConnectionError`, поэтому текущий `except SPSDKConnectionError`
|
||||
его пропускает. Ловим оба явным кортежем
|
||||
`(SPSDKConnectionError, SPSDKTimeoutError)` в трёх местах:
|
||||
`load_flashloader`, `flash` (основная + ram_only ветки), `erase_chip`.
|
||||
|
||||
2. **Вариант B для erase** (Р10): в `erase_chip` (и в `flash`, где
|
||||
команды возвращают `False` по тем же причинам) — при `ok == False`
|
||||
выполнить быстрый `detect_sdp()`; если устройство исчезло с шины →
|
||||
`ConnectionLostError`, иначе → обычный `FlashBackendError` с прежним
|
||||
текстом. Проверка `detect_sdp()` добавляется **только в error-путь**,
|
||||
на happy path не влияет.
|
||||
|
||||
3. **`_format_error_message` в `flasher.py` не трогается** — он уже
|
||||
корректно даёт префикс «Соединение с платой потеряно» для любого
|
||||
`connection_lost=True`. Достаточно, чтобы backend правильно поднял
|
||||
`ConnectionLostError`.
|
||||
|
||||
### Гейт 4a
|
||||
|
||||
- [ ] Юнит-тест: `SPSDKTimeoutError` из `write_memory` → `ConnectionLostError`
|
||||
(мок).
|
||||
- [ ] Юнит-тест: `flash_erase_all` → `False` + `detect_sdp()==False` →
|
||||
`ConnectionLostError`; `False` + `detect_sdp()==True` →
|
||||
обычный `FlashBackendError` (мок).
|
||||
- [ ] Существующие 41 тест зелёные (регрессии нет).
|
||||
- [ ] **Железо (повтор деструктивных сценариев):**
|
||||
- [ ] Выдернуть USB во время `write-memory` → в `#flash-log`
|
||||
«Соединение с платой потеряно», не «Непредвиденная ошибка».
|
||||
- [ ] Выдернуть во время chip erase → то же сообщение.
|
||||
- [ ] Повторная вставка → прошивка успешна (порт не «занят»).
|
||||
- [ ] macOS + Windows.
|
||||
|
||||
---
|
||||
|
||||
## Фаза 4b — Сокращение логов
|
||||
|
||||
**Цель:** лог-файл прошивки уменьшается на порядок; `#flash-log` в TUI
|
||||
показывает осмысленный прогресс, а не ~135 однотипных строк.
|
||||
|
||||
### Файлы
|
||||
|
||||
| Файл | Тип правки |
|
||||
| --- | --- |
|
||||
| `app/main.py` | правки: уровни логгеров (Р12) + env-переключатель DEBUG |
|
||||
| `app/screens/flash.py` | правки: троттлинг `write`-событий в `_on_progress` (Р11) |
|
||||
|
||||
### Содержание
|
||||
|
||||
1. **Уровни логгеров (Р12):** root по умолчанию `INFO`; `spsdk`,
|
||||
`libusbsio`, `libusbsio.hidapi.dev`, `spsdk.mboot.protocol.bulk_protocol`
|
||||
→ `WARNING` (именно они дают портянки HID-байтов). Полный DEBUG
|
||||
включается через переменную окружения (например
|
||||
`SERVICE_LOG_LEVEL=DEBUG`) — точное имя согласовать, не изобретаю.
|
||||
|
||||
2. **Троттлинг `#flash-log` (Р11):** в `flash.py::_on_progress`
|
||||
прогресс-бар обновляется всегда, а `write_line` в лог для фазы
|
||||
`write` — только при пересечении 10%-границы (0/10/20/…/100).
|
||||
Остальные фазы (`configure`/`erase`/`fcb`/`reset`/`done`/`error`/
|
||||
`hab_build`) логируются как есть — их немного.
|
||||
|
||||
### Гейт 4b
|
||||
|
||||
- [ ] Лог-файл одной прошивки на порядок короче; DEBUG-портянок
|
||||
spsdk/libusbsio нет при дефолтном уровне.
|
||||
- [ ] `SERVICE_LOG_LEVEL=DEBUG` (или согласованное имя) возвращает
|
||||
полный DEBUG — диагностика доступна.
|
||||
- [ ] `#flash-log`: ~10 строк прогресса записи вместо ~135, бар
|
||||
по-прежнему плавный.
|
||||
- [ ] Регрессия: прошивка/erase/диагностика на железе работают.
|
||||
|
||||
---
|
||||
|
||||
## Фаза 5 — Упаковка PyInstaller + UI-полировка
|
||||
|
||||
**Цель:** один исполняемый бандл на Windows и macOS, проходящий полный
|
||||
полевой цикл на чистой машине без Zadig/сети/Python. Плюс кнопка
|
||||
«Выйти» на `WaitingScreen`.
|
||||
|
||||
### Файлы
|
||||
|
||||
| Файл | Тип правки |
|
||||
| --- | --- |
|
||||
| `tools/production/service_tui.spec` | новый — PyInstaller spec |
|
||||
| `app/screens/waiting.py` | правки: кнопка «Выйти из приложения» (Предложение 2) |
|
||||
| just-рецепт | новый — имя задачи согласовать, **не изобретаю** |
|
||||
| `app/app.tcss` | правки при необходимости — стиль кнопки Quit на Waiting |
|
||||
|
||||
### Содержание spec (из плана V4, §Фаза 5)
|
||||
|
||||
- `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` при необходимости —
|
||||
документированный NXP механизм для frozen);
|
||||
- **`collect_dynamic_libs("libusbsio")`** — нативный HID-транспорт
|
||||
(следствие Р7); `libusb-1.0.*` в бандле **отсутствует**;
|
||||
- `datas`: `tools/host/dcd/{dcd.bin, w25q128_fdcb.bin, w25q512_fdcb.bin,
|
||||
ivt_flashloader.bin}` → `data/`;
|
||||
- `datas`: `firmware/<Type>/firmware_test_hab.bin` (Type из `.env`, О2);
|
||||
- `datas`: `pyproject.toml` (для `_read_app_version` во frozen);
|
||||
- onedir (не onefile — onefile замедляет старт распаковкой);
|
||||
- резолвер путей backend'а уже готов: frozen → `sys.executable.parent`
|
||||
(`firmware_hab_path`, `_resolve_custom_binaries_dir`).
|
||||
|
||||
Целевая структура бандла:
|
||||
|
||||
```
|
||||
service-tui-vX.Y.Z-<os>/
|
||||
├── service_tui[.exe]
|
||||
├── _internal/
|
||||
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||
│ └── ... ← рантайм PyInstaller, libusbsio
|
||||
├── firmware/
|
||||
│ └── <Type>/firmware_test_hab.bin
|
||||
└── custom_binaries/ ← пустая
|
||||
```
|
||||
|
||||
### UI-полировка (Предложение 2)
|
||||
|
||||
Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично
|
||||
`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`).
|
||||
|
||||
### Гейт 5 (Windows + macOS)
|
||||
|
||||
- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный
|
||||
полевой цикл — детект SDP → firmware_test → диагностика →
|
||||
custom (W25Q128 и W25Q512) → chip erase.
|
||||
- [ ] То же на macOS.
|
||||
- [ ] Версия на `WaitingScreen` корректна во frozen.
|
||||
- [ ] Порты резолвятся при перетыкании в другой физический USB-порт
|
||||
(проверка Р8 на обеих ОС).
|
||||
- [ ] Кнопка «Выйти» на `WaitingScreen` работает.
|
||||
- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны —
|
||||
подтверждено О1) виден во frozen-бандле.
|
||||
|
||||
---
|
||||
|
||||
## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз
|
||||
|
||||
**Цель:** синхронизировать документацию с реальностью монолита,
|
||||
провести отложенную зачистку комментариев/grep, собрать релизный
|
||||
артефакт из тега.
|
||||
|
||||
### Файлы
|
||||
|
||||
| Файл | Тип правки |
|
||||
| --- | --- |
|
||||
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) |
|
||||
| `CHANGELOG.md` | правки |
|
||||
| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap |
|
||||
| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) |
|
||||
| `HOW_TO_FLASH.md` | правки |
|
||||
| `tools/production/README.md` | правки |
|
||||
| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||
|
||||
### Содержание
|
||||
|
||||
1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход
|
||||
по всему коду — актуализировать docstring-провенансы («прямой порт
|
||||
flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита.
|
||||
Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто
|
||||
в `app/`) — либо достигается, либо остаётся осознанно как
|
||||
документация происхождения (решение по каждому вхождению).
|
||||
2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess),
|
||||
нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8),
|
||||
нативная обработка отвала USB, упаковка одним exe.
|
||||
3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план
|
||||
RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1).
|
||||
4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты),
|
||||
`flash_backend.py` — production-TUI; независимые реализации (Р2).
|
||||
5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk.
|
||||
6. **Ограничение «одна плата на столе»** (О3) — в README.
|
||||
7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как
|
||||
запланированную пост-релизную фичу.
|
||||
8. Тег релиза = версия из `pyproject.toml`.
|
||||
|
||||
### Гейт 6
|
||||
|
||||
- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5).
|
||||
- [ ] `just host::flash*`, `incoming`, `production` работают как раньше —
|
||||
регрессия dev-пути.
|
||||
- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на
|
||||
релизном бинаре.
|
||||
- [ ] POST-1 зафиксирован в бэклоге, не потерян.
|
||||
|
||||
---
|
||||
|
||||
## Сводная последовательность и зависимости
|
||||
|
||||
```
|
||||
4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1
|
||||
│ │ │ │
|
||||
│ │ │ └── доки, зачистка, тег, регрессия dev-пути
|
||||
│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting
|
||||
│ └── уровни логов + троттлинг #flash-log
|
||||
└── типизация обрыва (SPSDKTimeoutError + erase вариант B)
|
||||
|
||||
Блокеры перед фазами:
|
||||
4a: нет — старт сразу
|
||||
4b: нет — после 4a
|
||||
5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG
|
||||
6: все гейты 4a/4b/5 зелёные на железе
|
||||
```
|
||||
|
||||
## Открытые мелочи (согласовать по ходу, не блокируют старт 4a)
|
||||
|
||||
| Вопрос | Когда нужен | Предложение |
|
||||
| --- | --- | --- |
|
||||
| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) |
|
||||
| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю |
|
||||
| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-<os>` (из плана) |
|
||||
|
||||
---
|
||||
|
||||
## Риски этого этапа
|
||||
|
||||
| Риск | Фаза | Митигация |
|
||||
| --- | --- | --- |
|
||||
| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает |
|
||||
| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель |
|
||||
| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле |
|
||||
| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` |
|
||||
| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет |
|
||||
|
||||
---
|
||||
|
||||
# Приложение: работа в новом треде
|
||||
|
||||
Этот roadmap рассчитан на продолжение в **новом чате без контекста**
|
||||
предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы
|
||||
новый тред стартовал без потерь.
|
||||
|
||||
## A. Какой набор правил к чему применяется
|
||||
|
||||
Проектные правила «Role & Hardware Context» (senior embedded C, i.MX
|
||||
RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`,
|
||||
CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`).
|
||||
|
||||
**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в
|
||||
`tools/production`. Поэтому:
|
||||
|
||||
| Правило | Применимо к Python-работе roadmap? |
|
||||
| --- | --- |
|
||||
| Unified diffs, не полные переписывания | ✅ Да |
|
||||
| «Какой файл / какая функция затронуты» — первым | ✅ Да |
|
||||
| ASK при неоднозначности/противоречии | ✅ Да |
|
||||
| Не изобретать just-таски / пути / структуру | ✅ Да |
|
||||
| Проверять существующие файлы перед правкой | ✅ Да |
|
||||
| No malloc/free в драйверах и ISR | ❌ C-специфично |
|
||||
| NXP SDK HAL вместо raw-регистров | ❌ C-специфично |
|
||||
| Doxygen на public API | ❌ (Python — docstrings, уже используются) |
|
||||
| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) |
|
||||
| CMake target_compile_options | ❌ Неприменимо |
|
||||
|
||||
Когда/если roadmap коснётся C-части — C-правила снова в силе.
|
||||
|
||||
## B. Первый вопрос на старте нового треда (не потерять)
|
||||
|
||||
**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip`
|
||||
предлагается обернуть в `try/except`, и **любую ошибку самой проверки**
|
||||
(не только «устройство отсутствует») трактовать как обрыв — потому что
|
||||
проверка и так выполняется только после уже случившегося сбоя, шина
|
||||
нестабильна, и «не смог проверить» практически всегда означает «платы
|
||||
нет». Требуется явное подтверждение этой трактовки перед написанием
|
||||
кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный
|
||||
`FlashBackendError`.)
|
||||
|
||||
## C. Файлы, которые нужно предоставить — по фазам
|
||||
|
||||
Пути относительно `tools/production/`, если не указано иное. Пометка
|
||||
**[есть в этом треде]** — файл уже фигурировал и его актуальная версия
|
||||
известна; в новом треде его всё равно нужно приложить заново.
|
||||
|
||||
### Фаза 4a — типизация обрыва
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B |
|
||||
| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию |
|
||||
| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) |
|
||||
| `app/models.py` | контекст: `FlashProgress` |
|
||||
|
||||
### Фаза 4b — логи
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) |
|
||||
| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) |
|
||||
| `.env` / `.env.example` (`tools/production/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными |
|
||||
|
||||
### Фаза 5 — упаковка PyInstaller + UI
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `pyproject.toml` (`tools/production/`) | зависимости, версия, `requires-python` — база для spec |
|
||||
| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта |
|
||||
| `app/main.py` | entry point для PyInstaller |
|
||||
| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen |
|
||||
| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit |
|
||||
| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) |
|
||||
| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла |
|
||||
| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) |
|
||||
| `project_tree.txt` или `ls -R tools/production` | реальная структура пакета `app/` для spec |
|
||||
| существующий `.spec`, если уже есть | не изобретать заново |
|
||||
|
||||
### Фаза 6 — документация и релиз
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `CHANGELOG.md` | дописать секцию монолита |
|
||||
| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap |
|
||||
| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) |
|
||||
| `HOW_TO_FLASH.md` | актуализировать под TUI-backend |
|
||||
| `tools/production/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI |
|
||||
| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) |
|
||||
| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) |
|
||||
|
||||
## D. Полный список актуальных файлов монолита (снимок на входе)
|
||||
|
||||
Чтобы в новом треде можно было приложить всё разом, если удобнее не
|
||||
дробить по фазам. Актуальные (пост-Фаза-4) версии:
|
||||
|
||||
```
|
||||
tools/production/
|
||||
├── pyproject.toml
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py
|
||||
│ ├── app.tcss
|
||||
│ ├── main.py (точка входа — фактически в tools/production/main.py, см. pyproject scripts)
|
||||
│ ├── models.py
|
||||
│ ├── flasher.py ← Фаза 2/4, актуальная версия
|
||||
│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест)
|
||||
│ ├── usb_ports.py ← Фаза 1
|
||||
│ ├── firmware_client.py
|
||||
│ ├── m5_client.py
|
||||
│ ├── orchestrator.py
|
||||
│ ├── boot_art.py
|
||||
│ ├── widgets.py (или widgets/)
|
||||
│ └── screens/
|
||||
│ ├── __init__.py
|
||||
│ ├── waiting.py
|
||||
│ ├── flash.py ← Фаза 4 (правлены комментарии)
|
||||
│ ├── post_flash.py
|
||||
│ ├── connection_watcher.py
|
||||
│ └── diag/
|
||||
│ ├── __init__.py
|
||||
│ ├── confirm_panel.py
|
||||
│ ├── results.py
|
||||
│ └── test_list.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ └── test_flash_backend.py ← 41 тест
|
||||
├── spike/ (Фаза 0, в релиз не идёт)
|
||||
│ ├── spike_hab.py
|
||||
│ ├── spike_flash.py
|
||||
│ └── spike_readback.py (диагностика Гейта 3, на будущее)
|
||||
└── custom_binaries/ (пустая, для оператора)
|
||||
|
||||
tools/host/ (dev-CLI, Р2 — НЕ трогается)
|
||||
├── flash_usb.py
|
||||
└── dcd/
|
||||
├── ivt_flashloader.bin
|
||||
├── dcd.bin
|
||||
├── w25q128_fdcb.bin
|
||||
└── w25q512_fdcb.bin
|
||||
```
|
||||
|
||||
> Примечание: `main.py` в `pyproject.toml` прописан как
|
||||
> `service-tui = "main:main"` — точка входа лежит в
|
||||
> `tools/production/main.py` (не в `app/`), а `app/app.py` содержит
|
||||
> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5.
|
||||
|
||||
## E. Что уже решено и не пересматривается (сводка для нового треда)
|
||||
|
||||
- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже).
|
||||
- **Р10** — erase-таймаут → вариант B (detect_sdp после False).
|
||||
- **Р11** — троттлинг лога 10%.
|
||||
- **Р12** — уровни логов + env DEBUG.
|
||||
- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны.
|
||||
- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`.
|
||||
- **О3** — одна плата на столе, ограничение v1.
|
||||
- **POST-1** — циклический прогон тестов, после релиза.
|
||||
- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py`
|
||||
меняются только там, где явно указано в roadmap.
|
||||
- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе.
|
||||
- Порядок ревью: один файл за раз, полные файлы для новых/целиком
|
||||
переписываемых, unified diff для точечных правок.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# TUI Service Tool Plan
|
||||
|
||||
1. Навести порядок с экранами "USB-POLL", "TRANSFER", возможно сделать bootlogo с версией программы и лого компании
|
||||
2. Уменьшить размер активной области до предела и сделать его равным для всех экранов
|
||||
3. Добавить обратный отчет в тесте buttons (таймаут)
|
||||
4. Выгрузка результатов тестов в json файл с привязкой к UID микроконтроллера
|
||||
5. Возможно ли копировать UID с экрана терминала?
|
||||
6. Проверить работоспособность HIL тестов (CAN & Opto)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
"""
|
||||
test_flash_backend.py — юнит-тесты app/flash_backend.py.
|
||||
uv run pytest tests/test_flash_backend.py -v
|
||||
|
||||
Два уровня:
|
||||
1. SDP/McuBoot/detect — моки (unittest.mock через monkeypatch). Железо не
|
||||
|
|
@ -160,10 +161,14 @@ def test_configure_flexspi_ok():
|
|||
)
|
||||
|
||||
|
||||
def test_configure_flexspi_fail():
|
||||
def test_configure_flexspi_fail(monkeypatch):
|
||||
# Плата на месте → честная ошибка операции, НЕ обрыв (вариант B, Фаза 4a).
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
mboot = Mock(configure_memory=Mock(return_value=False))
|
||||
with pytest.raises(fb.FlashBackendError):
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb.configure_flexspi(mboot)
|
||||
assert not isinstance(ei.value, fb.ConnectionLostError)
|
||||
assert ei.value.connection_lost is False
|
||||
|
||||
|
||||
def test_write_fcb_auto_uses_magic_word():
|
||||
|
|
@ -190,13 +195,15 @@ def test_write_fcb_explicit_ok(tmp_path):
|
|||
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):
|
||||
def test_write_fcb_explicit_write_fails(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
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):
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb.write_fcb_explicit(mboot, fcb)
|
||||
assert ei.value.connection_lost is False
|
||||
|
||||
|
||||
# ─── flash() ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -309,6 +316,9 @@ 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, "detect_sdp", Mock(return_value=True)
|
||||
) # плата на месте → честная ошибка
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, write_memory=False)
|
||||
|
||||
|
|
@ -361,6 +371,9 @@ def test_erase_chip_happy_path(monkeypatch, events):
|
|||
|
||||
|
||||
def test_erase_chip_fails(monkeypatch, events):
|
||||
monkeypatch.setattr(
|
||||
fb, "detect_sdp", Mock(return_value=True)
|
||||
) # плата на месте → честная ошибка
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=MagicMock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, flash_erase_all=False)
|
||||
|
||||
|
|
@ -446,3 +459,104 @@ def test_firmware_hab_path_frozen(monkeypatch, tmp_path):
|
|||
monkeypatch.setattr(fb.sys, "executable", str(exe))
|
||||
p = fb.firmware_hab_path("firmware_test", "Debug")
|
||||
assert p == exe.parent / "firmware" / "Debug" / "firmware_test_hab.bin"
|
||||
|
||||
|
||||
# ─── Фаза 4a: типизация обрыва USB ───────────────────────────────────────
|
||||
#
|
||||
# Три проявления обрыва (RELEASE_ROADMAP.md):
|
||||
# 1) SDP/McuBoot бросают SPSDKConnectionError — ловилось и до 4a;
|
||||
# 2) read-фаза после write бросает SPSDKTimeoutError — НЕ ловилось (баг);
|
||||
# 3) команда возвращает False по таймауту (erase/write) — вариант B (Р10).
|
||||
|
||||
|
||||
def test_connection_lost_tuple_covers_timeout():
|
||||
"""Инвариант, на котором держится вся 4a: SPSDKTimeoutError НЕ потомок
|
||||
SPSDKConnectionError, поэтому обязан быть в кортеже явно. Страховка от
|
||||
случайного регресса при апгрейде spsdk."""
|
||||
assert fb.SPSDKTimeoutError in fb._CONNECTION_LOST_EXCEPTIONS
|
||||
assert not issubclass(fb.SPSDKTimeoutError, fb.SPSDKConnectionError)
|
||||
|
||||
|
||||
def test_sdp_still_present_swallows_check_error(monkeypatch):
|
||||
"""§B: ошибка самой проверки detect_sdp() трактуется как «устройства нет»."""
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(side_effect=RuntimeError("bus gone")))
|
||||
assert fb._sdp_still_present() is False
|
||||
|
||||
|
||||
def test_fail_command_device_gone_is_connection_lost(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
with pytest.raises(fb.ConnectionLostError) as ei:
|
||||
fb._fail_command("flash_erase_all вернул False")
|
||||
assert ei.value.connection_lost is True
|
||||
|
||||
|
||||
def test_fail_command_device_present_is_plain_error(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb._fail_command("flash_erase_all вернул False")
|
||||
assert not isinstance(ei.value, fb.ConnectionLostError)
|
||||
assert ei.value.connection_lost is False
|
||||
|
||||
|
||||
def test_load_flashloader_timeout_is_connection_lost(monkeypatch):
|
||||
"""SPSDKTimeoutError из SDP-обмена → ConnectionLostError (обёртка 4a)."""
|
||||
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=True), read_bytes=Mock(return_value=b"\x00" * 16)
|
||||
),
|
||||
)
|
||||
sdp_ctx = MagicMock()
|
||||
sdp_ctx.__enter__.return_value = sdp_ctx
|
||||
sdp_ctx.write_file.side_effect = fb.SPSDKTimeoutError()
|
||||
monkeypatch.setattr(fb, "SDP", Mock(return_value=sdp_ctx))
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError):
|
||||
fb.load_flashloader()
|
||||
|
||||
|
||||
def test_flash_write_memory_timeout_is_connection_lost(monkeypatch, events, tmp_path):
|
||||
"""Гейт 4a #1: SPSDKTimeoutError из write_memory → ConnectionLostError,
|
||||
а не safety-net «Непредвиденная ошибка»."""
|
||||
hab_bin = tmp_path / "fw_hab.bin"
|
||||
hab_bin.write_bytes(b"\xd1" + b"\x00" * 63)
|
||||
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock()))
|
||||
ctx = _mock_mcuboot_ctx(monkeypatch)
|
||||
ctx.write_memory.side_effect = fb.SPSDKTimeoutError()
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError) as ei:
|
||||
fb.flash(hab_bin, progress_cb=_collector(events))
|
||||
assert ei.value.connection_lost is True
|
||||
assert "reset" not in _phases(events)
|
||||
assert "done" not in _phases(events)
|
||||
|
||||
|
||||
def test_flash_write_false_device_gone_is_connection_lost(
|
||||
monkeypatch, events, tmp_path
|
||||
):
|
||||
"""Вариант B в flash(): write_memory=False + плата пропала → ConnectionLostError."""
|
||||
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)
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError):
|
||||
fb.flash(hab_bin, progress_cb=_collector(events))
|
||||
|
||||
|
||||
def test_erase_chip_false_device_gone_is_connection_lost(monkeypatch, events):
|
||||
"""Гейт 4a #2a: flash_erase_all=False + detect_sdp=False → ConnectionLostError."""
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=MagicMock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, flash_erase_all=False)
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError) as ei:
|
||||
fb.erase_chip(progress_cb=_collector(events))
|
||||
assert ei.value.connection_lost is True
|
||||
assert "done" not in _phases(events)
|
||||
|
|
|
|||
Loading…
Reference in a new issue