From c6bc0e993531ee659e7b987efcf7084b1b63fee5 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 6 Jul 2026 19:07:32 +0300 Subject: [PATCH] # service-tui: docs refactored --- bsp/provisioning/README.md | 1 - firmware/test/{README.md => README-1.md} | 0 .../test/src/tests/{README.md => README-2.md} | 0 tools/production/README.md | 130 +++-- tools/production/app/TFT_BOOTLOADER_OLD.bin | Bin 47432 -> 0 bytes tools/production/docs/DEV_ARCH.md | 313 +++++++---- tools/production/docs/DEV_PLAN.md | 115 +++++ tools/production/docs/INITIAL_PLAN.md | 488 ------------------ tools/production/docs/MONOLITH_APP_PLAN.md | 452 ---------------- tools/production/docs/RELEASE_PLAN_v1.md | 128 ----- tools/production/docs/RELEASE_PLAN_v2.md | 467 ----------------- 11 files changed, 404 insertions(+), 1690 deletions(-) rename firmware/test/{README.md => README-1.md} (100%) rename firmware/test/src/tests/{README.md => README-2.md} (100%) delete mode 100644 tools/production/app/TFT_BOOTLOADER_OLD.bin create mode 100644 tools/production/docs/DEV_PLAN.md delete mode 100644 tools/production/docs/INITIAL_PLAN.md delete mode 100644 tools/production/docs/MONOLITH_APP_PLAN.md delete mode 100644 tools/production/docs/RELEASE_PLAN_v1.md delete mode 100644 tools/production/docs/RELEASE_PLAN_v2.md diff --git a/bsp/provisioning/README.md b/bsp/provisioning/README.md index 0ee86c0..b8a044e 100644 --- a/bsp/provisioning/README.md +++ b/bsp/provisioning/README.md @@ -76,7 +76,6 @@ target_link_libraries(firmware_test PRIVATE bsp_provisioning) | `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | | `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers | - --- ## Особенности diff --git a/firmware/test/README.md b/firmware/test/README-1.md similarity index 100% rename from firmware/test/README.md rename to firmware/test/README-1.md diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README-2.md similarity index 100% rename from firmware/test/src/tests/README.md rename to firmware/test/src/tests/README-2.md diff --git a/tools/production/README.md b/tools/production/README.md index 395117a..12b4553 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -1,11 +1,10 @@ # service-tui — TUI сервисного инженера TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. -Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. +Написано на Python + [Textual](https://textual.textualize.io/). Работает на MacOS, Windows. > Внутреннее устройство, диаграммы архитектуры и заметки для разработчиков — -> в [DEV_ARCH.md](DEV_ARCH.md). Этот документ — только про то, как приложением -> пользоваться. +> в [DEV_ARCH.md](docs/DEV_ARCH.md) --- @@ -18,11 +17,13 @@ TUI-приложение для диагностики и прошивки пл ```bash ┌────────────────────────────────────────────────────┐ -│ service_tool v0.3.0 │ +│ service_tool vX.Y.Z │ │ │ │ [LOGO_ART] │ │ │ │ Подключите плату индикатора к USB... ⠋ │ +│ │ +│ [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` @@ -55,29 +56,37 @@ TUI не пытается восстановить прежнее состоян │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ -│ │ ▶ Сборка HAB-образа (nxpimage)... │ │ -│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ +│ │ ▶ Прошивка: firmware_test │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` -Лог виден постоянно (не только во время прошивки), прогресс-бар — только -во время активной операции (скрыт в простое), без числового `%` — только -полоса и построчный лог в реальном времени. Панель выбора прошивки -ограничена по высоте и скроллится сама, если разрастается (варианты -"Другое") — лог снизу гарантированно не сжимается меньше 6 строк. +**Если после «Загрузить» появилась ошибка, а плата всё ещё видна на этом же +экране** — это ожидаемо: логическая ошибка (не найден файл, не подошёл +формат) не выкидывает на экран ожидания, потому что плата физически +подключена. Прочитайте сообщение в логе, поправьте выбор и нажмите +«Загрузить» ещё раз. На экран ожидания TUI переключает только при реальном +физическом обрыве USB. **"Другое" — для бинарников, собранных не в этом репозитории.** В -`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без -FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до `nxpimage`). -TUI сама собирает из него загружаемый образ на лету: +`custom_binaries/` кладётся бинарник — сырой (код + таблица векторов, без +FCB/IVT/DCD) либо уже готовый HAB-образ, в зависимости от источника. TUI +сама достраивает недостающее на лету: -1. `nxpimage hab export` — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") -2. в Flash пишется явный FCB под выбранную память платы (не тот же +1. Собирает загружаемый HAB-образ (добавляет IVT, +DCD — если включён + тумблер "Использует SDRAM") +2. В Flash пишется явный FCB под выбранную память платы (не тот же auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для - W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`) -3. образ прошивается с `0x60001000`, как обычно + W25Q256/512 он ненадёжен, см. `DEV_ARCH.md §8.4`) +3. Образ прошивается стандартным адресом + +**Нужен ли тумблер DCD — зависит от конкретного бинарника, не от того, в +каком виде он получен.** Одна и та же связка `bootloader + tft_app` не +требует DCD, а часть кастомных/легаси образов (например, старый загрузчик, +используемый на производстве) требует его независимо от формата файла. Если +не уверены, нужен ли конкретному образу DCD — уточните у того, кто его +предоставил, прежде чем прошивать. **Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не нужно выставлять заново на каждой следующей плате: прошили одну, вынули @@ -96,8 +105,8 @@ Production/Custom этот шаг не нужен). │ Переведите плату в нормальный режим: │ │ BOOT_MOD_1 → GND → Reset │ │ │ -│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ │ Автопереход через: 40с │ +│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` @@ -128,6 +137,7 @@ Production/Custom этот шаг не нужен). ``` Что важно знать: + - **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать все"/"Снять все". - **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена @@ -147,7 +157,7 @@ Production/Custom этот шаг не нужен). --- -## Рабочие процессы сервисника +## Рабочие процессы сервисного инженера ### Диагностика (firmware_test уже прошит) @@ -174,11 +184,12 @@ Production/Custom этот шаг не нужен). Для плат старых ревизий и любых образов, собранных не в этом репозитории. ```bash -1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/ +1. Положить бинарник (сырой или уже HAB, см. раздел выше) в custom_binaries/ (или в директорию из SERVICE_CUSTOM_BINARIES_DIR) 2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen -3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости -4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB +3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD, если + конкретно этот образ его требует (уточнить у источника файла) +4. Загрузить — TUI сама соберёт HAB-образ (если нужно) и запишет правильный FCB 5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже подставлен, останется нажать «Загрузить» ``` @@ -196,14 +207,6 @@ Production/Custom этот шаг не нужен). ## Конфигурация (`.env`) ```ini -# USB VID:PID — BootROM SDP (константы NXP, не менять) -BOOTROM_VID=1fc9 -BOOTROM_PID=0130 - -# USB VID:PID — Flashloader (константы NXP, не менять) -FLASHLOADER_VID=15a2 -FLASHLOADER_PID=0073 - # USB VID:PID — firmware_test CDC (наше устройство) SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad @@ -212,7 +215,7 @@ SERVICE_CDC_PID=00ad SERVICE_M5_VID=303a SERVICE_M5_PID=4001 -# Директория с сырыми кастомными бинарниками для FlashScreen → "Другое". +# Директория с кастомными бинарниками для FlashScreen → "Другое". # По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом # с main.py в dev-режиме); создаётся автоматически при старте. # SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries @@ -221,10 +224,20 @@ SERVICE_M5_PID=4001 # Release временно нестабилен — по умолчанию Debug. FIRMWARE_BUILD_TYPE=Debug +# Уровень логирования. По умолчанию INFO (плюс WARNING принудительно для +# шумных модулей spsdk/libusbsio). DEBUG — полный лог, включая построчные +# HID-дампы каждой команды spsdk (для диагностики проблем прошивки). +# SERVICE_LOG_LEVEL=DEBUG + # Опционально: путь к директории лога TUI # SERVICE_LOG_DIR=/tmp ``` +> `.env` не загружается в упакованном (frozen) приложении — standalone-бинарь +> работает на встроенных значениях по умолчанию. Переменные окружения (не +> `.env`-файл) по-прежнему действуют и во frozen-режиме, если их выставить +> перед запуском. + --- ## Запуск @@ -232,45 +245,56 @@ FIRMWARE_BUILD_TYPE=Debug ### Из монорепозитория (разработчик) ```bash -just host::service-setup # установить зависимости tools/production/ +just host::service-setup # установить/обновить зависимости tools/production/ just host::service-tui # запустить TUI ``` ### Standalone-бинарь (сервисник) +Распаковать `service-tui-vX.Y.Z-.zip` в любую директорию и запустить +`service_tui` (`service_tui.exe` на Windows). Файл самодостаточен — не +требует установленного Python, `uv`, драйверов (Zadig/WinUSB) или сетевого +доступа. + +### Сборка релизного бандла (разработчик) + ```bash -just host::service-build -# → tools/production/dist/service_tui +just build::hab-all-release # или hab-all-debug — собрать HAB-образы заранее +just host::package-tui # → tools/production/dist/service-tui-vX.Y.Z-/ ``` -> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен -> инициализированный `tools/host/` (`just host::setup-tools`), либо -> абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`). +Устройство бандла (`_internal/`, `firmware/`, `custom_binaries/`) и детали +сборки (`service_tui.spec`) — в [DEV_ARCH.md §14](DEV_ARCH.md#14-упаковка-pyinstaller-фаза-5). + +> Если на Windows `package-tui` падает с `Permission denied` на шаге +> переименования — закройте запущенный `service_tui.exe` от предыдущей +> сборки и повторите (см. `DEV_ARCH.md §14.4`). --- ## Зависимости -| Пакет | Версия | Назначение | -| --------------- | ------ | ----------------------------------------------------- | -| `textual` | ≥ 0.80 | TUI фреймворк | -| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | -| `pyusb` | ≥ 1.0 | детект BootROM SDP (не виден через pyserial на macOS) | -| `python-dotenv` | ≥ 1.0 | загрузка `.env` | -| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | - -**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает -`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть -инициализирован (`just host::setup-tools`). +| Пакет | Версия | Назначение | +| --------------- | ------ | -------------------------------------------------------------------------------------- | +| `textual` | ≥ 0.80 | TUI фреймворк | +| `pyserial` | ≥ 3.5 | USB CDC ACM (firmware_test) + Serial (M5StampPLC) | +| `spsdk` | 3.7.0 | прошивка in-process: SDP, McuBoot, HabImage | +| `python-dotenv` | ≥ 1.0 | загрузка `.env` | +| `pyinstaller` | ≥ 6.0 | сборка standalone-бандла | +| `pyusb` | ≥ 1.0 | не используется текущей детект-логикой (см. `DEV_ARCH.md §2`), оставлен в зависимостях | --- ## Логирование ```bash -tools/production/service_tui.log ← по умолчанию -$SERVICE_LOG_DIR/service_tui.log ← если задан в .env +tools/production/service_tui.log ← по умолчанию (рядом с main.py в dev, + рядом с exe во frozen) +$SERVICE_LOG_DIR/service_tui.log ← если задан в .env / окружении ``` -Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не -пишет в stdout — Textual захватывает терминал. +Уровень по умолчанию: `INFO` для модулей приложения (`WARNING` для `textual` +и принудительно для шумных модулей `spsdk`/`libusbsio`, которые на `DEBUG` +печатают построчные HID-дампы каждой команды). Полный `DEBUG` — через +`SERVICE_LOG_LEVEL=DEBUG`. TUI не пишет в stdout — Textual захватывает +терминал. diff --git a/tools/production/app/TFT_BOOTLOADER_OLD.bin b/tools/production/app/TFT_BOOTLOADER_OLD.bin deleted file mode 100644 index f5a469290f8312405ba9678b4fc812dd15610daf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47432 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 SCNANwBw diff --git a/tools/production/docs/DEV_ARCH.md b/tools/production/docs/DEV_ARCH.md index 8e8563f..a7b4655 100644 --- a/tools/production/docs/DEV_ARCH.md +++ b/tools/production/docs/DEV_ARCH.md @@ -6,34 +6,42 @@ > взаимодействия с firmware/M5, экранную архитектуру Textual, известные > особенности фреймворка. > Пользовательская документация (экраны, запуск, конфигурация, -> рабочие процессы сервисника) — в [README.md](README.md). +> рабочие процессы сервисника) — в [README.md](../README.md). --- ## 1. Структура проекта +## 1. Структура проекта + ```bash tools/production/ -├── main.py ← точка входа (10 строк) +├── main.py ← точка входа ├── pyproject.toml ← зависимости uv +├── dist/ ← дистрибутивы программы (PyInstaller) ├── uv.lock -├── custom_binaries/ ← runtime, gitignored, создаётся автоматически -│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое» -└── app/ +├── service_tui.spec ← PyInstaller spec +├── custom_binaries/ ← runtime, создаётся автоматически; +│ сырые/готовые бинарники для FlashScreen → «Другое» +└── app/ ← implicit namespace package + │ + │ ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов ├── app.tcss ← единый файл стилей для всех экранов ├── models.py ← все типы данных (dataclass/Enum) ├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen - ├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8) - ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8) - ├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py + ├── firmware_client.py ← async USB CDC клиент firmware_test + ├── m5_client.py ← async M5StampPLC клиент + ├── flash_backend.py ← spsdk 3.7.0 in-process: SDP, McuBoot, HabImage + ├── flasher.py ← async-обёртка над flash_backend для Textual workers + ├── usb_ports.py ← резолвер serial-портов по VID:PID ├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты ├── widgets/ │ ├── __init__.py │ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов └── screens/ ├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen - ├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер причины возврата + ├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер, кнопка «Выйти» ├── flash.py ← FlashScreen — прошивка / chip erase ├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки ├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB @@ -55,7 +63,7 @@ graph LR subgraph app["app/"] FC["firmware_client.py\nUSB CDC ACM, UTF-8"] M5["m5_client.py\nSerial JSON-lines, UTF-8"] - FL["flasher.py\nsubprocess + pyusb detect"] + FL["flasher.py + flash_backend.py\nspsdk in-process: SDP/McuBoot/HabImage"] OR["orchestrator.py\nconfirm/progress/timeout router"] end TUI --> FC & M5 & FL & OR @@ -70,22 +78,16 @@ graph LR M5HW["M5StampPLC\nRLY1–4 + CAN"] end - subgraph Host["tools/host/"] - FU["flash_usb.py\nsdphost + blhost"] - end - - FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW - FL -->|"subprocess uv run"| FU - FU -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM - M5 <-->|"JSON-lines\nSerial"| M5HW + FC |"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW + FL |"spsdk (libusbsio HID)\nVID:PID 1FC9:0130 / 15A2:0073"| ROM + M5 |"JSON-lines\nSerial"| M5HW M5HW -->|"RLY1–4"| Board ``` -> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как -> основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через -> `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC. -> M5StampPLC детектируется отдельно в `m5_client.py` тем же способом -> (`pyusb`, VID/PID из `.env` — см. раздел 5). +> **Детект USB:** `flash_backend.detect_sdp()`/`detect_cdc()` используют spsdk +> напрямую (`SdpUSBInterface.scan()` / `MbootUSBInterface.scan()`, HID-транспорт +> через `libusbsio`). CDC firmware_test и M5StampPLC резолвятся через +> `pyserial` (`usb_ports.py::resolve_serial_port()`, `m5_client.py`). --- @@ -103,7 +105,7 @@ stateDiagram-v2 WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC FLASHING --> POST_FLASH : firmware_test прошит успешно - FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое + FLASHING --> WAITING : Production/Custom прошит,\nили потеря USB (в простое ИЛИ во время операции) POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с @@ -115,6 +117,13 @@ stateDiagram-v2 `ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на сообщения `DeviceDetected`/`FlashDone`/`DiagDone`. +> **Важная деталь, не показанная на диаграмме**: +> `FLASHING --> WAITING` по стрелке «ошибка» срабатывает **только** при +> физическом обрыве USB (`FlashResult.connection_lost=True`). Логическая +> ошибка (файл не найден, битый custom-бинарь) — плата на месте, экран +> остаётся на `FLASHING` (нет перехода состояния вообще, поэтому на +> диаграмме это не отдельная стрелка). См. §6. + --- ## 4. Обработка confirm_request @@ -154,9 +163,7 @@ flowchart TD **Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно одним событием `SUMMARY` — настоящим от firmware или синтетическим -(`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии -зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда -(исторический баг, см. `CHANGELOG.md`). +(`aborted: true`), если чтение порта оборвалось по таймауту. --- @@ -184,11 +191,6 @@ flowchart TD PID — при детекте ориентироваться на `just host::m5-scan`, а не на документацию, если она когда-либо разойдётся с кодом. -**Важно на будущее:** документация (`HIL_BENCH.md`/`HIL_HOW_TO.md`) местами не -успевает за изменениями `agent.py`. При любых будущих изменениях протокола -агента (новые команды, смена формата ответа) — сверяться напрямую через -`grep` по `tools/hil/m5/agent.py`, а не полагаться только на документацию. - --- ## 6. Мониторинг соединения и разрыв сессии @@ -197,22 +199,41 @@ flowchart TD `FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине. - **На `FlashScreen`** — проверка приостановлена во время активной - прошивки/erase (обрыв обнаружит сам `flash_usb.py` subprocess). + прошивки/erase (`self._flashing == True`). - **На `DiagScreen`** — проверка приостановлена во время прогона тестов (обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не просто исчезновение устройства из списка). -- При срабатывании — `ConnectionLost` message → экран постит - `FlashDone(success=False, target=None)` / `DiagDone(reason=...)` → - `ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает - на `WaitingScreen(disconnect_reason=...)`. `FlashDone` в этой ветке не несёт - `preset` — «липкий» выбор (см. §8) сохраняется отдельно, в момент нажатия - «Загрузить», а не при завершении прошивки. -- `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем - продолжает обычный автодетект. -Архитектурное решение: **сессия никогда не восстанавливается** — после +**Три независимых механизма детекта обрыва**: + +1. **`ConnectionWatcherMixin` в простое** — периодический опрос шины. +2. **`flash_backend.py` во время активной операции** — spsdk бросает + `SPSDKConnectionError`/`SPSDKTimeoutError` (оба ловятся явным кортежем + `_CONNECTION_LOST_EXCEPTIONS` — `SPSDKTimeoutError` НЕ наследует + `SPSDKConnectionError`, оба - потомки `SPSDKError` +3. **Вариант B** — некоторые команды spsdk (`flash_erase_all`, + `write_memory` и т.п.) при таймауте не бросают исключение, а тихо + возвращают `False`. `_fail_command()` в этом случае сам проверяет + `_sdp_still_present()`: плата пропала с шины → `ConnectionLostError`; + плата на месте → обычная `FlashBackendError`. + +Оба механизма 2 и 3 транслируются в `Flasher.flash()`/`erase_chip()` как +`FlashResult(ok: bool, connection_lost: bool)` — **не голый `bool`**. Это +принципиально для `FlashScreen`: + +- `connection_lost=True` → `FlashDone(target=None, error_message=...)` → + `ServiceApp` переключает на `WaitingScreen(disconnect_reason=...)`. +- `connection_lost=False` → **экран не покидает себя**. + Плата физически на месте, сообщение об ошибке уже в `#flash-log`, кнопки + разблокированы (`_set_busy(False)`) — оператор может поправить выбор + (другой файл, другой вариант памяти) и повторить, не выдёргивая USB. + +`WaitingScreen` показывает причину возврата баннером на 4 секунды, затем +продолжает обычный автодетект. + +**Cессия никогда не восстанавливается** — после разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует -диагностику с нуля. +заново с нуля. --- @@ -251,15 +272,14 @@ AppFrame { ### 8.1 Проблема -Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются -`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 — -для них auto-config Flashloader (`configure-memory 0xC0000007` → -`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси -бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader -не документирован как надёжный для 4-байтной адресации, а сами бинарники -приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат, -что `inputImageFile` в `hab_*.yaml` до сборки). Решение — собирать HAB -на лету и писать FCB явно, а не полагаться на auto-config. +Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются заранее +(`just build::hab-*`) и всегда идут на плату с W25Q128 — для них auto-config +Flashloader достаточен. Для сторонних/легаси бинарников (старые платы, +W25Q256/512) это не так: auto-config Flashloader не документирован как +надёжный для 4-байтной адресации, а сами бинарники приходят «сырыми» (код + +таблица векторов, без FCB/IVT/DCD) либо уже готовым HAB-образом — зависит от +источника. Решение — собирать HAB на лету (если нужно) и писать FCB явно, а +не полагаться на auto-config. ### 8.2 Модели (`models.py`) @@ -286,53 +306,44 @@ class FlashPreset: одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/ памяти/DCD, нажал «Загрузить», вынул, вставил следующую. -Рассматривался отдельный режим «массовое программирование» (авто-прошивка -по факту детекта SDP, без нажатия кнопки на каждую плату) — отклонён: -в SDP/Flashloader-режиме нет способа прочитать UID платы, авто-старт без -подтверждения оператора убирает последний шанс заметить, что в руках не та -плата. Оставлена только «липкая» память выбора (этот раздел). +**Нужен ли DCD — implementation-defined, зависит от конкретного бинарника, +не от его формата (сырой/готовый HAB).** Правило «сырой → включить DCD, +готовый HAB → выключить» **неверно как общее правило**: например, в связке +`bootloader + tft_app` сам `bootloader` не требует DCD, а часть кастомных +бинарников (в т.ч. старый загрузчик, используемый на производстве) требует +DCD независимо от того, в каком виде получен файл. Оператор должен знать +по конкретному образу, инициализирует ли он SDRAM самостоятельно — TUI не +может определить это автоматически по содержимому файла. ### 8.3 Конвейер сборки (`flasher.py`) -``` +```bash Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) - └── _run_flash_custom() - ├── _build_custom_hab(raw_bin, use_dcd, progress_cb) - │ ├── генерирует temp .yaml в tools/host/hab/ (по образцу hab_bootloader_*.yaml: - │ │ startAddress=0x60000000, ivtOffset=0x1000, initialLoadSize=0x2000, - │ │ family=mimxrt1050, + DCDFilePath: ../dcd/dcd.bin если use_dcd) - │ ├── uv run nxpimage hab export --force -c -o , - │ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath - │ │ резолвится от этой директории, как в build.just) - │ └── стриминг stdout nxpimage в progress_cb (не только logger.debug — - │ иначе во время сборки лог FlashScreen выглядит «зависшим») - └── flash_usb.py --bin-path --fcb-path tools/host/dcd/{fcb_variant}_fdcb.bin - (временный .yaml и собранный HAB-образ удаляются после прошивки) +└── _flash_custom() +├── _build_custom_hab(raw_bin, use_dcd, progress_cb) +│ └── flash_backend.build_custom_hab() — in-process spsdk API: +│ Config (family=mimxrt1050, startAddress=0x60000000, +│ ivtOffset=0x1000, initialLoadSize=0x2000, +│ + DCDFilePath, если use_dcd) → HabImage.export() +└── _run_flash_op(flash_backend.flash, hab_bin, fcb_path=...) +временный HAB-образ удаляется после прошивки +(finally: shutil.rmtree(hab_bin.parent)) ``` -`dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не -зависит от того, что именно исполняется), поэтому просто константный путь, -без вариантов. +`dcd/dcd.bin` (`tools/host/dcd/dcd.bin`) — один и тот же файл независимо от +проекта (SEMC/SDRAM-init не зависит от того, что именно исполняется), простой +константный путь, без вариантов. Резолвится через `flash_backend._host_dcd_dir()` +— двухрежимный (dev/frozen), см. §14. -### 8.4 `flash_usb.py` — явная запись FCB вместо auto-config +### 8.4 Явная запись FCB вместо auto-config -```python -def write_fcb_explicit(fcb_path: Path) -> None: - """write-memory 0x60000000 — буквальная запись 512-байтного - FCB-блоба (tag 'FCFB'), а не magic option word 0xF000000F. - Обязателен для кастомных бинарей — auto-config Flashloader проверен - только для W25Q128.""" -``` +`flash_backend.py::write_fcb_explicit()` — `write_memory(0x60000000, fcb_bin)`, +буквальная запись 512-байтного FCB-блоба (tag `FCFB`), а не magic option word +`0xF000000F`. Обязателен для кастомных бинарей — auto-config Flashloader +проверен только для W25Q128 (см. §Известные открытые вопросы). -Активируется флагом `--fcb-path` (только вместе с `--bin-path`). Штатный -`--firmware`-путь (три сборки из `BUILD_DIR`) не тронут: без `--fcb-path` -поведение идентично тому, что было до этой доработки. - -Заодно увеличен таймаут `blhost` для `flash-erase-all` (chip erase) — -`-t 200000` вместо дефолтного: W25Q512 стирается заметно дольше W25Q128, -дефолтного таймаута `blhost` не хватало. `flash-erase-region` (стирание -пары секторов под FCB+HAB при обычной прошивке) не трогали — там масштаб -на порядки меньше, дефолта достаточно независимо от чипа. +Штатный путь (`firmware_test`/`bootloader`/`app` из `build//`) не +затрагивается — использует auto-config, как и раньше. ### 8.5 UI (`flash.py`) @@ -348,6 +359,13 @@ def write_fcb_explicit(fcb_path: Path) -> None: дополнительно защищён `min-height: 6` — лог гарантированно виден даже в худшем случае. +**Троттлинг лога:** прогресс-бар обновляется на каждом +событии `FlashProgress`, но `#flash-log` для фазы `write` пишет только при +пересечении 10%-границы — без этого запись HAB-образа даёт ~135 строк в лог +на одну прошивку. Первая строка фазы (`"Запись <имя> (<размер> байт)"`) всегда +проходит; остальные фазы (`configure`/`erase`/`fcb`/`reset`/`error`) логируются +без троттлинга — их и так немного. + --- ## 9. Архитектура экранов @@ -372,7 +390,7 @@ graph TB subgraph Clients["Клиенты"] FC["FirmwareClient"] M5["M5Client"] - FL["Flasher"] + FL["Flasher\n(async) + flash_backend\n(spsdk in-process)"] end WS -->|"DeviceDetected(FLASHING)"| FS @@ -411,7 +429,7 @@ sequenceDiagram OP->>TUI: запустить service_tui TUI->>WS: push_screen() - WS->>WS: pyusb poll каждые 1.5 с + WS->>WS: USB poll каждые 1.5 с OP->>FW: подключить плату USB WS->>TUI: DeviceDetected(DIAGNOSING) @@ -471,7 +489,7 @@ sequenceDiagram --- -## 11. Версионирование firmware +## 11. Версионирование firmware и TUI `firmware_test` версионируется через CMake (`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через @@ -484,7 +502,10 @@ sequenceDiagram отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из `pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata` сознательно не используется — проект не ставится как пакет -(`tool.uv.package = false`), метаданных может не быть. +(`tool.uv.package = false`), метаданных может не быть. Резолв +`Path(__file__).resolve().parents[2] / "pyproject.toml"` одинаково корректен +в dev и frozen (относительный от модуля, а не абсолютный) — при условии, что +`service_tui.spec` кладёт `pyproject.toml` в корень бандла (см. §14). --- @@ -523,7 +544,9 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл `mount_all()`. - **`CSS_PATH` резолвится относительно файла класса**, не относительно корня проекта — постоянно расходится при рефакторинге структуры. Решение: один - `CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. + `CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. Во frozen + дополнительно требует, чтобы `app.tcss` физически лежал в бандле по тому же + относительному пути (см. §14). - **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает ширину по умолчанию равную длине заголовка — длинный контент обрезается независимо от `height` строки. Нужно использовать `add_column(label, @@ -547,14 +570,103 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл --- +## 14. Упаковка + +### 14.1 Структура бандла + +```bash +service-tui-vX.Y.Z-/ +├── service_tui[.exe] +├── _internal/ +│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data +│ └── ... ← рантайм PyInstaller, libusbsio (из Analysis) +├── firmware/ +│ └── /firmware_test_hab.bin ← копируется post-build +└── custom_binaries/ ← пустая, для оператора +``` + +Два разных механизма наполнения — не взаимозаменяемы: + +- **`_internal/data/`** — через `datas` в `service_tui.spec` + (`collect_data_files("spsdk")` + `tools/host/dcd/*.bin`). Резолвится в + рантайме через `sys._MEIPASS` (для onedir `_MEIPASS` == `_internal/`). +- **`firmware/`** — PyInstaller `datas` физически не может положить файл + вне `_internal/`, поэтому это отдельный **post-build copy-шаг** в + `just host::package-tui` (не часть `.spec`), копирующий `build//*_hab.bin` + в бандл. Резолвится в рантайме через `Path(sys.executable).resolve().parent` + (сиблинг exe, не `_MEIPASS`) — сознательный выбор: HAB-образы должны быть + легко заменяемы без пересборки бандла. +- **`custom_binaries/`** — создаётся дважды, независимо: приложением само + при первом запуске (`flasher.py::_resolve_custom_binaries_dir()`, + `mkdir(exist_ok=True)`) и заодно явно в `package-tui` (`mkdir -p` перед + финальным переименованием) — избыточно, но безвредно, бандл выглядит + «полным» ещё до первого запуска. + +### 14.2 Двухрежимный резолв путей (`flash_backend.py`) + +Все функции, отдающие пути к data-файлам, различают dev/frozen: + +| Функция | Dev | Frozen | +| -------------------------------------------------------------------- | -------------------------------- | ------------------------------------------- | +| `firmware_hab_path()` | `BUILD_DIR`/`build//` | `sys.executable.parent / "firmware"` | +| `_host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS / "data"` | +| `flashloader_bin_path()` / `real_dcd_bin_path()` / `fcb_blob_path()` | производные от `_host_dcd_dir()` | | +| `_resolve_custom_binaries_dir()` (`flasher.py`) | рядом с `main.py` | `sys.executable.parent / "custom_binaries"` | + +`main.py::_setup_logging()` и `.env`-загрузка тоже различают режимы: +лог-файл во frozen пишется рядом с exe (не внутрь `_internal/`); `.env` во +frozen не подгружается вообще (frozen-сборка работает на fallback-константах +в коде, не полагаясь на файл, которого в бандле нет). + +### 14.3 `service_tui.spec` — сборка (важные детали) + +- **onedir, не onefile** — onefile ощутимо медленнее стартует (распаковка во + временную директорию при каждом запуске). +- **`collect_data_files("spsdk")`** — обязателен, не перестраховка: ~380 + файлов (`data/devices/*/database.yaml` и т.п.), которые реально резолвит + `HabImage`/`Config` для `family=mimxrt1050`. +- **`collect_dynamic_libs("libusbsio")`** — заберёт бинарники **всех** + поддерживаемых платформ (`bin/osx_arm64/`, `bin/x64/`, `bin/linux_*` и + т.д. — `rglob` без фильтра по текущей ОС). Не баг: сама `libusbsio.py` + резолвит нужный файл в рантайме по `platform.system()`/`platform.machine()`, + лишние платформы просто раздувают бандл. При необходимости можно + отфильтровать под текущую ОС отдельно. +- **`hiddenimports=["app", "app.app", "app.screens", "app.widgets"]`** — + явная подстраховка из-за отсутствия `__init__.py` в `app/` (см. §1). + Современный PyInstaller обычно справляется и без этого через анализ + импортов из `main.py`, но цена перестраховки нулевая. +- **`upx=False`** — сознательно, не дефолт PyInstaller: UPX-паковка вместе + с нативными HID-либами (libusbsio) — известный источник проблем с + загрузкой. + +### 14.4 Известные грабли упаковки + +- **Windows: `mv`/`rm -rf` в post-build шаге может упасть с + `Permission denied`**, если целевая директория из предыдущей сборки ещё + содержит заблокированный файл (например, `service_tui.exe` от прошлого + запуска, не закрытый перед повторной упаковкой, либо антивирус временно + удерживает хендл на свежесозданном `.exe`). Симптом: сообщение об ошибке + показывает путь **вложенным** (`dist/service-tui-vX.Y.Z-windows/service_tui`) + — это Unix-семантика `mv` в существующую директорию, сигнал, что `rm -rf` + не до конца очистил цель. Лечится закрытием запущенного exe перед повторной + упаковкой. +- **`just` + bash-shebang рецепты на Windows** — на некоторых машинах поиск + `bash` через PATH может резолвиться в `C:\Windows\System32\bash.exe` + (WSL-заглушка) вместо Git Bash, если WSL сконфигурирован некорректно — + проявляется как `WSL (...) ERROR: execve(/bin/bash) failed`. Специфично + для конкретной машины/PATH, не для рецепта — решается на уровне окружения + (порядок PATH, состояние WSL), не в `Justfile`. + +--- + ## Известные открытые вопросы -- **Release-сборка firmware нестабильна** (медленное мигание — подозрение на - проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно - форсирует Debug через `FIRMWARE_BUILD_TYPE`. +- **Release-сборка firmware нестабильна** : работает только с оптимизацией уровня O1 - **`tools/shared/m5_agent.py`** — сознательно не делался: pytest HIL-окружение и TUI используют независимые M5-клиенты, признано правильным - архитектурным решением, а не техдолгом. + архитектурным решением, а не техдолгом. (Устаревшая `just host::service-build` + ссылается на несуществующий `tools/shared/` через `--add-data` — рецепт, + скорее всего, нерабочий, кандидат на удаление в пользу `package-tui`.) - Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и «копирование UID с экрана» — отложены, не начаты. - **Массовое программирование** — решено НЕ делать авто-прошивку по факту @@ -564,7 +676,6 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл идентифицировать по UID. - **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили не полагаться на него вообще, для кастомных бинарей FCB всегда пишется - явно (`--fcb-path`, см. §8.4). Остаётся не до конца понятым, работает ли + явно (§8.4). Остаётся не до конца понятым, работает ли `configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос снят с повестки архитектурным решением, а не исследован до конца. - \ No newline at end of file diff --git a/tools/production/docs/DEV_PLAN.md b/tools/production/docs/DEV_PLAN.md new file mode 100644 index 0000000..cbad137 --- /dev/null +++ b/tools/production/docs/DEV_PLAN.md @@ -0,0 +1,115 @@ +# service-tui — Единый мастер-план разработки и релиза (Master Plan v1.0) + +> **Статус документа:** Консолидированный рабочий документ на основе планов миграции на монолит (V4), USB-кроссплатформенности, верификации прошивки и дорожной карты выпуска версии v1.0. + +--- + +## 1. Контекст проекта и целевая архитектура + +**Цель прошивки (`firmware_test`):** Полная диагностика платы **MIMXRT1052CVJ5B** на сервисе и производстве (обработка возвратов по рекламации). Прошивка запускается напрямую через BootROM (USB SDP) без предварительной записи загрузчика в Flash. + +**Архитектура стенда:** + +* **Основной канал:** Хост подключается к плате через один кабель USB CDC ACM (интерфейс `firmware_test`). UART/LPUART в сервисе сознательно не используются. +* **HIL-стенд (опционально):** Автоматические HIL-тесты управляются через модуль **M5StampPLC** (реле RLY1–4 + CAN-трансивер). +* **Оркестрация:** TUI-приложение на Textual одновременно координирует работу `firmware_test` (через USB CDC) и M5 (через Serial JSON-lines). + +--- + +## 2. Закрытые архитектурные решения (Не пересматривать) + +### Транспорт и парсинг протокола + +* **Единый канал:** USB CDC ACM — единственный интерфейс рантайма. Строковый парсинг без использования тяжелого cJSON (через поиск `strstr` по ключевым полям `"type"` / `"cmd"`). +* **Разделение тестов:** Тесты делятся на автономные (`requires_hil=false`) и стендовые (`requires_hil=true`). Если M5StampPLC не обнаружен на шине, TUI автоматически делает HIL-тесты недоступными для выбора (окрашивает в серый цвет) и исключает их из группового запуска. +* **Динамический реестр (`list_tests`):** Плата сама отдает список поддерживаемых тестов с метаданными. TUI строит интерфейс динамически и не хардкодит список тестов внутри себя. Порядок выполнения при `run_selected` всегда определяется реестром таргета, а не порядком ID в запросе хоста. + +### Аппаратная интеграция и особенности NXP BSP + +* **SDRAM и DCD:** Контроллер SEMC инициализируется блоком DCD до передачи управления в `main()`. Функция `bsp_sdram_init()` выполняет исключительно верификацию стабильности памяти. +* **QSPI Flash в ITCM:** Функции работы с QSPI выполняются из быстрой памяти ITCM с использованием макросов `AT_QUICKACCESS_SECTION_CODE` и инициализации через `__STARTUP_INITIALIZE_RAMFUNCTION`. +* **W25Q256/512:** Используются выделенные 4-байтные опкоды чтения/записи/стирания без перевода чипа в глобальный 4-байтный режим (команда `0xB7`). +* **MQS Аудио:** Периферия требует стерео PCM16 буфера (SAI3 + eDMA канал 0). На плате выведен только правый канал (`MQS_RIGHT`), поэтому буфер формируется как стерео с идентичными данными L и R. +* **Порядок включения MQS:** Сначала запускается усилитель LM4875M (`bsp_mqs_amp_init()`), далее выдерживается пауза 300 мс для заряда конденсаторов C103/C105, и только потом инициализируется ядро MQS. Нарушение порядка приводит к громкому щелчку или отсутствию звука. Использование `bsp_mqs_play()` реализовано асинхронно с поллингом USB CDC во избежание голодания интерфейса. +* **PWMChannelEnable (SDK ≥ 2.13):** Флаг `pwmchannelenable` в структуре `pwm_signal_param_t` обязательно выставляется в `true`, иначе функция `PWM_SetupPwm()` не откроет выход `OUTEN`, что маскируется отладчиком и воспроизводится только при «холодном» сбросе платы. +* **ERRATA 50235 (FlexCAN + USB):** Макрос `FLEXCAN_Init()` вызывает `assert` на тактирование LPUART1 (`CCM_CCGR5_CG12`). Если после инициализации USB CDC этот гейт закрыт — плата уходит в `HardFault`. Решение: принудительный вызов `CLOCK_EnableClock(kCLOCK_Lpuart1)` перед инициализацией CAN. +* **Синхронное чтение оптовходов:** Внедрена функция `bsp_opto_force_read()` для прямого чтения состояния пинов без дебаунса, что устраняет race condition, возникающий из-за дребезга контактов реле на HIL-стенде. + +### Монолитная архитектура TUI (V4) и кроссплатформенность + +* **Отказ от Nuitka и venv:** Упаковка приложения выполняется исключительно штатными средствами PyInstaller в один самодостаточный каталог (`onedir`), без развертывания виртуального окружения Python на целевой машине инженера. +* **In-process spsdk:** Вся работа с BootROM SDP и McuBoot переведена на прямое использование Python API пакета `spsdk==3.7.0`. Скрипт `flash_usb.py` полностью исключен из production-цепочки и оставлен разработчикам как инструмент автоматизации в `Justfile`. +* **Нативный USB-детект без Zadig:** Устройства в режимах SDP (`1FC9:0130`) и Flashloader (`15A2:0073`) определяются как HID-устройства методами `SdpUSBInterface.scan()` и `MbootUSBInterface.scan()`. Обмен идет через системную библиотеку `libusbsio`, что полностью устраняет необходимость использования утилиты **Zadig** (WinUSB) на ОС Windows. +* **Резолв Serial-портов:** Осуществляется рантайм-поиск по VID:PID через `serial.tools.list_ports`. Явные пути к портам в `.env` используются только в качестве оверрайда (escape hatch). Платформа macOS использует неблокирующие callout-устройства `/dev/cu.usbmodem*`. +* **«Липкий» выбор (`FlashPreset`):** Выбор оператора (файл прошивки, тип памяти платы, тумблер DCD) кэшируется в контексте процесса. При прошивке партии одинаковых плат настройки подставляются автоматически, оператору достаточно нажать «Загрузить». +* **Каркас AppFrame:** Все экраны оборачиваются в фиксированный CSS-контейнер `AppFrame` (макс. размер `112x35`). Это гарантирует визуальную консистентность интерфейса и устраняет критический краш Textual 8.x при mouse drag. + +--- + +## 3. Итоговая матрица тестов прошивки + +| ID | Название теста | Critical | Требует HIL | Тип выполнения | Драйвер BSP | +| :-------- | :----------------- | :------: | :---------: | :--------------------------------- | :--------------- | +| `sdram` | SDRAM 32 MB | ✅ | ❌ | Автономный (self) | `bsp_sdram` | +| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Автономный (self) | `bsp_qspi_flash` | +| `usd` | microSD (SDIO) | ❌ | ❌ | Интерактивный | `bsp_sd` + FatFS | +| `display` | TFT Display RGB888 | ❌ | ❌ | Интерактивный (4 цвета, 2 ротации) | `bsp_display` | +| `buttons` | Test Buttons 1/2 | ❌ | ❌ | Интерактивный (физический клик) | `bsp_button` | +| `mqs` | MQS Audio Out | ❌ | ❌ | Интерактивный (тон ~4 сек) | `bsp_mqs` | +| `can` | CAN loopback | ❌ | ✅ | Стендовый HIL (M5 CAN RX/TX) | `bsp_can` | +| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | Стендовый HIL (M5 реле 2, 3, 4) | `bsp_opto` | + +*Примечание: Тесты `uart_ttl` и `uart_iso` удалены из реестра как избыточные для сервисного применения.* + +--- + +## 4. Дорожная карта до релиза v1.0 (Текущие фазы) + +### Фаза 4a — Добор типизации обрыва USB + +* **Цель:** Перехват всех физических отключений кабеля во время операций и вывод унифицированного сообщения *«Соединение с платой потеряно»* вместо необработанных исключений. +* **Реализация:** + 1. Расширить блоки `try-except` в `flash_backend.py` (`load_flashloader`, `flash`, `erase_chip`), обрабатывая кортеж исключений `(SPSDKConnectionError, SPSDKTimeoutError)`. + 2. Внедрить «Вариант B» для долгих операций (например, `flash_erase_all`), которые при таймауте возвращают `False` без генерации исключения: в случае `ok == False` вызывать мгновенный `detect_sdp()`. Если устройство исчезло с шины — поднимать `ConnectionLostError(connection_lost=True)`. Любой сбой самой проверки трактовать как обрыв связи. +* **Критерий успеха (Гейт):** Выдергивание кабеля во время стирания или записи вызывает корректную реакцию экрана `FlashScreen`, кнопки разблокируются, лог информирует об обрыве, а повторное подключение позволяет продолжить работу без перезапуска TUI. + +### Фаза 4b — Оптимизация логирования и троттлинг + +* **Цель:** Исключение избыточных HID-дампов из основного лога и разгрузка текстового виджета Textual. +* **Реализация:** + 1. В `main.py` установить глобальный уровень логов `INFO`. Сторонние логгеры (`spsdk`, `libusbsio`, подмодули протоколов bulk) принудительно перевести в режим `WARNING`. Полный дамп активировать только при передаче переменной окружения `SERVICE_LOG_LEVEL=DEBUG`. + 2. Внедрить шаг троттлинга в `flash.py::_on_progress`. Значения в графический прогресс-бар отправлять без задержек (для плавности), а текстовые записи фазы `write` отправлять в виджет `#flash-log` только при пересечении шага в **10%** (0%, 10%, 20%... 100%). +* **Критерий успеха (Гейт):** Лог одной сессии прошивки сокращается со ~135 строк до ~10. Отсутствует визуальное замедление интерфейса. + +### Фаза 5 — Упаковка через PyInstaller и полировка UI + +* **Цель:** Создание бинарного дистрибутива под целевые ОС (Windows, macOS). +* **Реализация:** + 1. Написать конфигурационный файл `service_tui.spec`. Использовать директиву `collect_dynamic_libs("libusbsio")` для копирования нативных библиотек HID-транспорта под текущую ОС. Библиотеки `pyusb` и `libusb-1.0` исключить из сборки. + 2. Настроить секцию `datas` для переноса файлов `tools/host/dcd/` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) во внутреннюю папку бандла `data/`. Двухрежимный резолвер путей в коде должен прозрачно переключаться на `sys._MEIPASS` при `getattr(sys, 'frozen', False)`. + 3. Скопировать стабильный отладочный образ `firmware_test_hab.bin` (Debug) в директорию `firmware/` на одном уровне с исполняемым файлом, обеспечивая возможность его замены инженерами без пересборки бандла. + 4. Добавить кнопку «✕ Выйти из приложения» на стартовый экран `WaitingScreen` с привязкой к методу `self.app.exit()`. +* **Критерий успеха (Гейт):** На чистой машине (без установленного Python, uv и привязанных через Zadig драйверов) дистрибутив запускается, успешно определяет плату в SDP-режиме, загружает Flashloader, выполняет Chip Erase и зашивает диагностическую прошивку. + +### Фаза 6 — Документация, CHANGELOG и выпуск релиза + +* **Цель:** Финализация кодовой базы и документации. +* **Реализация:** + 1. Выполнить тотальную зачистку (`grep-cleanup`) исходного кода. Удалить отладочные комментарии, неактуальные упоминания вызовов через `subprocess` и `uv run`. Актуализировать docstrings модулей `flash_backend.py` и `flasher.py`. + 2. Сформировать финальный `CHANGELOG.md`, зафиксировав переход на монолит V4, нативную обработку ошибок USB-шины и отказ от Zadig. + 3. Исключить инструкции по настройке Zadig из руководств `README.md` и `HOW_TO_FLASH.md`. Добавить описание ограничений первой версии (строго одна плата на стенде одновременно). + 4. Создать релизный тег в Git, соответствующий текущему значению версии в `pyproject.toml`. + +--- + +## 5. Бэклог и пост-релизные задачи (Версии v1.1+) + +Задачи, согласованные к реализации, но вынесенные за рамки стабильного релиза v1.0: + +1. **Этап 7 — Provisioning платы:** + * Считывание уникального аппаратного идентификатора кристалла `OCOTP_UNIQUE_ID` средствами SDK-модуля `fsl_ocotp`. + * Передача события формата `{"type":"provision_ready","chip_uid":"..."}` на хост по завершении матрицы тестов. + * Ожидание подтверждения `provision_ack` от TUI и последующая фиксация статуса успешного прохождения в первом секторе Flash-памяти за пределами исполняемой зоны XIP (проработка логики защиты от повторной перезаписи). +2. **Экспорт результатов (POST-1):** Создание обработчика для автоматической выгрузки результатов диагностики в структурированный JSON-файл с жесткой привязкой к считанному UID микроконтроллера. +3. **UID буфер обмена:** Реализация механизма копирования или выделения UID контроллера напрямую с экрана терминала `DiagScreen`. +4. **Циклический прогон тестов:** Добавление тумблера «Циклический режим» для непрерывного фонового тестирования неинтерактивных узлов платы (SDRAM, QSPI Flash, CAN, Opto) с целью выявления плавающих аппаратных дефектов и температурной нестабильности элементов. diff --git a/tools/production/docs/INITIAL_PLAN.md b/tools/production/docs/INITIAL_PLAN.md deleted file mode 100644 index 7fa9d46..0000000 --- a/tools/production/docs/INITIAL_PLAN.md +++ /dev/null @@ -1,488 +0,0 @@ -# firmware_test — План разработки - -> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified). - ---- - -## Контекст проекта - -**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации). -Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика. - -**Стенд:** - -- Хост подключается через USB CDC ACM — единственный канал firmware_test -- HIL-тесты управляются через M5StampPLC (опционально) -- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно - ---- - -## Текущий статус - -| Компонент | Статус | Примечание | -| ------------------------------ | ------ | ------------------------------------------------ | -| `bsp_usb_cdc` | ✅ | HIL тест пройден | -| firmware_test скелет | ✅ | `main.c` + `cli.c` | -| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | -| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | -| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | -| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | -| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | -| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | -| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified | -| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified | -| `test_opto` | ✅ | Этап 6б, hardware-verified | -| `test_can` | ✅ | Этап 6в, hardware-verified | -| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | -| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | -| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | -| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified | -| Provisioning | ⬜ | Этап 7 | -| TUI сервисного инженера | ⬜ | Этап 8 | - ---- - -## Матрица тестов — итоговая - -| ID | Название | Critical | HIL | Тип | BSP | Статус | -| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ | -| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | -| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | -| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | -| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | -| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | -| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ | -| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | - -**Убранные тесты (закрытые решения):** - -- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется -- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно - ---- - -## Закрытые архитектурные решения - -> Не пересматривать без явного запроса. - -### Этапы 1–5 (ранее зафиксированные) - -- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test. -- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`. -- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует. -- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`. -- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7). -- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`. -- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL. -- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP. - -### Этап 6 (новые решения) - -- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL). - TUI фильтрует HIL-тесты если M5StampPLC не подключён. -- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста. - TUI строит UI динамически, не хардкодит список тестов. -- **`run_selected`:** запуск произвольного подмножества тестов по списку ID. - Порядок выполнения — как в реестре таргета, не как в запросе. - Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI. -- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request` - от HIL-теста TUI командует M5, получает результат, отправляет confirm. -- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты - недоступны (серые в UI, не входят в `run_selected`). -- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. -- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен - один канал. Буфер всегда стерео (L+R идентичны). -- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая), - `confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL. - `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`. -- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`. - Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M. - Нарушение порядка приводит к щелчку при старте или отсутствию звука. -- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking), - параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с. -- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно - выставлять в `true`. При инициализации через designated initializers без явного - указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит - на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого - прогона). Воспроизводится только при cold reset. -- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на - `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate - может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` - перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым — - закрывать не нужно, LPUART1 тактируется с минимальным потреблением. - `bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`. -- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при - старте, может быть пропущено). Проверяет живость через `ping → pong`. -- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина - без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется - в `test_opto.c` после settle — обходит race condition когда чётное число ISR - при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`. -- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm, - но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`. -- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`. - Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`, - `bsp_opto_force_read()` читает финальное состояние пина напрямую. - -### Этап 8 (TUI решения) - -- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost). - Оператор сам переставляет перемычку BOOT — это ок, документируется. -- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130) - или CDC firmware_test (session_start) — и показывает соответствующий экран. -- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты, - работает в SSH-сессии, вписывается в uv-экосистему. -- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/` - и `tools/production/`. - ---- - -## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН - -### 6а — Расширение протокола ✅ - -**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md` - -#### Новая команда `list_tests` - -```json -→ {"type":"cmd","cmd":"list_tests"} -← {"type":"test_list","tests":[ - {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, - {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, - {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, - {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, - {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, - {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, - {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, - {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true} - ]} -``` - -#### Новая команда `run_selected` - -```json -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} -← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi",...} -← {"type":"test_begin","id":"display",...} -← {"type":"test_result","id":"display",...} -← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"} -``` - -Если хотя бы один ID не найден в реестре: - -```json -← {"ok":false,"error":"UNKNOWN_TEST"} -``` - -**Реализация в `test_runner.c`:** - -- Новый режим `RUNNER_MODE_SELECTED` -- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc -- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция - -### 6б — test_opto.c ✅ - -**Файл:** `firmware/test/src/tests/test_opto.c` - -6 шагов, попарно ACTIVE/INACTIVE для трёх каналов: - -| Шаг | confirm_request id | M5 действие | Проверка | -| --- | ------------------- | ----------- | -------------------------------- | -| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` | -| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` | -| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` | -| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` | -| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` | -| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` | - -- Init: `bsp_opto_init()` единым вызовом для всех каналов -- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`) -- FAIL при несоответствии: `detail = " state mismatch: expected ACTIVE got INACTIVE"` -- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг - -### 6в — test_can.c ✅ - -**Файл:** `firmware/test/src/tests/test_can.c` - -2 шага, оба направления независимо: - -**Шаг 1 — RX (M5 → таргет):** - -```bash -confirm_request("can_rx_ready") -→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF]) -→ TUI: confirm(true) -→ таргет: bsp_can_receive(&frame, 500 мс) -→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF] -→ FAIL если timeout или несовпадение -``` - -**Шаг 2 — TX (таргет → M5):** - -```bash -bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс) -confirm_request("can_tx_verify") -→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data -→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет -→ FAIL если confirmed=false или timeout -``` - -- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует -- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` - -### 6г — bsp_mqs + test_mqs.c ✅ - -**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c` - -**bsp_mqs:** - -- SAI3 + eDMA (DMA0 канал 0) + MQS периферия -- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT` -- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358 -- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`, - `bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume` - -**test_mqs:** - -- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида -- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле -- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL -- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг) -- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL` - -### 6д — HIL pytest для firmware_test ✅ - -**Файлы:** - -``` -tools/hil/conftest.py ← фикстура firmware_cdc -tools/hil/06_test_firmware_opto.py -tools/hil/06_test_firmware_can.py -``` - -**Фикстура `firmware_cdc`:** - -```python -@pytest.fixture(scope="module") -def firmware_cdc(m5): - """ - Открывает USB CDC порт firmware_test. - firmware_test уже прошит в Flash (не загружается pyOCD). - Проверяет живость через ping → pong. - """ -``` - -**`FirmwareCdcClient`** — тонкий клиент: - -- `send_cmd(cmd_dict)` — отправить JSON команду -- `wait_event(type, timeout_s)` — ждать события нужного типа -- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}` -- `run_test(id)` — запустить тест, вернуть test_result dict - -**Justfile:** - -```bash -hil-firmware-opto → pytest 06_test_firmware_opto.py -v -hil-firmware-can → pytest 06_test_firmware_can.py -v -``` - ---- - -## Этап 7 — Provisioning - -### Что нужно - -1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp` -2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary` -3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста -4. Записывать статус в Flash (первый сектор после прошивки, вне XIP) - -### BSP (предварительно) - -```c -/* bsp/provisioning/include/bsp/provisioning.h */ -bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */ -``` - -### Открытые вопросы — Этап 7 - -- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID? -- [ ] Нужна ли защита от повторного provisioning (write-once)? - ---- - -## Этап 8 — TUI сервисного инженера - -### Стек технологий - -| Компонент | Выбор | Обоснование | -| ------------- | ------------- | ----------------------------------------------------- | -| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема | -| Serial | pyserial | Уже в стеке (tools/hil) | -| Прошивка | spsdk | sdphost + blhost, уже в tools/host | -| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом | - -### Структура приложения - -```bash -tools/production/ -├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv -├── uv.lock -├── main.py ← точка входа -├── app/ -│ ├── tui.py ← Textual App, экраны, layout -│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test -│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py) -│ ├── flasher.py ← USB SDP обёртка над spsdk -│ ├── orchestrator.py ← confirm_request → M5 action → confirm response -│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses) -└── README.md - -tools/shared/ -└── m5_agent.py ← общая M5-логика для hil/ и production/ -``` - -### Два режима работы - -**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) - -``` -┌─ Прошивка платы ─────────────────────────────────┐ -│ Обнаружен BootROM (SDP режим) │ -│ │ -│ Что прошить? │ -│ ◉ firmware_test (диагностика) │ -│ ○ Production (bootloader + tft_app) │ -│ │ -│ Файл: [/path/to/firmware_test_hab.bin ···] │ -│ │ -│ [ Прошить ] │ -│ │ -│ ████████████░░░░░░ 64% Запись во Flash... │ -└────────────────────────────────────────────────────┘ -``` - -**Режим B — Диагностика** (триггер: session_start получен по CDC) - -``` -┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ -│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ -├────────────────────────────────────────────────────┤ -│ Выбор тестов: │ Результаты: │ -│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ -│ ☑ QSPI Flash │ qspi ✓ PASS │ -│ ☑ microSD │ usd ✗ FAIL │ -│ ☑ TFT Display │ mount failed: 5 │ -│ ☑ Кнопки │ display ✓ PASS │ -│ ☑ MQS Audio │ buttons ✓ PASS │ -│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │ -│ ☑ Оптовходы [HIL] │ ... │ -├────────────────────────────────────────────────────┤ -│ [ Запустить выбранные ] [ Все тесты ] │ -│ ████████████████░░░░ 80% Тест: display │ -├────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└────────────────────────────────────────────────────┘ -``` - -### Поведение confirm_request в TUI - -| Тип теста | Источник confirm | Действие TUI | -| -------------------- | ------------------ | --------------------------------------------- | -| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | -| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | - -HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt. - -### Конфигурация (.env) - -```ini -# Существующие переменные (tools/hil/.env): -HIL_VCOM_PORT=/dev/ttyACM0 -HIL_M5_PORT=/dev/ttyACM1 - -# Новые переменные для production TUI: -SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start -SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5 -FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin -PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin -PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin -``` - -### Запуск - -```bash -just host::service-tui # запустить TUI сервисного инженера -just host::service-flash # прошить без TUI (для автоматизации) -``` - -### Процесс работы сервисника - -**Диагностика (firmware_test уже в Flash):** - -```bash -1. Плата в нормальном режиме (BOOT_MOD_1 → GND) -2. Подключить USB к сервисному ПК -3. just host::service-tui → TUI обнаружил session_start → Режим B -4. Выбрать тесты → Запустить → Смотреть результаты -``` - -**Перепрошивка (нужна новая версия firmware_test или production):** - -```bash -1. Перемычка BOOT_MOD_1 → 3V3 -2. Reset, подключить USB -3. TUI обнаружил 1FC9:0130 → Режим A -4. Выбрать бинарь → Прошить -5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B -``` - ---- - -## Порядок реализации - -``` -✅ Этап 1 протокол v2 + runner -✅ Этап 2 bsp_sdram + test_sdram -✅ Этап 3 bsp_qspi_flash + test_qspi -✅ Этап 4 bsp_sd + test_usd -✅ Этап 5 display + buttons -✅ Этап 6а протокол: list_tests + run_selected -✅ Этап 6б test_opto.c + hardware верификация -✅ Этап 6в test_can.c + hardware верификация -✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) -✅ Этап 6е HIL pytest: 06_test_firmware_opto.py -✅ Этап 6ж HIL pytest: 06_test_firmware_can.py -✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация - -⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ - -⬜ Этап 8а tools/production/ скелет + models + clients -⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) -⬜ Этап 8в Экран прошивки (flasher + SDP автодетект) -⬜ Этап 8г Provisioning в TUI -⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики) - -⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру -``` - ---- - -## Зависимости между этапами - -``` -✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs) - ↓ - ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) - ↓ - ⬜ 7 (provisioning) - ↓ - ⬜ 8 (TUI) -``` \ No newline at end of file diff --git a/tools/production/docs/MONOLITH_APP_PLAN.md b/tools/production/docs/MONOLITH_APP_PLAN.md deleted file mode 100644 index 2350860..0000000 --- a/tools/production/docs/MONOLITH_APP_PLAN.md +++ /dev/null @@ -1,452 +0,0 @@ -# service-tui — миграция на монолит (V4), USB-кроссплатформенность и релиз - -> Единый рабочий документ. Объединяет и заменяет `MONOLITH_PLAN.md` -> и `USB_CROSSPLATFORM.md`; заменяет шаг 3 («Упаковка», вариант B -> с venv) в `RELEASE_PLAN.md`. Шаги 1–2 плана релиза (merge, CHANGELOG) -> выполнены и не затрагиваются; шаги 4–6 переезжают в фазы 5–6. - -Ветка: `feature-tui-monolith` от `dev`. Каждая фаза = коммит(ы) с -зелёным гейтом; откат любой фазы не ломает предыдущие. - ---- - -## 1. Принятые решения (зафиксировано) - -### Р1. Nuitka снят с повестки; упаковка — PyInstaller - -Защита исходников — не требование. PyInstaller — официально -поддержанный NXP путь развёртывания spsdk. Один exe, без -`tools_host/.venv`. - -### Р2. `tools/host/flash_usb.py` НЕ трогаем - -Остаётся dev-CLI для `just host::flash*`, `incoming`, `production`. -TUI получает собственный нативный spsdk-backend. Прецедент закрыт -ранее: `tools/shared/m5_agent.py` сознательно не делался — независимые -клиенты признаны правильным решением, не техдолгом. Бонусы: -мгновенный откат на любой фазе и независимый эталон поведения для -hardware-гейтов. - -### Р3. Соответствие CLI → Python API (проверено по документации spsdk) - -| Сейчас (subprocess) | Станет (in-process) | -| ---------------------------------------------------- | ----------------------------------------------------------------- | -| `sdphost -u ... write-file 0x20001C00 ` | `SDP.write_file(0x20001C00, data)` | -| `sdphost -u ... jump-address 0x20001C00` | `SDP.jump_and_run(0x20001C00)` | -| `blhost -u ... get-property 1 0` (поллинг) | `McuBoot.get_property(PropertyTag.CURRENT_VERSION)` | -| `blhost ... fill-memory 0x2000 4 0xC0000007 word` | `McuBoot.fill_memory(0x2000, 4, 0xC0000007)` | -| `blhost ... configure-memory 9 0x2000` | `McuBoot.configure_memory(0x2000, mem_id=9)` | -| `blhost ... flash-erase-region 0x60000000 0` | `McuBoot.flash_erase_region(0x60000000, size)` | -| `blhost ... write-memory 0` | `McuBoot.write_memory(addr, data)` | -| `blhost -t 200000 ... flash-erase-all 9` | `McuBoot.flash_erase_all(mem_id=9)` + таймаут ⚠В2 | -| `blhost ... reset` | `McuBoot.reset(reopen=False)` | -| `uv run nxpimage hab export -c -o ` | `HabImage` (пакет `spsdk.image.hab`) → `.export()` ⚠В1 | -| Детект SDP/Flashloader | `SdpUSBInterface.scan(...)` / `MbootUSBInterface.scan()` (см. Р7) | - -### Р4. Потоковая модель - -- `flash_backend.py` — чистый синхронный Python, **ноль** импортов - Textual/asyncio; прогресс — синхронный callback. -- Мост поток→loop живёт **внутри `Flasher`** (не в экранах): - `asyncio.to_thread(...)` + `asyncio.run_coroutine_threadsafe()`. -- Публичный API `Flasher` заморожен → `flash.py`/`waiting.py` в фазах - 1–3 не редактируются. Главный контейнер регрессии. -- Worker-поток не трогает виджеты (грабли `self._running`/ - `MessagePump` из DEV_ARCH §13 сюда не заносим). - -### Р5. Отмену операций сознательно НЕ делаем - -Как сейчас: кнопки блокируются `_set_busy`. Блокирующий USB-вызов из -потока корректно не прервать; обрыв кабеля backend обнаружит сам через -исключения spsdk — это и есть заявленный выигрыш V4 вместо -зомби-subprocess. - -### Р6. Data-файлы и временные файлы - -- Единый источник `tools/host/dcd/` (`ivt_flashloader.bin`, `dcd.bin`, - `*_fdcb.bin`) — их использует и нетронутый `flash_usb.py`. TUI - резолвит двухрежимным паттерном (dev: repo-relative; frozen: рядом - с exe через PyInstaller `datas`). Дублей блобов в репо не заводим. -- Временные HAB-файлы — в `tempfile.gettempdir()`; cwd-магия - «temp .yaml в `tools/host/hab/`» умирает вместе с subprocess (в - Python API пути абсолютные). Побочный выигрыш: frozen-бандлу не - нужна записываемая директория внутри себя. - -### Р7. Детект устройств — через spsdk, `pyusb` удаляется ⚠ пересмотр закрытого решения - -Пересматривает `_detect_usb` (pyusb) и вытекающее требование -Zadig/WinUSB из `RELEASE_PLAN.md` шаг 4. **Требует твоего явного -подтверждения** — после него считается принятым. - -Суть: SDP BootROM (`1FC9:0130`) и Flashloader (`15A2:0073`) — это -**HID**-устройства. spsdk общается с ними через libusbsio/hidapi, -которому Zadig не нужен — именно поэтому sdphost/blhost/SPT у NXP -работают на Windows из коробки. WinUSB был нужен только нашему -pyusb-детекту; привязка WinUSB к HID-устройству вдобавок *отбирает* -его у стандартного HID-стека. Требование Zadig — самонаведённое. - -Замена (фаза 1): - -- `detect_sdp()` → `SdpUSBInterface.scan(device_id="0x1FC9:0x0130")`; -- детект Flashloader → `MbootUSBInterface.scan()`; -- `detect_cdc()` → только `serial.tools.list_ports` по VID:PID - (CDC по определению виден как COM-порт; pyusb-ветка ничего не - добавляла). - -Следствия: Zadig исчезает из полевой инструкции целиком; `pyusb` и -`libusb-1.0.dll` уходят из зависимостей/бандла; вместо них в бандл -должны попасть нативные библиотеки libusbsio (гейт фазы 5). - -### Р8. Резолв serial-портов: VID:PID — идентичность, имя порта — рантайм - -Имена портов не переносимы даже в пределах одной ОС (перевоткнул в -другой USB-порт — имя изменилось: `cu.usbmodemXXXX` / `COMn` / -`ttyACMn`). Принцип: - -``` -1. Задан _PORT в окружении → использовать as-is (escape hatch). -2. Иначе list_ports по VID:PID. -3. Одно совпадение → info.device (pyserial открывает и cu.*, и COMn, - включая COM>9, без платформенных приседаний). -4. Ноль → «не найдено» (для TUI — штатное состояние WaitingScreen). -5. Несколько → первое + warning в лог; дизамбигуация по - serial_number — задел на будущее (см. О3). -``` - -Реализация — новый `tools/production/app/usb_ports.py`. В shared не -выносится (прецедент Р2): HIL-стенд стационарный, пиновка портов в -`.env` там осмысленна и остаётся как есть. - -### Р9. Пересмотр `.env` (контекст `tools/production`; HIL-блок не трогается) - -| Переменная | Судьба | -| --------------------------------------------------------------- | -------------------------------------------- | -| `BOOTROM_VID/PID`, `FLASHLOADER_VID/PID`, `SERVICE_CDC_VID/PID` | Остаются (идентичность) | -| `SERVICE_M5_VID/PID` | Добавить (сейчас M5 идентифицируется портом) | -| `HIL_USB_CDC_PORT`, `HIL_M5_PORT` (в контексте TUI) | Необязательный override | - -Принцип: production-TUI запускается на чистой машине **вообще без -`.env`** — все значения имеют fallback-константы в коде (для VID/PID -уже так). `.env` — инструмент разработчика/стенда, не артефакт рядом -с exe. - ---- - -## 2. Матрица «USB-класс × ОС» (справочная база решений Р7/Р8) - -| Устройство | VID:PID | Класс | macOS | Windows 10/11 | Linux | -| --------------------------- | ----------- | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -| BootROM SDP | `1FC9:0130` | HID | Из коробки (IOHIDFamily) | Из коробки (`hidclass`) | Из коробки; права на `hidraw` (udev) | -| Flashloader | `15A2:0073` | HID | Из коробки | Из коробки | То же | -| firmware_test | `1996:00AD` | CDC ACM | Из коробки, `/dev/cu.usbmodem*` | Из коробки с Win10 (`usbser.sys` по классу), `COMn` | Из коробки, `/dev/ttyACM*`, dialout (рецепт `setup-m5-udev` есть) | -| M5StampPLC | см. О1 | CDC или мост | см. О1 | см. О1 | — | -| `pyusb`/libusb перечисление | — | — | Работает (текущий детект) | **Не видит без WinUSB/libusbK** (задокументировано в RELEASE_PLAN) + нужен `libusb-1.0.dll` в бандле | Работает при правах | - -macOS-нюанс, снимаемый резолвером Р8 автоматически: использовать -`cu.*`, не `tty.*` (callout не ждёт DCD) — `list_ports` на macOS и -так отдаёт `cu.*`. - ---- - -## 3. Открытые вопросы (требуют ответа/измерения до соответствующей фазы) - -**О1. M5StampPLC — чем представляется хосту?** Честно: не знаю — -нативный ESP32-S3 USB CDC (VID Espressif `303A`) или мост -CH9102/CP210x. Снимается за минуту: воткнуть M5, выполнить -`python -m serial.tools.list_ports -v` → VID:PID + serial_number. -Развилка: нативный CDC → драйверы не нужны нигде, Р8 закрывает -вопрос; мост → на изолированной Windows без сети нужен один -вендорский драйвер (честная необходимость уровня ОС, в отличие от -Zadig), пункт в инструкцию сервисника + проверка на гейте 5. -**Измерение — в фазе 0.** - -**О2. Состав `firmware/` в релизе v1** — унаследован из -`RELEASE_PLAN.md`: только `firmware_test` Debug (Release нестабилен)? -`bootloader`+`app` не включаем? Нужен для фазы 5, не раньше. - -**О3. Несколько одинаковых устройств одновременно** — сознательно за -скобками v1: у SDP BootROM различающего серийника нет, UX -WaitingScreen рассчитан на одну плату. Фиксируется в README как -известное ограничение, не как долг. Подтверди, что объём согласован. - ---- - -## Фаза 0 — Спайк / де-риск (без изменений в TUI) - -Цель: подтвердить неизвестные API (⚠В1, ⚠В2, сигнатуры `scan()`) -и совместимость зависимостей — до первой строки боевого кода. - -**Файлы:** `tools/production/pyproject.toml`, `uv.lock`, -`tools/production/spike/` (временная директория, в релиз не идёт). - -1. Добавить `spsdk==3.7.0` в `tools/production`, `uv lock`/`uv sync` — - дерево (156 пакетов в `tools/host/uv.lock`) не должно конфликтовать - с `textual`/`pyserial`. -2. `spike_hab.py`: HAB из «сырого» бинарника через `HabImage` с теми же - опциями, что в `_build_custom_hab()` (`startAddress=0x60000000, - ivtOffset=0x1000, initialLoadSize=0x2000, family=mimxrt1050`; - DCD on/off). — закрывает ⚠В1. -3. `spike_flash.py`: `SdpUSBInterface.scan` → `SDP.write_file` + - `jump_and_run` → поллинг `McuBoot.get_property` → - `configure_memory`; выяснить механизм таймаута ≥200 с для - `flash_erase_all` (эквивалент `blhost -t 200000`). — закрывает ⚠В2 - и сигнатуры Р7. -4. Прогнать `spike_flash.py` на **Windows-машине без Zadig** — - дешёвая ранняя проверка Р7 (детект + HID-транспорт). -5. Измерение О1 (VID:PID/serial M5 через list_ports). - -### Гейт 0 - -- [ ] `uv lock` без конфликтов. -- [ ] **Golden-тест HAB (byte-exact):** образ из `HabImage` побайтно - равен `nxpimage hab export` с тем же конфигом, DCD on/off. - Железо не нужно. Оформить pytest'ом — остаётся навсегда как - регрессия на апгрейды spsdk. -- [ ] Железо: flashloader поднимается через Python API, - `get_property` отвечает. -- [ ] Windows без Zadig: SDP виден, flashloader грузится. -- [ ] Известен способ задать таймаут ≥200 с для erase-all. -- [ ] О1 закрыт (VID:PID зафиксирован, ветка развилки известна). -- [ ] **Стоп-условие В1:** `HabImage` не даёт byte-exact / API - непригоден → HAB остаётся subprocess-вызовом `nxpimage` - in-process; остальной монолит не страдает; фаза 3 сужается. - Решение фиксируется до старта фазы 1. - ---- - -## Фаза 1 — Backend-модуль (синхронное ядро, без UI) - -**Файлы (новые):** `tools/production/app/flash_backend.py`, -`tools/production/app/usb_ports.py`, -`tools/production/tests/test_flash_backend.py`. -**Файлы (правки):** нет — `flasher.py`, `flash.py` не трогаются. - -`flash_backend.py` — прямой перенос логики `flash_usb.py` по таблице Р3: - -- `detect_sdp()/detect_cdc()` — по Р7 (spsdk scan + list_ports), - pyusb-код не переносится; -- `load_flashloader(progress)` — идемпотентно, как сейчас («уже - запущен — пропускаем»), поллинг с тем же 10-секундным лимитом; -- `configure_flexspi()`, `write_fcb()`, `write_fcb_explicit(path)` — - 1:1 с `flash_usb.py`, включая option words `0xC0000007`/`0xF000000F` - и расчёт `erase_size` по 4K-секторам; -- `flash_image(hab_bin, fcb_path|None, progress)`, `erase_chip(progress)`; -- прогресс: `Callable[[FlashProgress], None]`, фазы — честные этапы - конвейера (`flashloader/configure/erase/fcb/write/reset`) вместо - regex-парсинга stdout. Если спайк подтвердил `progress_callback` - у записи — процент внутри `write_memory`, иначе поэтапный - (5 этапов ≈ 20% гранулярность — приемлемо); -- ошибки: доменное `FlashBackendError(phase, cause)`; внутри перехват - `SdpError`/`McuBootError`/`McuBootConnectionError`; таймаут и - «устройство пропало» различимы. - -`usb_ports.py` — резолвер Р8 (`UsbId`, `resolve_serial_port`). - -### Гейт 1 - -- [ ] Unit-тесты (без железа): мок `McuBoot`/`SDP`, сверка - последовательности команд с `flash_usb.py` как эталоном для - flash/erase/fcb-explicit; исключения → `FlashBackendError` - с корректной фазой; резолвер портов (override / одно / - ноль / несколько совпадений). -- [ ] Smoke на железе через mini-CLI (`python -m app.flash_backend`): - прошивка `firmware_test_hab.bin` (Debug), плата грузится, - текущий TUI (subprocess-версия!) видит CDC, `ping→pong`. -- [ ] Chip erase на W25Q512 укладывается в таймаут. - ---- - -## Фаза 2 — Пересадка `Flasher` на backend (async-фасад) - -**Файлы (правки):** `tools/production/app/flasher.py` — переписывается -изнутри при неизменном публичном API. -**Не трогаются:** `flash.py`, `waiting.py`, `app.py`, -`connection_watcher.py`, `models.py`. - -- `flash()/erase_chip()`: вместо `uv run ...` и `_run_cmd` — - `await asyncio.to_thread(backend..., ...)`; loop захватывается до - ухода в поток, прогресс пробрасывается через - `run_coroutine_threadsafe(progress_cb(p), loop)`. -- `detect_sdp()/detect_cdc()/list_custom_binaries()` — делегирование - в backend, сигнатуры и `@staticmethod` прежние. -- PRODUCTION-цепочка (bootloader → app при успехе) остаётся в - `Flasher.flash()`. -- Удаляются: `_run_cmd`, `_run_flash`, `_run_flash_bin`, - `_parse_progress`, `_RE_PERCENT`, `_RE_PHASE`, пути `uv`/скрипта. - `_run_flash_custom`/`_build_custom_hab` пока на subprocess - (мигрируют в фазе 3) — смешанный режим допустим, API этого не видит. -- Резолв `BUILD_DIR` HAB-образов переезжает в backend: dev — - `/build//_hab.bin`, frozen — - `sys.executable.parent / "firmware"` (схема из RELEASE_PLAN §3, - теперь без venv). - -### Гейт 2 - -- [ ] `git diff` подтверждает: `flash.py` не изменён ни на строку. -- [ ] Headless Textual-тест: FlashScreen — прогресс обновляется, - кнопки блокируются/разблокируются, `FlashDone` с корректными - полями. -- [ ] Железо: полный цикл через TUI — firmware_test → - PostFlashScreen → диагностика; PRODUCTION (два образа подряд); - chip erase. Поведение визуально эквивалентно subprocess-версии. -- [ ] `#flash-log` не «зависает» на долгих этапах. - ---- - -## Фаза 3 — Кастомные бинарники: HAB in-process + явный FCB - -**Файлы (правки):** `flash_backend.py` (+`build_custom_hab()`), -`flasher.py` (custom-путь → backend). -**Не трогается:** `flash.py` (UI custom-группы готов). - -- `build_custom_hab(raw_bin, use_dcd)`: конфиг формируется в памяти, - `DCDFilePath` — абсолютным путём через резолвер Р6; временный образ — - в системном tmp. Если сработало стоп-условие В1 — та же сигнатура, - внутри subprocess `nxpimage`; UI разницы не видит. -- `_run_flash_custom`: `build_custom_hab` → `flash_image(hab, - fcb_path=dcd/_fdcb.bin)` — вся цепочка in-process - (`write_fcb_explicit` готов с фазы 1). -- Golden-тест фазы 0 расширяется custom-кейсом (реальный - легаси-бинарник, DCD on/off). - -### Гейт 3 (повторяет чек-лист RELEASE_PLAN шага 4 по custom-пути) - -- [ ] Golden-тест HAB зелёный для custom-кейса. -- [ ] Железо W25Q128: custom, DCD off → грузится. -- [ ] Железо W25Q512: custom → грузится; якорная проверка 4-байтной - адресации (aliasing-методика) в порядке. -- [ ] Chip erase → повторная прошивка → плата живая. -- [ ] «Липкий» `FlashPreset` работает (следующая плата — выбор - подставлен). - ---- - -## Фаза 4 — Нативная обработка отвала USB + зачистка - -**Файлы (правки):** `flash_backend.py`, `flasher.py`; точечно -`flash.py`/`connection_watcher.py` — только если гейт покажет -необходимость (по умолчанию нет). - -- Обрыв посреди операции: `McuBootConnectionError`/таймауты → - `FlashBackendError(..., connection_lost=True)` → `Flasher` возвращает - `False` + финальный `FlashProgress(phase="error")` с - человекочитаемым сообщением. Схема с `ConnectionWatcherMixin` - прежняя: во время `_flashing` watcher приглушён, обрыв репортит сам - backend — то, что раньше делал subprocess, без зомби-процессов. -- Ревизия ресурсов: USB-интерфейсы закрываются в - `finally`/context-manager'ах при любом исходе (утечка HID-хэндла — - классическая причина «device busy» при повторе). -- Зачистка: следов subprocess-эры, `uv`, путей `flash_usb.py`, - `_HAB_DIR`-магии в `tools/production/` не остаётся. - -### Гейт 4 (деструктивные сценарии на железе) - -- [ ] Выдернуть USB во время `write-memory` → ошибка в TUI, возврат - на WaitingScreen, повторная вставка → повторная прошивка - успешна (порт не «занят»). -- [ ] Выдернуть во время chip erase (W25Q512) → то же; выход из - приложения чистый, подвисших потоков нет. -- [ ] Выдернуть в простое на FlashScreen → срабатывает watcher - (регрессия старого пути). -- [ ] `grep -r "flash_usb\|uv run\|subprocess\|usb.core" \ - tools/production/app/` — пусто. - ---- - -## Фаза 5 — Упаковка PyInstaller (замена шага 3 RELEASE_PLAN) - -**Файлы (новые):** `tools/production/service_tui.spec`, рецепты в -`just/ci.just` или `host.just` (по месту; имена задач согласуем -отдельно, не изобретаю). - -``` -service-tui-vX.Y.Z-/ -├── service_tui[.exe] ← PyInstaller, onedir (onefile на Windows -│ замедляет старт распаковкой — не берём) -├── _internal/ ← рантайм PyInstaller -│ └── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin -│ (datas из tools/host/dcd/), data spsdk -├── firmware/ -│ └── Debug/firmware_test_hab.bin (состав — см. О2) -└── custom_binaries/ ← пустая, создаётся и так -``` - -Ключевые пункты spec: - -- `collect_data_files("spsdk")` (+ при необходимости - `SPSDK_DATA_FOLDER` — документированный NXP механизм для frozen); -- **`collect_dynamic_libs("libusbsio")`** — нативный HID-транспорт - spsdk (следствие Р7); `libusb-1.0.*` в бандле отсутствует; -- `datas`: `tools/host/dcd/{dcd.bin, w25q128_fdcb.bin, - w25q512_fdcb.bin, ivt_flashloader.bin}` → `data/`; -- резолвер путей backend'а: frozen → `data/` рядом с exe; -- версия: `_read_app_version()` (tomllib) — `pyproject.toml` в - `datas`, проверить чтение во frozen. - -### Гейт 5 (замена шага 4 RELEASE_PLAN; Windows + macOS) - -- [ ] Чистая Windows-машина, **без Zadig, без сети, без Python/uv**: - полный полевой цикл — детект SDP → firmware_test → диагностика → - custom (128 и 512) → chip erase. -- [ ] Если О1 = мост: установка одного вендорского драйвера по - инструкции, M5-функции работают. -- [ ] То же на macOS (в ветке «мост» — проверить и там). -- [ ] Версия на WaitingScreen корректна во frozen. -- [ ] Порты резолвятся при перетыкании в другой физический USB-порт - (проверка Р8 на обеих ОС). - ---- - -## Фаза 6 — Документация, CHANGELOG, релиз (шаги 5–6 RELEASE_PLAN) - -**Файлы:** `CHANGELOG.md`; `RELEASE_PLAN.md` (закрыть шаг 3 ссылкой -сюда); `docs/DEV_ARCH.md` (§2 — убрать `subprocess uv run` из -диаграммы, §8.3 — новый конвейер); `HOW_TO_FLASH.md`; `README.md` -`tools/production`; `.env.example` (по Р9). - -- CHANGELOG: монолит (flash_backend, отказ от venv/subprocess), - нативный детект без Zadig, кроссплатформенный резолв портов, - нативная обработка отвала USB, упаковка одним exe. -- Zadig-инструкция в доки **не добавляется** (RELEASE_PLAN планировал - добавить — отменено по Р7); при ветке О1-«мост» — добавляется - инструкция по одному вендорскому драйверу. -- Зафиксировать разделение: `flash_usb.py` — dev-CLI (just-рецепты), - `flash_backend.py` — production-TUI; независимые реализации по - прецеденту M5-клиентов (Р2). -- Golden-тест HAB — обязательный при апгрейде spsdk. -- Ограничение «одна плата на столе» (О3) — в README. -- Тег релиза = версия из `pyproject.toml`. - -### Гейт 6 - -- [ ] Документация синхронизирована (железо подтверждено гейтами 3–5). -- [ ] `just host::flash*`, `incoming`, `production` работают как - раньше — регрессия dev-пути. -- [ ] Релизный артефакт собран из тега; чек-лист гейта 5 повторён на - релизном бинаре. - ---- - -## Сводка рисков и trade-offs - -| Риск / trade-off | Фаза | Митигация / цена | -| --------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | -| `HabImage` в 3.7.0 не byte-exact / непригоден | 0 | Стоп-условие В1: HAB остаётся subprocess in-process; монолит не страдает | -| Таймаут `flash_erase_all` (W25Q512) | 0 | Спайк ⚠В2 до боевого кода | -| Сигнатуры `scan()` / поведение HID-скана на Windows | 0 | Спайк на Windows без Zadig | -| Гранулярность прогресса хуже stdout-парсинга | 1–2 | Поэтапный прогресс (5 фаз); честнее текущего — `flash_usb.py` процентов фактически не печатает, бар и сейчас живёт на фазах | -| Утечка USB-хэндла → «device busy» | 4 | Context-managers + деструктивный гейт 4 | -| PyInstaller: data/hooks spsdk, нативные libusbsio | 5 | Документированный NXP путь + `SPSDK_DATA_FOLDER` + `collect_dynamic_libs`; риск смещён на CI, не в поле | -| Textual-грабли при потоках | 2 | Мост изолирован во `Flasher`; экраны не трогаются до фазы 4 | -| Отказ от отмены операций (Р5) | — | Цена: UX как сегодня (ждать до конца/обрыва); выигрыш: нет некорректного прерывания USB-транзакций | -| Первое-из-нескольких при дубликатах устройств (О3) | — | Warning в лог; serial_number-дизамбигуация — задел | - -## Порядок ревью - -По практике проекта — один файл за раз; полные файлы там, где файл -новый или переписывается целиком (`flash_backend.py`, `usb_ports.py`, -`flasher.py`), unified diff — для точечных правок (`pyproject.toml`, -документация, `.env.example`). - -**Старт (после подтверждения Р7 и, желательно, измерения О1):** -фаза 0 — diff `pyproject.toml` + `spike_hab.py` + `spike_flash.py`. diff --git a/tools/production/docs/RELEASE_PLAN_v1.md b/tools/production/docs/RELEASE_PLAN_v1.md deleted file mode 100644 index b99d09d..0000000 --- a/tools/production/docs/RELEASE_PLAN_v1.md +++ /dev/null @@ -1,128 +0,0 @@ -# service-tui — план первого релиза - -> Не путать с `TUI_PLAN.md` (бэклог фич UI — экспорт результатов в JSON, -> копирование UID) — этот документ про выпуск текущей версии, а не про -> новые возможности. Если в проекте уже есть `PLAN.md`/`TUI_PLAN.md` — -> имеет смысл свести их в один файл; пока создан отдельно, чтобы не -> перезаписать вслепую то, чего я не видел. - -Ветка разработки: `feature-tui-python` → `dev`. Особенностей процесса -(review/CI-гейты) нет. - ---- - -## 1. Слияние `feature-tui-python` → `dev` - -Ничем не заблокировано — документация синхронизирована, полевые прогоны -идут без блокирующих багов. Выполняется отдельно от вырезки релиза (шаг 6). - -**Статус:** готово. - ---- - -## 2. `CHANGELOG.md` - -Запись по всему циклу этого треда: custom-бинарники (`custom_binaries/`, -`FcbVariant`, явная запись FCB вместо auto-config), sticky-выбор -(`FlashPreset`), CSS-фикс переполнения `FlashScreen`, увеличенный таймаут -`flash-erase-all` для W25Q512. - -**Статус:** готово. - ---- - -## 3. Упаковка приложения - -**Решение:** вариант **B** — предсобранный `tools_host/` (venv со spsdk) -кладётся рядом с exe в релизном архиве, без необходимости `uv`/сети на -машине сервисника. - -### Layout архива - -``` -service-tui-vX.Y.Z-/ -├── service_tui[.exe] ← PyInstaller -├── firmware/ -│ └── Debug/firmware_test_hab.bin -├── custom_binaries/ ← пустая, создаётся и так -└── tools_host/ ← предсобранный venv + flash_usb.py, dcd/ - ├── .venv/ - ├── flash_usb.py - ├── dcd/ - └── pyproject.toml / uv.lock -``` - -### Код — `flasher.py` - -Два режима резолва пути/интерпретатора `flash_usb.py`, по аналогии с уже -реализованным резолвом `custom_binaries/`: - -```python -if getattr(sys, "frozen", False): - _HOST_TOOLS_DIR = Path(sys.executable).resolve().parent / "tools_host" - _PYTHON = _HOST_TOOLS_DIR / ".venv" / ( - "Scripts/python.exe" if os.name == "nt" else "bin/python" - ) - cmd = [str(_PYTHON), str(_HOST_TOOLS_DIR / "flash_usb.py"), ...] -else: - _HOST_TOOLS_DIR = Path(__file__).parents[3] / "tools" / "host" - cmd = ["uv", "run", "--directory", str(_HOST_TOOLS_DIR), "python", "flash_usb.py", ...] -``` - -`BUILD_DIR` (штатные `firmware_test`/`bootloader`/`app`) — та же логика: -frozen-режим по умолчанию резолвит `sys.executable.parent / "firmware"`, -dev-режим — как сейчас (`/build`). - -### Открытые вопросы - -- **Состав `firmware/` в v1** — предполагается только `firmware_test` - **Debug** (Release помечен нестабильным; `firmware_test` — единственный - образ, непосредственно нужный для диагностики). `bootloader`+`app` - (Production) в первый релиз не включены — требует подтверждения. -- **Переносимость venv между машинами** — если в `spsdk` есть нативные - компоненты (не чистый Python), venv, собранный на CI/машине разработчика, - может не завестись на машине сервисника с другой версией ОС/libc. - Требует проверки на шаге 4, а не предположения. - -**Статус:** не начато. - ---- - -## 4. Тестирование на отдельном хосте (Windows) - -**Известный риск (не блокер, но обязательный шаг перед раздачей):** -`pyusb` на Windows не видит устройство без явно привязанного драйвера -(WinUSB/libusbK). Без этого `Flasher.detect_sdp()` молча возвращает `False` -и `WaitingScreen` никогда не поймает плату в SDP-режиме — фолбэк на -`serial.tools.list_ports` не спасает, у SDP нет serial-порта в принципе. - -**Обязательные подготовительные действия на тестовой машине:** -1. Через **Zadig** привязать WinUSB к `1FC9:0130` (BootROM SDP) -2. Через **Zadig** привязать WinUSB к `15A2:0073` (Flashloader) -3. Это нужно занести в инструкцию для сервисника (`README.md`/`HOW_TO_FLASH.md`), - не держать только в голове — иначе на полевом Windows-ноутбуке повторится - та же засада молча. - -**Также проверить на этом шаге:** -- Переносимость `tools_host/.venv` (см. открытый вопрос шага 3) -- Полный цикл: прошивка firmware_test → диагностика → custom-бинарник (128/512) → chip erase - -**Статус:** не начато, ждёт шага 3. - ---- - -## 5. Доработки по необходимости - -Резерв на то, что найдётся на шаге 4. Содержание заранее не известно — -пункт-заглушка, наполняется по факту тестирования. - ---- - -## 6. Первый релиз - -Состав: `service-tui` (упакован по схеме шага 3) + `firmware_test` (Debug, -см. открытый вопрос шага 3). Версия TUI берётся из `pyproject.toml` -(`_read_app_version()`, уже используется на `WaitingScreen`) — тег релиза -предлагается синхронизировать с этим значением. - -**Статус:** не начато, ждёт шагов 1–5. \ No newline at end of file diff --git a/tools/production/docs/RELEASE_PLAN_v2.md b/tools/production/docs/RELEASE_PLAN_v2.md deleted file mode 100644 index 3ccee56..0000000 --- a/tools/production/docs/RELEASE_PLAN_v2.md +++ /dev/null @@ -1,467 +0,0 @@ -# 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//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-/ -├── service_tui[.exe] -├── _internal/ -│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data -│ └── ... ← рантайм PyInstaller, libusbsio -├── firmware/ -│ └── /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-` (из плана) | - ---- - -## Риски этого этапа - -| Риск | Фаза | Митигация | -| --- | --- | --- | -| `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 для точечных правок.