# service-tui: docs refactored
This commit is contained in:
parent
6d23069103
commit
c6bc0e9935
11 changed files with 404 additions and 1690 deletions
|
|
@ -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 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Особенности
|
||||
|
|
|
|||
|
|
@ -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-<os>.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-<os>/
|
||||
```
|
||||
|
||||
> 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) |
|
||||
| `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-бинаря |
|
||||
|
||||
**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает
|
||||
`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть
|
||||
инициализирован (`just host::setup-tools`).
|
||||
| `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 захватывает
|
||||
терминал.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
|
|
@ -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 <yaml> -o <out>,
|
||||
│ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath
|
||||
│ │ резолвится от этой директории, как в build.just)
|
||||
│ └── стриминг stdout nxpimage в progress_cb (не только logger.debug —
|
||||
│ иначе во время сборки лог FlashScreen выглядит «зависшим»)
|
||||
└── flash_usb.py --bin-path <hab_bin> --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 <fcb_path> — буквальная запись 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/<Type>/`) не
|
||||
затрагивается — использует 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-<os>/
|
||||
├── service_tui[.exe]
|
||||
├── _internal/
|
||||
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||
│ └── ... ← рантайм PyInstaller, libusbsio (из Analysis)
|
||||
├── firmware/
|
||||
│ └── <Type>/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/<Type>/*_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/<Type>/` | `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` для этих чипов корректно в принципе — вопрос
|
||||
снят с повестки архитектурным решением, а не исследован до конца.
|
||||
|
||||
115
tools/production/docs/DEV_PLAN.md
Normal file
115
tools/production/docs/DEV_PLAN.md
Normal file
|
|
@ -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) с целью выявления плавающих аппаратных дефектов и температурной нестабильности элементов.
|
||||
|
|
@ -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 = "<id> 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 <bin> # прошить без 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)
|
||||
```
|
||||
|
|
@ -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 <flashloader>` | `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 <size> 0` | `McuBoot.flash_erase_region(0x60000000, size)` |
|
||||
| `blhost ... write-memory <addr> <file> 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 <yaml> -o <bin>` | `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. Задан <NAME>_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 —
|
||||
`<repo>/build/<Type>/<name>_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/<variant>_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-<os>/
|
||||
├── 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`.
|
||||
|
|
@ -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-<os>/
|
||||
├── 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-режим — как сейчас (`<repo_root>/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.
|
||||
|
|
@ -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/<Type>/firmware_test_hab.bin` (Type из `.env`, О2);
|
||||
- `datas`: `pyproject.toml` (для `_read_app_version` во frozen);
|
||||
- onedir (не onefile — onefile замедляет старт распаковкой);
|
||||
- резолвер путей backend'а уже готов: frozen → `sys.executable.parent`
|
||||
(`firmware_hab_path`, `_resolve_custom_binaries_dir`).
|
||||
|
||||
Целевая структура бандла:
|
||||
|
||||
```
|
||||
service-tui-vX.Y.Z-<os>/
|
||||
├── service_tui[.exe]
|
||||
├── _internal/
|
||||
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||
│ └── ... ← рантайм PyInstaller, libusbsio
|
||||
├── firmware/
|
||||
│ └── <Type>/firmware_test_hab.bin
|
||||
└── custom_binaries/ ← пустая
|
||||
```
|
||||
|
||||
### UI-полировка (Предложение 2)
|
||||
|
||||
Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично
|
||||
`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`).
|
||||
|
||||
### Гейт 5 (Windows + macOS)
|
||||
|
||||
- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный
|
||||
полевой цикл — детект SDP → firmware_test → диагностика →
|
||||
custom (W25Q128 и W25Q512) → chip erase.
|
||||
- [ ] То же на macOS.
|
||||
- [ ] Версия на `WaitingScreen` корректна во frozen.
|
||||
- [ ] Порты резолвятся при перетыкании в другой физический USB-порт
|
||||
(проверка Р8 на обеих ОС).
|
||||
- [ ] Кнопка «Выйти» на `WaitingScreen` работает.
|
||||
- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны —
|
||||
подтверждено О1) виден во frozen-бандле.
|
||||
|
||||
---
|
||||
|
||||
## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз
|
||||
|
||||
**Цель:** синхронизировать документацию с реальностью монолита,
|
||||
провести отложенную зачистку комментариев/grep, собрать релизный
|
||||
артефакт из тега.
|
||||
|
||||
### Файлы
|
||||
|
||||
| Файл | Тип правки |
|
||||
| --- | --- |
|
||||
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) |
|
||||
| `CHANGELOG.md` | правки |
|
||||
| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap |
|
||||
| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) |
|
||||
| `HOW_TO_FLASH.md` | правки |
|
||||
| `tools/production/README.md` | правки |
|
||||
| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||
|
||||
### Содержание
|
||||
|
||||
1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход
|
||||
по всему коду — актуализировать docstring-провенансы («прямой порт
|
||||
flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита.
|
||||
Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто
|
||||
в `app/`) — либо достигается, либо остаётся осознанно как
|
||||
документация происхождения (решение по каждому вхождению).
|
||||
2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess),
|
||||
нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8),
|
||||
нативная обработка отвала USB, упаковка одним exe.
|
||||
3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план
|
||||
RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1).
|
||||
4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты),
|
||||
`flash_backend.py` — production-TUI; независимые реализации (Р2).
|
||||
5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk.
|
||||
6. **Ограничение «одна плата на столе»** (О3) — в README.
|
||||
7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как
|
||||
запланированную пост-релизную фичу.
|
||||
8. Тег релиза = версия из `pyproject.toml`.
|
||||
|
||||
### Гейт 6
|
||||
|
||||
- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5).
|
||||
- [ ] `just host::flash*`, `incoming`, `production` работают как раньше —
|
||||
регрессия dev-пути.
|
||||
- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на
|
||||
релизном бинаре.
|
||||
- [ ] POST-1 зафиксирован в бэклоге, не потерян.
|
||||
|
||||
---
|
||||
|
||||
## Сводная последовательность и зависимости
|
||||
|
||||
```
|
||||
4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1
|
||||
│ │ │ │
|
||||
│ │ │ └── доки, зачистка, тег, регрессия dev-пути
|
||||
│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting
|
||||
│ └── уровни логов + троттлинг #flash-log
|
||||
└── типизация обрыва (SPSDKTimeoutError + erase вариант B)
|
||||
|
||||
Блокеры перед фазами:
|
||||
4a: нет — старт сразу
|
||||
4b: нет — после 4a
|
||||
5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG
|
||||
6: все гейты 4a/4b/5 зелёные на железе
|
||||
```
|
||||
|
||||
## Открытые мелочи (согласовать по ходу, не блокируют старт 4a)
|
||||
|
||||
| Вопрос | Когда нужен | Предложение |
|
||||
| --- | --- | --- |
|
||||
| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) |
|
||||
| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю |
|
||||
| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-<os>` (из плана) |
|
||||
|
||||
---
|
||||
|
||||
## Риски этого этапа
|
||||
|
||||
| Риск | Фаза | Митигация |
|
||||
| --- | --- | --- |
|
||||
| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает |
|
||||
| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель |
|
||||
| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле |
|
||||
| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` |
|
||||
| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет |
|
||||
|
||||
---
|
||||
|
||||
# Приложение: работа в новом треде
|
||||
|
||||
Этот roadmap рассчитан на продолжение в **новом чате без контекста**
|
||||
предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы
|
||||
новый тред стартовал без потерь.
|
||||
|
||||
## A. Какой набор правил к чему применяется
|
||||
|
||||
Проектные правила «Role & Hardware Context» (senior embedded C, i.MX
|
||||
RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`,
|
||||
CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`).
|
||||
|
||||
**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в
|
||||
`tools/production`. Поэтому:
|
||||
|
||||
| Правило | Применимо к Python-работе roadmap? |
|
||||
| --- | --- |
|
||||
| Unified diffs, не полные переписывания | ✅ Да |
|
||||
| «Какой файл / какая функция затронуты» — первым | ✅ Да |
|
||||
| ASK при неоднозначности/противоречии | ✅ Да |
|
||||
| Не изобретать just-таски / пути / структуру | ✅ Да |
|
||||
| Проверять существующие файлы перед правкой | ✅ Да |
|
||||
| No malloc/free в драйверах и ISR | ❌ C-специфично |
|
||||
| NXP SDK HAL вместо raw-регистров | ❌ C-специфично |
|
||||
| Doxygen на public API | ❌ (Python — docstrings, уже используются) |
|
||||
| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) |
|
||||
| CMake target_compile_options | ❌ Неприменимо |
|
||||
|
||||
Когда/если roadmap коснётся C-части — C-правила снова в силе.
|
||||
|
||||
## B. Первый вопрос на старте нового треда (не потерять)
|
||||
|
||||
**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip`
|
||||
предлагается обернуть в `try/except`, и **любую ошибку самой проверки**
|
||||
(не только «устройство отсутствует») трактовать как обрыв — потому что
|
||||
проверка и так выполняется только после уже случившегося сбоя, шина
|
||||
нестабильна, и «не смог проверить» практически всегда означает «платы
|
||||
нет». Требуется явное подтверждение этой трактовки перед написанием
|
||||
кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный
|
||||
`FlashBackendError`.)
|
||||
|
||||
## C. Файлы, которые нужно предоставить — по фазам
|
||||
|
||||
Пути относительно `tools/production/`, если не указано иное. Пометка
|
||||
**[есть в этом треде]** — файл уже фигурировал и его актуальная версия
|
||||
известна; в новом треде его всё равно нужно приложить заново.
|
||||
|
||||
### Фаза 4a — типизация обрыва
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B |
|
||||
| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию |
|
||||
| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) |
|
||||
| `app/models.py` | контекст: `FlashProgress` |
|
||||
|
||||
### Фаза 4b — логи
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) |
|
||||
| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) |
|
||||
| `.env` / `.env.example` (`tools/production/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными |
|
||||
|
||||
### Фаза 5 — упаковка PyInstaller + UI
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `pyproject.toml` (`tools/production/`) | зависимости, версия, `requires-python` — база для spec |
|
||||
| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта |
|
||||
| `app/main.py` | entry point для PyInstaller |
|
||||
| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen |
|
||||
| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit |
|
||||
| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) |
|
||||
| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла |
|
||||
| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) |
|
||||
| `project_tree.txt` или `ls -R tools/production` | реальная структура пакета `app/` для spec |
|
||||
| существующий `.spec`, если уже есть | не изобретать заново |
|
||||
|
||||
### Фаза 6 — документация и релиз
|
||||
|
||||
| Файл | Зачем |
|
||||
| --- | --- |
|
||||
| `CHANGELOG.md` | дописать секцию монолита |
|
||||
| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap |
|
||||
| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) |
|
||||
| `HOW_TO_FLASH.md` | актуализировать под TUI-backend |
|
||||
| `tools/production/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI |
|
||||
| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) |
|
||||
| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) |
|
||||
|
||||
## D. Полный список актуальных файлов монолита (снимок на входе)
|
||||
|
||||
Чтобы в новом треде можно было приложить всё разом, если удобнее не
|
||||
дробить по фазам. Актуальные (пост-Фаза-4) версии:
|
||||
|
||||
```
|
||||
tools/production/
|
||||
├── pyproject.toml
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py
|
||||
│ ├── app.tcss
|
||||
│ ├── main.py (точка входа — фактически в tools/production/main.py, см. pyproject scripts)
|
||||
│ ├── models.py
|
||||
│ ├── flasher.py ← Фаза 2/4, актуальная версия
|
||||
│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест)
|
||||
│ ├── usb_ports.py ← Фаза 1
|
||||
│ ├── firmware_client.py
|
||||
│ ├── m5_client.py
|
||||
│ ├── orchestrator.py
|
||||
│ ├── boot_art.py
|
||||
│ ├── widgets.py (или widgets/)
|
||||
│ └── screens/
|
||||
│ ├── __init__.py
|
||||
│ ├── waiting.py
|
||||
│ ├── flash.py ← Фаза 4 (правлены комментарии)
|
||||
│ ├── post_flash.py
|
||||
│ ├── connection_watcher.py
|
||||
│ └── diag/
|
||||
│ ├── __init__.py
|
||||
│ ├── confirm_panel.py
|
||||
│ ├── results.py
|
||||
│ └── test_list.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ └── test_flash_backend.py ← 41 тест
|
||||
├── spike/ (Фаза 0, в релиз не идёт)
|
||||
│ ├── spike_hab.py
|
||||
│ ├── spike_flash.py
|
||||
│ └── spike_readback.py (диагностика Гейта 3, на будущее)
|
||||
└── custom_binaries/ (пустая, для оператора)
|
||||
|
||||
tools/host/ (dev-CLI, Р2 — НЕ трогается)
|
||||
├── flash_usb.py
|
||||
└── dcd/
|
||||
├── ivt_flashloader.bin
|
||||
├── dcd.bin
|
||||
├── w25q128_fdcb.bin
|
||||
└── w25q512_fdcb.bin
|
||||
```
|
||||
|
||||
> Примечание: `main.py` в `pyproject.toml` прописан как
|
||||
> `service-tui = "main:main"` — точка входа лежит в
|
||||
> `tools/production/main.py` (не в `app/`), а `app/app.py` содержит
|
||||
> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5.
|
||||
|
||||
## E. Что уже решено и не пересматривается (сводка для нового треда)
|
||||
|
||||
- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже).
|
||||
- **Р10** — erase-таймаут → вариант B (detect_sdp после False).
|
||||
- **Р11** — троттлинг лога 10%.
|
||||
- **Р12** — уровни логов + env DEBUG.
|
||||
- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны.
|
||||
- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`.
|
||||
- **О3** — одна плата на столе, ограничение v1.
|
||||
- **POST-1** — циклический прогон тестов, после релиза.
|
||||
- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py`
|
||||
меняются только там, где явно указано в roadmap.
|
||||
- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе.
|
||||
- Порядок ревью: один файл за раз, полные файлы для новых/целиком
|
||||
переписываемых, unified diff для точечных правок.
|
||||
Loading…
Reference in a new issue