# 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_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||||
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
|
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Особенности
|
## Особенности
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
# service-tui — TUI сервисного инженера
|
# service-tui — TUI сервисного инженера
|
||||||
|
|
||||||
TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе.
|
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
|
```bash
|
||||||
┌────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────┐
|
||||||
│ service_tool v0.3.0 │
|
│ service_tool vX.Y.Z │
|
||||||
│ │
|
│ │
|
||||||
│ [LOGO_ART] │
|
│ [LOGO_ART] │
|
||||||
│ │
|
│ │
|
||||||
│ Подключите плату индикатора к USB... ⠋ │
|
│ Подключите плату индикатора к USB... ⠋ │
|
||||||
|
│ │
|
||||||
|
│ [ ✕ Выйти из приложения ] │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -55,29 +56,37 @@ TUI не пытается восстановить прежнее состоян
|
||||||
│ │
|
│ │
|
||||||
│ ████████████░░░░░░ ← без числового % │
|
│ ████████████░░░░░░ ← без числового % │
|
||||||
│ ┌────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
│ │ ▶ Сборка HAB-образа (nxpimage)... │ │
|
│ │ ▶ Прошивка: firmware_test │ │
|
||||||
│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │
|
|
||||||
│ │ ... │ │
|
│ │ ... │ │
|
||||||
│ └────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────┘ │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Лог виден постоянно (не только во время прошивки), прогресс-бар — только
|
**Если после «Загрузить» появилась ошибка, а плата всё ещё видна на этом же
|
||||||
во время активной операции (скрыт в простое), без числового `%` — только
|
экране** — это ожидаемо: логическая ошибка (не найден файл, не подошёл
|
||||||
полоса и построчный лог в реальном времени. Панель выбора прошивки
|
формат) не выкидывает на экран ожидания, потому что плата физически
|
||||||
ограничена по высоте и скроллится сама, если разрастается (варианты
|
подключена. Прочитайте сообщение в логе, поправьте выбор и нажмите
|
||||||
"Другое") — лог снизу гарантированно не сжимается меньше 6 строк.
|
«Загрузить» ещё раз. На экран ожидания TUI переключает только при реальном
|
||||||
|
физическом обрыве USB.
|
||||||
|
|
||||||
**"Другое" — для бинарников, собранных не в этом репозитории.** В
|
**"Другое" — для бинарников, собранных не в этом репозитории.** В
|
||||||
`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без
|
`custom_binaries/` кладётся бинарник — сырой (код + таблица векторов, без
|
||||||
FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до `nxpimage`).
|
FCB/IVT/DCD) либо уже готовый HAB-образ, в зависимости от источника. TUI
|
||||||
TUI сама собирает из него загружаемый образ на лету:
|
сама достраивает недостающее на лету:
|
||||||
|
|
||||||
1. `nxpimage hab export` — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM")
|
1. Собирает загружаемый HAB-образ (добавляет IVT, +DCD — если включён
|
||||||
2. в Flash пишется явный FCB под выбранную память платы (не тот же
|
тумблер "Использует SDRAM")
|
||||||
|
2. В Flash пишется явный FCB под выбранную память платы (не тот же
|
||||||
auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для
|
auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для
|
||||||
W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`)
|
W25Q256/512 он ненадёжен, см. `DEV_ARCH.md §8.4`)
|
||||||
3. образ прошивается с `0x60001000`, как обычно
|
3. Образ прошивается стандартным адресом
|
||||||
|
|
||||||
|
**Нужен ли тумблер DCD — зависит от конкретного бинарника, не от того, в
|
||||||
|
каком виде он получен.** Одна и та же связка `bootloader + tft_app` не
|
||||||
|
требует DCD, а часть кастомных/легаси образов (например, старый загрузчик,
|
||||||
|
используемый на производстве) требует его независимо от формата файла. Если
|
||||||
|
не уверены, нужен ли конкретному образу DCD — уточните у того, кто его
|
||||||
|
предоставил, прежде чем прошивать.
|
||||||
|
|
||||||
**Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не
|
**Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не
|
||||||
нужно выставлять заново на каждой следующей плате: прошили одну, вынули
|
нужно выставлять заново на каждой следующей плате: прошили одну, вынули
|
||||||
|
|
@ -96,8 +105,8 @@ Production/Custom этот шаг не нужен).
|
||||||
│ Переведите плату в нормальный режим: │
|
│ Переведите плату в нормальный режим: │
|
||||||
│ BOOT_MOD_1 → GND → Reset │
|
│ BOOT_MOD_1 → GND → Reset │
|
||||||
│ │
|
│ │
|
||||||
│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │
|
|
||||||
│ Автопереход через: 40с │
|
│ Автопереход через: 40с │
|
||||||
|
│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -128,6 +137,7 @@ Production/Custom этот шаг не нужен).
|
||||||
```
|
```
|
||||||
|
|
||||||
Что важно знать:
|
Что важно знать:
|
||||||
|
|
||||||
- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать
|
- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать
|
||||||
все"/"Снять все".
|
все"/"Снять все".
|
||||||
- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена
|
- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена
|
||||||
|
|
@ -147,7 +157,7 @@ Production/Custom этот шаг не нужен).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Рабочие процессы сервисника
|
## Рабочие процессы сервисного инженера
|
||||||
|
|
||||||
### Диагностика (firmware_test уже прошит)
|
### Диагностика (firmware_test уже прошит)
|
||||||
|
|
||||||
|
|
@ -174,11 +184,12 @@ Production/Custom этот шаг не нужен).
|
||||||
Для плат старых ревизий и любых образов, собранных не в этом репозитории.
|
Для плат старых ревизий и любых образов, собранных не в этом репозитории.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/
|
1. Положить бинарник (сырой или уже HAB, см. раздел выше) в custom_binaries/
|
||||||
(или в директорию из SERVICE_CUSTOM_BINARIES_DIR)
|
(или в директорию из SERVICE_CUSTOM_BINARIES_DIR)
|
||||||
2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
||||||
3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости
|
3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD, если
|
||||||
4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB
|
конкретно этот образ его требует (уточнить у источника файла)
|
||||||
|
4. Загрузить — TUI сама соберёт HAB-образ (если нужно) и запишет правильный FCB
|
||||||
5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже
|
5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже
|
||||||
подставлен, останется нажать «Загрузить»
|
подставлен, останется нажать «Загрузить»
|
||||||
```
|
```
|
||||||
|
|
@ -196,14 +207,6 @@ Production/Custom этот шаг не нужен).
|
||||||
## Конфигурация (`.env`)
|
## Конфигурация (`.env`)
|
||||||
|
|
||||||
```ini
|
```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 (наше устройство)
|
# USB VID:PID — firmware_test CDC (наше устройство)
|
||||||
SERVICE_CDC_VID=1996
|
SERVICE_CDC_VID=1996
|
||||||
SERVICE_CDC_PID=00ad
|
SERVICE_CDC_PID=00ad
|
||||||
|
|
@ -212,7 +215,7 @@ SERVICE_CDC_PID=00ad
|
||||||
SERVICE_M5_VID=303a
|
SERVICE_M5_VID=303a
|
||||||
SERVICE_M5_PID=4001
|
SERVICE_M5_PID=4001
|
||||||
|
|
||||||
# Директория с сырыми кастомными бинарниками для FlashScreen → "Другое".
|
# Директория с кастомными бинарниками для FlashScreen → "Другое".
|
||||||
# По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом
|
# По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом
|
||||||
# с main.py в dev-режиме); создаётся автоматически при старте.
|
# с main.py в dev-режиме); создаётся автоматически при старте.
|
||||||
# SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries
|
# SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries
|
||||||
|
|
@ -221,10 +224,20 @@ SERVICE_M5_PID=4001
|
||||||
# Release временно нестабилен — по умолчанию Debug.
|
# Release временно нестабилен — по умолчанию Debug.
|
||||||
FIRMWARE_BUILD_TYPE=Debug
|
FIRMWARE_BUILD_TYPE=Debug
|
||||||
|
|
||||||
|
# Уровень логирования. По умолчанию INFO (плюс WARNING принудительно для
|
||||||
|
# шумных модулей spsdk/libusbsio). DEBUG — полный лог, включая построчные
|
||||||
|
# HID-дампы каждой команды spsdk (для диагностики проблем прошивки).
|
||||||
|
# SERVICE_LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
# Опционально: путь к директории лога TUI
|
# Опционально: путь к директории лога TUI
|
||||||
# SERVICE_LOG_DIR=/tmp
|
# SERVICE_LOG_DIR=/tmp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `.env` не загружается в упакованном (frozen) приложении — standalone-бинарь
|
||||||
|
> работает на встроенных значениях по умолчанию. Переменные окружения (не
|
||||||
|
> `.env`-файл) по-прежнему действуют и во frozen-режиме, если их выставить
|
||||||
|
> перед запуском.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
@ -232,45 +245,56 @@ FIRMWARE_BUILD_TYPE=Debug
|
||||||
### Из монорепозитория (разработчик)
|
### Из монорепозитория (разработчик)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just host::service-setup # установить зависимости tools/production/
|
just host::service-setup # установить/обновить зависимости tools/production/
|
||||||
just host::service-tui # запустить TUI
|
just host::service-tui # запустить TUI
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standalone-бинарь (сервисник)
|
### Standalone-бинарь (сервисник)
|
||||||
|
|
||||||
|
Распаковать `service-tui-vX.Y.Z-<os>.zip` в любую директорию и запустить
|
||||||
|
`service_tui` (`service_tui.exe` на Windows). Файл самодостаточен — не
|
||||||
|
требует установленного Python, `uv`, драйверов (Zadig/WinUSB) или сетевого
|
||||||
|
доступа.
|
||||||
|
|
||||||
|
### Сборка релизного бандла (разработчик)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just host::service-build
|
just build::hab-all-release # или hab-all-debug — собрать HAB-образы заранее
|
||||||
# → tools/production/dist/service_tui
|
just host::package-tui # → tools/production/dist/service-tui-vX.Y.Z-<os>/
|
||||||
```
|
```
|
||||||
|
|
||||||
> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен
|
Устройство бандла (`_internal/`, `firmware/`, `custom_binaries/`) и детали
|
||||||
> инициализированный `tools/host/` (`just host::setup-tools`), либо
|
сборки (`service_tui.spec`) — в [DEV_ARCH.md §14](DEV_ARCH.md#14-упаковка-pyinstaller-фаза-5).
|
||||||
> абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`).
|
|
||||||
|
> Если на Windows `package-tui` падает с `Permission denied` на шаге
|
||||||
|
> переименования — закройте запущенный `service_tui.exe` от предыдущей
|
||||||
|
> сборки и повторите (см. `DEV_ARCH.md §14.4`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
|
|
||||||
| Пакет | Версия | Назначение |
|
| Пакет | Версия | Назначение |
|
||||||
| --------------- | ------ | ----------------------------------------------------- |
|
| --------------- | ------ | -------------------------------------------------------------------------------------- |
|
||||||
| `textual` | ≥ 0.80 | TUI фреймворк |
|
| `textual` | ≥ 0.80 | TUI фреймворк |
|
||||||
| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial |
|
| `pyserial` | ≥ 3.5 | USB CDC ACM (firmware_test) + Serial (M5StampPLC) |
|
||||||
| `pyusb` | ≥ 1.0 | детект BootROM SDP (не виден через pyserial на macOS) |
|
| `spsdk` | 3.7.0 | прошивка in-process: SDP, McuBoot, HabImage |
|
||||||
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
||||||
| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря |
|
| `pyinstaller` | ≥ 6.0 | сборка standalone-бандла |
|
||||||
|
| `pyusb` | ≥ 1.0 | не используется текущей детект-логикой (см. `DEV_ARCH.md §2`), оставлен в зависимостях |
|
||||||
**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает
|
|
||||||
`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть
|
|
||||||
инициализирован (`just host::setup-tools`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Логирование
|
## Логирование
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/production/service_tui.log ← по умолчанию
|
tools/production/service_tui.log ← по умолчанию (рядом с main.py в dev,
|
||||||
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env
|
рядом с exe во frozen)
|
||||||
|
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env / окружении
|
||||||
```
|
```
|
||||||
|
|
||||||
Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не
|
Уровень по умолчанию: `INFO` для модулей приложения (`WARNING` для `textual`
|
||||||
пишет в stdout — 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, известные
|
> взаимодействия с firmware/M5, экранную архитектуру Textual, известные
|
||||||
> особенности фреймворка.
|
> особенности фреймворка.
|
||||||
> Пользовательская документация (экраны, запуск, конфигурация,
|
> Пользовательская документация (экраны, запуск, конфигурация,
|
||||||
> рабочие процессы сервисника) — в [README.md](README.md).
|
> рабочие процессы сервисника) — в [README.md](../README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Структура проекта
|
## 1. Структура проекта
|
||||||
|
|
||||||
|
## 1. Структура проекта
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/production/
|
tools/production/
|
||||||
├── main.py ← точка входа (10 строк)
|
├── main.py ← точка входа
|
||||||
├── pyproject.toml ← зависимости uv
|
├── pyproject.toml ← зависимости uv
|
||||||
|
├── dist/ ← дистрибутивы программы (PyInstaller)
|
||||||
├── uv.lock
|
├── uv.lock
|
||||||
├── custom_binaries/ ← runtime, gitignored, создаётся автоматически
|
├── service_tui.spec ← PyInstaller spec
|
||||||
│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое»
|
├── custom_binaries/ ← runtime, создаётся автоматически;
|
||||||
└── app/
|
│ сырые/готовые бинарники для FlashScreen → «Другое»
|
||||||
|
└── app/ ← implicit namespace package
|
||||||
|
│
|
||||||
|
│
|
||||||
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
||||||
├── app.tcss ← единый файл стилей для всех экранов
|
├── app.tcss ← единый файл стилей для всех экранов
|
||||||
├── models.py ← все типы данных (dataclass/Enum)
|
├── models.py ← все типы данных (dataclass/Enum)
|
||||||
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
|
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
|
||||||
├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8)
|
├── firmware_client.py ← async USB CDC клиент firmware_test
|
||||||
├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8)
|
├── m5_client.py ← async M5StampPLC клиент
|
||||||
├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py
|
├── 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, таймауты
|
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
|
||||||
├── widgets/
|
├── widgets/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
||||||
└── screens/
|
└── screens/
|
||||||
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
||||||
├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер причины возврата
|
├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер, кнопка «Выйти»
|
||||||
├── flash.py ← FlashScreen — прошивка / chip erase
|
├── flash.py ← FlashScreen — прошивка / chip erase
|
||||||
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
||||||
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
||||||
|
|
@ -55,7 +63,7 @@ graph LR
|
||||||
subgraph app["app/"]
|
subgraph app["app/"]
|
||||||
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
||||||
M5["m5_client.py\nSerial JSON-lines, 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"]
|
OR["orchestrator.py\nconfirm/progress/timeout router"]
|
||||||
end
|
end
|
||||||
TUI --> FC & M5 & FL & OR
|
TUI --> FC & M5 & FL & OR
|
||||||
|
|
@ -70,22 +78,16 @@ graph LR
|
||||||
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Host["tools/host/"]
|
FC |"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
|
||||||
FU["flash_usb.py\nsdphost + blhost"]
|
FL |"spsdk (libusbsio HID)\nVID:PID 1FC9:0130 / 15A2:0073"| ROM
|
||||||
end
|
M5 |"JSON-lines\nSerial"| M5HW
|
||||||
|
|
||||||
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
|
|
||||||
M5HW -->|"RLY1–4"| Board
|
M5HW -->|"RLY1–4"| Board
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как
|
> **Детект USB:** `flash_backend.detect_sdp()`/`detect_cdc()` используют spsdk
|
||||||
> основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через
|
> напрямую (`SdpUSBInterface.scan()` / `MbootUSBInterface.scan()`, HID-транспорт
|
||||||
> `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC.
|
> через `libusbsio`). CDC firmware_test и M5StampPLC резолвятся через
|
||||||
> M5StampPLC детектируется отдельно в `m5_client.py` тем же способом
|
> `pyserial` (`usb_ports.py::resolve_serial_port()`, `m5_client.py`).
|
||||||
> (`pyusb`, VID/PID из `.env` — см. раздел 5).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -103,7 +105,7 @@ stateDiagram-v2
|
||||||
WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC
|
WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC
|
||||||
|
|
||||||
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
||||||
FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое
|
FLASHING --> WAITING : Production/Custom прошит,\nили потеря USB (в простое ИЛИ во время операции)
|
||||||
|
|
||||||
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
||||||
|
|
||||||
|
|
@ -115,6 +117,13 @@ stateDiagram-v2
|
||||||
`ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на
|
`ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на
|
||||||
сообщения `DeviceDetected`/`FlashDone`/`DiagDone`.
|
сообщения `DeviceDetected`/`FlashDone`/`DiagDone`.
|
||||||
|
|
||||||
|
> **Важная деталь, не показанная на диаграмме**:
|
||||||
|
> `FLASHING --> WAITING` по стрелке «ошибка» срабатывает **только** при
|
||||||
|
> физическом обрыве USB (`FlashResult.connection_lost=True`). Логическая
|
||||||
|
> ошибка (файл не найден, битый custom-бинарь) — плата на месте, экран
|
||||||
|
> остаётся на `FLASHING` (нет перехода состояния вообще, поэтому на
|
||||||
|
> диаграмме это не отдельная стрелка). См. §6.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Обработка confirm_request
|
## 4. Обработка confirm_request
|
||||||
|
|
@ -154,9 +163,7 @@ flowchart TD
|
||||||
|
|
||||||
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно
|
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно
|
||||||
одним событием `SUMMARY` — настоящим от firmware или синтетическим
|
одним событием `SUMMARY` — настоящим от firmware или синтетическим
|
||||||
(`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии
|
(`aborted: true`), если чтение порта оборвалось по таймауту.
|
||||||
зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда
|
|
||||||
(исторический баг, см. `CHANGELOG.md`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -184,11 +191,6 @@ flowchart TD
|
||||||
PID — при детекте ориентироваться на `just host::m5-scan`, а не на
|
PID — при детекте ориентироваться на `just host::m5-scan`, а не на
|
||||||
документацию, если она когда-либо разойдётся с кодом.
|
документацию, если она когда-либо разойдётся с кодом.
|
||||||
|
|
||||||
**Важно на будущее:** документация (`HIL_BENCH.md`/`HIL_HOW_TO.md`) местами не
|
|
||||||
успевает за изменениями `agent.py`. При любых будущих изменениях протокола
|
|
||||||
агента (новые команды, смена формата ответа) — сверяться напрямую через
|
|
||||||
`grep` по `tools/hil/m5/agent.py`, а не полагаться только на документацию.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Мониторинг соединения и разрыв сессии
|
## 6. Мониторинг соединения и разрыв сессии
|
||||||
|
|
@ -197,22 +199,41 @@ flowchart TD
|
||||||
`FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
`FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
||||||
|
|
||||||
- **На `FlashScreen`** — проверка приостановлена во время активной
|
- **На `FlashScreen`** — проверка приостановлена во время активной
|
||||||
прошивки/erase (обрыв обнаружит сам `flash_usb.py` subprocess).
|
прошивки/erase (`self._flashing == True`).
|
||||||
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов
|
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов
|
||||||
(обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не
|
(обрыв надёжнее детектирует таймаут чтения порта внутри `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 не пытается определить, вернулась ли та же плата, просто стартует
|
разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует
|
||||||
диагностику с нуля.
|
заново с нуля.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -251,15 +272,14 @@ AppFrame {
|
||||||
|
|
||||||
### 8.1 Проблема
|
### 8.1 Проблема
|
||||||
|
|
||||||
Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются
|
Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются заранее
|
||||||
`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 —
|
(`just build::hab-*`) и всегда идут на плату с W25Q128 — для них auto-config
|
||||||
для них auto-config Flashloader (`configure-memory 0xC0000007` →
|
Flashloader достаточен. Для сторонних/легаси бинарников (старые платы,
|
||||||
`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси
|
W25Q256/512) это не так: auto-config Flashloader не документирован как
|
||||||
бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader
|
надёжный для 4-байтной адресации, а сами бинарники приходят «сырыми» (код +
|
||||||
не документирован как надёжный для 4-байтной адресации, а сами бинарники
|
таблица векторов, без FCB/IVT/DCD) либо уже готовым HAB-образом — зависит от
|
||||||
приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат,
|
источника. Решение — собирать HAB на лету (если нужно) и писать FCB явно, а
|
||||||
что `inputImageFile` в `hab_*.yaml` до сборки). Решение — собирать HAB
|
не полагаться на auto-config.
|
||||||
на лету и писать FCB явно, а не полагаться на auto-config.
|
|
||||||
|
|
||||||
### 8.2 Модели (`models.py`)
|
### 8.2 Модели (`models.py`)
|
||||||
|
|
||||||
|
|
@ -286,53 +306,44 @@ class FlashPreset:
|
||||||
одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/
|
одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/
|
||||||
памяти/DCD, нажал «Загрузить», вынул, вставил следующую.
|
памяти/DCD, нажал «Загрузить», вынул, вставил следующую.
|
||||||
|
|
||||||
Рассматривался отдельный режим «массовое программирование» (авто-прошивка
|
**Нужен ли DCD — implementation-defined, зависит от конкретного бинарника,
|
||||||
по факту детекта SDP, без нажатия кнопки на каждую плату) — отклонён:
|
не от его формата (сырой/готовый HAB).** Правило «сырой → включить DCD,
|
||||||
в SDP/Flashloader-режиме нет способа прочитать UID платы, авто-старт без
|
готовый HAB → выключить» **неверно как общее правило**: например, в связке
|
||||||
подтверждения оператора убирает последний шанс заметить, что в руках не та
|
`bootloader + tft_app` сам `bootloader` не требует DCD, а часть кастомных
|
||||||
плата. Оставлена только «липкая» память выбора (этот раздел).
|
бинарников (в т.ч. старый загрузчик, используемый на производстве) требует
|
||||||
|
DCD независимо от того, в каком виде получен файл. Оператор должен знать
|
||||||
|
по конкретному образу, инициализирует ли он SDRAM самостоятельно — TUI не
|
||||||
|
может определить это автоматически по содержимому файла.
|
||||||
|
|
||||||
### 8.3 Конвейер сборки (`flasher.py`)
|
### 8.3 Конвейер сборки (`flasher.py`)
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb)
|
Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb)
|
||||||
└── _run_flash_custom()
|
└── _flash_custom()
|
||||||
├── _build_custom_hab(raw_bin, use_dcd, progress_cb)
|
├── _build_custom_hab(raw_bin, use_dcd, progress_cb)
|
||||||
│ ├── генерирует temp .yaml в tools/host/hab/ (по образцу hab_bootloader_*.yaml:
|
│ └── flash_backend.build_custom_hab() — in-process spsdk API:
|
||||||
│ │ startAddress=0x60000000, ivtOffset=0x1000, initialLoadSize=0x2000,
|
│ Config (family=mimxrt1050, startAddress=0x60000000,
|
||||||
│ │ family=mimxrt1050, + DCDFilePath: ../dcd/dcd.bin если use_dcd)
|
│ ivtOffset=0x1000, initialLoadSize=0x2000,
|
||||||
│ ├── uv run nxpimage hab export --force -c <yaml> -o <out>,
|
│ + DCDFilePath, если use_dcd) → HabImage.export()
|
||||||
│ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath
|
└── _run_flash_op(flash_backend.flash, hab_bin, fcb_path=...)
|
||||||
│ │ резолвится от этой директории, как в build.just)
|
временный HAB-образ удаляется после прошивки
|
||||||
│ └── стриминг stdout nxpimage в progress_cb (не только logger.debug —
|
(finally: shutil.rmtree(hab_bin.parent))
|
||||||
│ иначе во время сборки лог FlashScreen выглядит «зависшим»)
|
|
||||||
└── flash_usb.py --bin-path <hab_bin> --fcb-path tools/host/dcd/{fcb_variant}_fdcb.bin
|
|
||||||
(временный .yaml и собранный HAB-образ удаляются после прошивки)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`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
|
`flash_backend.py::write_fcb_explicit()` — `write_memory(0x60000000, fcb_bin)`,
|
||||||
def write_fcb_explicit(fcb_path: Path) -> None:
|
буквальная запись 512-байтного FCB-блоба (tag `FCFB`), а не magic option word
|
||||||
"""write-memory 0x60000000 <fcb_path> — буквальная запись 512-байтного
|
`0xF000000F`. Обязателен для кастомных бинарей — auto-config Flashloader
|
||||||
FCB-блоба (tag 'FCFB'), а не magic option word 0xF000000F.
|
проверен только для W25Q128 (см. §Известные открытые вопросы).
|
||||||
Обязателен для кастомных бинарей — auto-config Flashloader проверен
|
|
||||||
только для W25Q128."""
|
|
||||||
```
|
|
||||||
|
|
||||||
Активируется флагом `--fcb-path` (только вместе с `--bin-path`). Штатный
|
Штатный путь (`firmware_test`/`bootloader`/`app` из `build/<Type>/`) не
|
||||||
`--firmware`-путь (три сборки из `BUILD_DIR`) не тронут: без `--fcb-path`
|
затрагивается — использует auto-config, как и раньше.
|
||||||
поведение идентично тому, что было до этой доработки.
|
|
||||||
|
|
||||||
Заодно увеличен таймаут `blhost` для `flash-erase-all` (chip erase) —
|
|
||||||
`-t 200000` вместо дефолтного: W25Q512 стирается заметно дольше W25Q128,
|
|
||||||
дефолтного таймаута `blhost` не хватало. `flash-erase-region` (стирание
|
|
||||||
пары секторов под FCB+HAB при обычной прошивке) не трогали — там масштаб
|
|
||||||
на порядки меньше, дефолта достаточно независимо от чипа.
|
|
||||||
|
|
||||||
### 8.5 UI (`flash.py`)
|
### 8.5 UI (`flash.py`)
|
||||||
|
|
||||||
|
|
@ -348,6 +359,13 @@ def write_fcb_explicit(fcb_path: Path) -> None:
|
||||||
дополнительно защищён `min-height: 6` — лог гарантированно виден даже в
|
дополнительно защищён `min-height: 6` — лог гарантированно виден даже в
|
||||||
худшем случае.
|
худшем случае.
|
||||||
|
|
||||||
|
**Троттлинг лога:** прогресс-бар обновляется на каждом
|
||||||
|
событии `FlashProgress`, но `#flash-log` для фазы `write` пишет только при
|
||||||
|
пересечении 10%-границы — без этого запись HAB-образа даёт ~135 строк в лог
|
||||||
|
на одну прошивку. Первая строка фазы (`"Запись <имя> (<размер> байт)"`) всегда
|
||||||
|
проходит; остальные фазы (`configure`/`erase`/`fcb`/`reset`/`error`) логируются
|
||||||
|
без троттлинга — их и так немного.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Архитектура экранов
|
## 9. Архитектура экранов
|
||||||
|
|
@ -372,7 +390,7 @@ graph TB
|
||||||
subgraph Clients["Клиенты"]
|
subgraph Clients["Клиенты"]
|
||||||
FC["FirmwareClient"]
|
FC["FirmwareClient"]
|
||||||
M5["M5Client"]
|
M5["M5Client"]
|
||||||
FL["Flasher"]
|
FL["Flasher\n(async) + flash_backend\n(spsdk in-process)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
WS -->|"DeviceDetected(FLASHING)"| FS
|
WS -->|"DeviceDetected(FLASHING)"| FS
|
||||||
|
|
@ -411,7 +429,7 @@ sequenceDiagram
|
||||||
|
|
||||||
OP->>TUI: запустить service_tui
|
OP->>TUI: запустить service_tui
|
||||||
TUI->>WS: push_screen()
|
TUI->>WS: push_screen()
|
||||||
WS->>WS: pyusb poll каждые 1.5 с
|
WS->>WS: USB poll каждые 1.5 с
|
||||||
|
|
||||||
OP->>FW: подключить плату USB
|
OP->>FW: подключить плату USB
|
||||||
WS->>TUI: DeviceDetected(DIAGNOSING)
|
WS->>TUI: DeviceDetected(DIAGNOSING)
|
||||||
|
|
@ -471,7 +489,7 @@ sequenceDiagram
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Версионирование firmware
|
## 11. Версионирование firmware и TUI
|
||||||
|
|
||||||
`firmware_test` версионируется через CMake
|
`firmware_test` версионируется через CMake
|
||||||
(`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через
|
(`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через
|
||||||
|
|
@ -484,7 +502,10 @@ sequenceDiagram
|
||||||
отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из
|
отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из
|
||||||
`pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata`
|
`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()`.
|
`mount_all()`.
|
||||||
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня
|
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня
|
||||||
проекта — постоянно расходится при рефакторинге структуры. Решение: один
|
проекта — постоянно расходится при рефакторинге структуры. Решение: один
|
||||||
`CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`.
|
`CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. Во frozen
|
||||||
|
дополнительно требует, чтобы `app.tcss` физически лежал в бандле по тому же
|
||||||
|
относительному пути (см. §14).
|
||||||
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает
|
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает
|
||||||
ширину по умолчанию равную длине заголовка — длинный контент обрезается
|
ширину по умолчанию равную длине заголовка — длинный контент обрезается
|
||||||
независимо от `height` строки. Нужно использовать `add_column(label,
|
независимо от `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 нестабильна** (медленное мигание — подозрение на
|
- **Release-сборка firmware нестабильна** : работает только с оптимизацией уровня O1
|
||||||
проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно
|
|
||||||
форсирует Debug через `FIRMWARE_BUILD_TYPE`.
|
|
||||||
- **`tools/shared/m5_agent.py`** — сознательно не делался: pytest
|
- **`tools/shared/m5_agent.py`** — сознательно не делался: pytest
|
||||||
HIL-окружение и TUI используют независимые M5-клиенты, признано правильным
|
HIL-окружение и TUI используют независимые M5-клиенты, признано правильным
|
||||||
архитектурным решением, а не техдолгом.
|
архитектурным решением, а не техдолгом. (Устаревшая `just host::service-build`
|
||||||
|
ссылается на несуществующий `tools/shared/` через `--add-data` — рецепт,
|
||||||
|
скорее всего, нерабочий, кандидат на удаление в пользу `package-tui`.)
|
||||||
- Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и
|
- Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и
|
||||||
«копирование UID с экрана» — отложены, не начаты.
|
«копирование UID с экрана» — отложены, не начаты.
|
||||||
- **Массовое программирование** — решено НЕ делать авто-прошивку по факту
|
- **Массовое программирование** — решено НЕ делать авто-прошивку по факту
|
||||||
|
|
@ -564,7 +676,6 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл
|
||||||
идентифицировать по UID.
|
идентифицировать по UID.
|
||||||
- **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили
|
- **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили
|
||||||
не полагаться на него вообще, для кастомных бинарей FCB всегда пишется
|
не полагаться на него вообще, для кастомных бинарей FCB всегда пишется
|
||||||
явно (`--fcb-path`, см. §8.4). Остаётся не до конца понятым, работает ли
|
явно (§8.4). Остаётся не до конца понятым, работает ли
|
||||||
`configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос
|
`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