# service-tui — TUI сервисного инженера TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. > Внутреннее устройство, диаграммы архитектуры и заметки для разработчиков — > в [ARCHITECTURE.md](docs/ARCHITECTURE.md). --- ## Экраны приложения ### Ожидание подключения (WaitingScreen) Стартовый экран. Автодетект USB — как только плата подключена, TUI сам определяет режим (прошивка или диагностика) и переключается. ```bash ┌────────────────────────────────────────────────────┐ │ service_tool v0.2.0 │ │ │ │ [LOGO_ART] │ │ │ │ Подключите плату индикатора к USB... ⠋ │ │ │ │ [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` Версия читается из `pyproject.toml` — при бампе версии мокап выше не нужно обновлять руками, TUI подставит актуальную сама. После детекта платы подсказка меняется на «Плата найдена, подключаемся...», спиннер продолжает крутиться — экран остаётся на месте ещё несколько секунд (до ~10 с, если подключён HIL-стенд M5StampPLC и его агент только что перезапустился при открытии порта), пока TUI подключается к плате и, опционально, к M5. Это не зависание. При потере соединения на любом другом экране сессия разрывается полностью — TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над подсказкой на 4 секунды появляется строка `⚠ <причина>` (например, «⚠ Соединение с платой потеряно»), затем скрывается сама — обычный автодетект продолжается без вмешательства оператора. ### Прошивка платы (FlashScreen) Триггер: обнаружена плата в режиме BootROM SDP. ```bash ┌────────────────────────────────────────────────────┐ │ ⚡ Загрузка прошивки на плату индикатора │ │ (режим BootROM) │ │ │ │ Выбор загружаемой прошивки │ │ ◉ Диагностическая прошивка (firmware_test) │ │ ○ Серийная прошивка (загрузчик) │ │ ○ Верификация (smoke-test, требует BOOT_MOD) │ ← только если «Серийная» │ ○ Другое │ │ Файл (custom_binaries/) │ │ [ TFT_BOOTLOADER_NEW.bin ▾ ] │ ← только если «Другое» │ Память платы │ │ [ W25Q128 / W25Q64 ▾ ] │ │ ○ Использует SDRAM (DCD) │ │ │ │ [ ▶ Загрузить ] [ ⚠ Очистить память ] [ ✕ Выйти из приложения ] │ │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ │ │ ▶ Сборка HAB-образа (HabImage)... │ │ │ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` Лог виден постоянно (не только во время прошивки), прогресс-бар — только во время активной операции (скрыт в простое), без числового `%` — только полоса и построчный лог в реальном времени. Панель выбора прошивки ограничена по высоте и скроллится сама, если разрастается (варианты "Другое") — лог снизу гарантированно не сжимается меньше 6 строк. **"Другое" — для бинарников, собранных не в этом репозитории.** В `custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до сборки HAB). TUI сама собирает из него загружаемый образ на лету, in-process через Python API `spsdk` (без вызова внешних CLI-утилит): 1. `HabImage` (spsdk) — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") 2. в Flash пишется явный FCB под выбранную память платы (не тот же auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для W25Q256/512 он ненадёжен, см. `docs/ARCHITECTURE.md`) 3. образ прошивается с `0x60001000`, как обычно **Выбор запоминается на весь запуск TUI** — файл, память платы, DCD и чек-бокс «Верификация» не нужно выставлять заново на каждой следующей плате: прошили одну, вынули USB, вставили следующую такую же — TUI уже подставила прошлый выбор, остаётся нажать "Загрузить". Сбрасывается только при перезапуске TUI. **Верификация записи — два независимых уровня.** После записи, перед reset, TUI **всегда** читает записанный диапазон обратно и сверяет с образом (silent-corruption ловится автоматически, для любой прошивки — в логе видна строка «Верификация записи: OK», отдельного экрана не требует). Чек-бокс «Верификация» у «Серийной прошивки» — это **другой**, более дорогой уровень: живой smoke-test самого загрузчика по USB CDC (SDRAM + идентификация QSPI-чипа), требует физической смены `BOOT_MOD` оператором — см. `VerifyScreen` ниже. OFF по умолчанию, чтобы не мешать массовой заливке партии плат. ### Живая проверка загрузчика (VerifyScreen) Показывается только после успешной **серийной** прошивки при включённом чек-боксе «Верификация» (иначе — сразу `WaitingScreen`, как раньше). ```bash ┌────────────────────────────────────────────────────┐ │ 🔎 Верификация загрузчика │ │ │ │ После серийной прошивки плата осталась в режиме │ │ BootROM. Чтобы проверить загрузчик, переведите её │ │ в нормальный режим: BOOT_MOD_1 → GND → Reset │ │ │ │ ⠋ Плата найдена, проверяю загрузчик... │ │ │ │ ✅ Загрузчик отвечает: v0.1.0 │ │ ✅ SDRAM smoke-test: пройден │ │ ✅ QSPI-чип: W25Q128 (16 МБ) │ │ ✅ Верификация пройдена │ │ │ │ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` Загрузчик и firmware_test используют один и тот же CDC VID:PID (намеренно, чтобы переиспользовать клиентский код) — но у загрузчика нет `list_tests`, поэтому эта проверка не идёт через обычный `WaitingScreen`-автодетект (который вёл бы в `DiagScreen`), а отдельным экраном сразу после прошивки. «⚠» вместо «❌» на отдельном пункте означает «нет ответа» (таймаут/старая прошивка без команды), не обязательно провал. Таймаут ожидания BOOT_MOD — 45с, после — сообщение об этом и переход дальше по кнопке. «Пропустить проверку» доступна в любой момент ожидания. ### Переход в рабочий режим (PostFlashScreen) Показывается **только** после успешной прошивки `firmware_test` (для Production/Custom этот шаг не нужен — у Production при включённой верификации свой отдельный экран, см. `VerifyScreen` выше). ```bash ┌────────────────────────────────────────────────────┐ │ ✅ firmware_test успешно записан │ │ │ │ Переведите плату в нормальный режим: │ │ BOOT_MOD_1 → GND → Reset │ │ │ │ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ │ Автопереход через: 40с │ └────────────────────────────────────────────────────┘ ``` ### Диагностика (DiagScreen) Триггер: плата видна как CDC-устройство и отвечает на связь. ```bash ┌────────────────────────────────────────────────────────────────┐ │ fw: 0.2.0 MCU ID: A1B2C3D4E5F60011 M5 Bench: ✓ подключён │ ├───────────────────────────┬──────────────────────────────────────┤ │ Доступные тесты │ Тест M5 Bench Статус Время │ │ [Выбрать все][Снять все] │ microSD [-] ✗ FAIL 0.1с │ │ ☐ SDRAM 32 MB │ no card detected (без обрезки) │ │ ☐ QSPI Flash │ SDRAM 32 MB [-] ✓ PASS 1.8с │ │ ☐ microSD │ QSPI Flash [-] ✓ PASS 0.6с │ │ ☐ TFT Display │ TFT Display [-] … │ │ ☐ CAN loopback [HIL] │ Кнопки [-] │ │ ☐ Оптовходы [HIL] │ │ ├──────────────────────────────────────────────────────────────────┤ │ ████████░░░░ Тест: usd — mount: ok ← только во время прогона │ ├──────────────────────────────────────────────────────────────────┤ │ [▶ Запустить выбранные тесты] [▶▶ Запустить все тесты] [✕ Выйти из приложения] │ ├──────────────────────────────────────────────────────────────────┤ │ ⚠ Экран залит красным цветом? 28с │ │ [ ✓ Да ] [ ✗ Нет ] ← только во время confirm │ └────────────────────────────────────────────────────────────────┘ ``` Что важно знать: - **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать все"/"Снять все". - **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена целиком, длинные сообщения переносятся на несколько строк без обрезания. Колонка `M5 Bench` отмечает `[*]`/`[-]` — требует ли тест HIL-стенд. - **HIL-тесты** (CAN, оптовходы) без подключённого M5StampPLC — серые, недоступны для выбора. - **Интерактивные проверки во время прогона теста** бывают трёх видов: - HIL-тесты (CAN, оптовходы) при наличии M5 — отвечает стенд автоматически, без участия оператора; - тесты кнопок — экран показывает подсказку `⌨ <инструкция>`, ответ не требуется, нужно физически нажать кнопку на плате; - остальное (дисплей, аудио) — форма "Да/Нет" с таймером `⚠ <вопрос>` на экране. - До завершения загрузки списка тестов правая колонка показывает подсказку «Выберите тесты и нажмите «Запустить выбранные»» вместо таблицы. --- ## Рабочие процессы сервисника ### Диагностика (firmware_test уже прошит) ```bash 1. BOOT_MOD_1 → GND, сбросить плату 2. Подключить USB → DiagScreen 3. Выбрать тесты (по умолчанию ничего не выбрано) или "Выбрать все" 4. Запустить → ответить на интерактивные запросы 5. Получить итог; FAIL-тесты — наверху таблицы, detail виден полностью ``` ### Перепрошивка firmware_test ```bash 1. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen 2. Выбрать «Диагностическая прошивка (firmware_test)» → Загрузить 3. PostFlashScreen: BOOT_MOD_1 → GND, сбросить плату 4. Нажать "Готово" (или дождаться авто-перехода через 40с) 5. TUI автоматически попадает в DiagScreen при следующем подключении ``` ### Прошивка стороннего бинарника (custom_binaries/) Для плат старых ревизий и любых образов, собранных не в этом репозитории. ```bash 1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/ (или в директорию из SERVICE_CUSTOM_BINARIES_DIR) 2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen 3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости 4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB 5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже подставлен, останется нажать «Загрузить» ``` ### Chip Erase ```bash 1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3) 2. FlashScreen → Очистить память (~30 с для W25Q128, дольше для W25Q512) 3. После erase BootROM не загрузит прошивку — требуется перепрошить ``` --- ## Известные ограничения - **Одна плата на столе одновременно.** В SDP/Flashloader-режиме плату нельзя идентифицировать по UID — авто-прошивка по факту детекта без подтверждения оператора убрала бы последний шанс заметить, что в руках не та плата. Массового программирования (несколько плат параллельно) нет и не планируется в этом виде — см. `docs/ARCHITECTURE.md`, §8. - **Циклический прогон тестов** (повторный автозапуск набора без ручного нажатия на каждую итерацию) — отложен на пост-релиз, не входит в текущую версию. - **Подключение к M5StampPLC (HIL-стенд) может занять до ~10 с** после детекта платы, если M5 только что был подключён к USB или агент на нём только что перезапустился (открытие serial-порта хостом перезапускает MicroPython на M5 — агенту нужно время на инициализацию I2C/AW9523/CAN перед готовностью отвечать). WaitingScreen в это время показывает «Плата найдена, подключаемся...» — это штатное поведение, не зависание. См. `docs/ARCHITECTURE.md`, §10. - **macOS-бандл не подписан Apple Developer ID и не нотаризован** — при первом запуске Gatekeeper блокирует каждый файл бандла по отдельности. Обход — `xattr -cr` на распакованную папку, см. «Запуск» → «macOS: первый запуск» выше. - **Загрузчик, оставленный подключённым после `VerifyScreen`, будет неправильно маршрутизирован.** `WaitingScreen` детектит любой CDC (у загрузчика и firmware_test один VID:PID) и ведёт в `DiagScreen`, которая ждёт протокол firmware_test (`list_tests` и т.п.) — у загрузчика его нет. На практике не мешает: после верификации плата снимается со стенда (сценарий "bootloader-only, потом массовая заливка SD"), не остаётся подключённой к TUI. --- ## Конфигурация (`.env`) ```ini # USB VID:PID — BootROM SDP (константы NXP, не менять) BOOTROM_VID=1fc9 BOOTROM_PID=0130 # USB VID:PID — Flashloader (константы NXP, не менять) FLASHLOADER_VID=15a2 FLASHLOADER_PID=0073 # USB VID:PID — firmware_test CDC (наше устройство) SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad # USB VID:PID — M5StampPLC (детект HIL-стенда) SERVICE_M5_VID=303a SERVICE_M5_PID=4001 # Директория с сырыми кастомными бинарниками для FlashScreen → "Другое". # По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом # с main.py в dev-режиме); создаётся автоматически при старте. # SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries # Тип сборки firmware_test для прошивки (Debug | Release). # Release временно нестабилен — по умолчанию Debug. FIRMWARE_BUILD_TYPE=Debug # Опционально: путь к директории лога TUI # SERVICE_LOG_DIR=/tmp # Уровень логирования. По умолчанию — INFO, при этом spsdk/libusbsio # принудительно приглушены до WARNING. DEBUG включает полный дамп, # включая сырые HID-пакеты spsdk (много строк на одну прошивку). # SERVICE_LOG_LEVEL=DEBUG ``` --- ## Запуск ### Из монорепозитория (разработчик) ```bash just host::service-setup # установить зависимости tools/service_tui/ just host::service-tui # запустить TUI ``` ### Standalone-бинарь (сервисник) ```bash just host::package-tui # → tools/service_tui/dist/service-tui-vX.Y.Z-/ ``` Бандл (PyInstaller, onedir) самодостаточен — прошивка идёт напрямую через spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` ни субпроцессом, ни как-либо иначе. Не нужны ни Zadig, ни установленный Python/uv на машине сервисника. Структура бандла и резолв путей во frozen — см. [ARCHITECTURE.md](docs/ARCHITECTURE.md), §14. #### macOS: первый запуск («Apple could not verify...» на каждый файл) Бандл не подписан Apple Developer ID и не нотаризован — Gatekeeper при первом запуске выдаёт отдельное предупреждение «не удалось проверить разработчика» **на каждый** файл бандла (исполняемый файл + каждая `.dylib` внутри `_internal/`, включая `libusbsio`), а не одно общее окно. Причина — карантинный атрибут (`com.apple.quarantine`), который macOS проставляет на файлы, распакованные из скачанного архива; через «Открыть» в Finder его пришлось бы снимать по одному диалогу на файл. Снять его сразу со всего бандла одной командой в терминале (один раз, после распаковки zip): ```bash xattr -cr service-tui-vX.Y.Z-macos/ ``` После этого бандл запускается без единого диалога. Это стандартный путь для несигнированных бинарников, распространяемых вне App Store/сайта разработчика — не баг конкретной сборки. --- ## Зависимости | Пакет | Версия | Назначение | | --------------- | ------ | ----------------------------------------------------------------------------------------------------------------- | | `textual` | ≥ 0.80 | TUI фреймворк | | `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | | `spsdk` | 3.7.0 | прошивка: SDP/McuBoot (Flash, chip erase) + HabImage (сборка кастомных HAB), нативный HID (libusbsio) — без Zadig | | `python-dotenv` | ≥ 1.0 | загрузка `.env` | | `pyinstaller` | ≥ 6.0 | сборка standalone-бандла (`just host::package-tui`) | **Никакой рантайм-зависимости на `tools/host/` в смысле subprocess-вызовов нет.** Прошивка выполняется in-process через `spsdk` (`app/flash_backend.py`). Из `tools/host/dcd/` читаются только статичные data-блобы (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) — они отслеживаются в git, `just host::setup-tools` для запуска TUI из исходников не требуется. `tools/host/ flash_usb.py` — независимый dev-CLI для `just host::flash*`, TUI его не вызывает (см. [ARCHITECTURE.md](docs/ARCHITECTURE.md), §1/§8). --- ## Логирование ```bash tools/service_tui/service_tui.log ← по умолчанию (dev) / рядом с exe (frozen) $SERVICE_LOG_DIR/service_tui.log ← если задан в .env ``` Уровень по умолчанию — `INFO`; `textual`, `spsdk` и `libusbsio` понижены до `WARNING` независимо от root (иначе прошивка даёт ~135 строк сырых HID-пакетов на одну операцию). `SERVICE_LOG_LEVEL=DEBUG` включает полный DEBUG везде, включая эти модули — используется при разборе проблем на железе. TUI не пишет в stdout — Textual захватывает терминал.