diff --git a/CHANGELOG.md b/CHANGELOG.md index 722f13e..753c485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,86 +55,153 @@ - Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты. - Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL. - Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации. -- Следить за развитием BSP: MQS, RGB, bootloader или `tft_app`. +- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app` — обе директории всё ещё не заведены. - Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log. +- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA. -## [Не выпущено] — service-tui: кастомная прошивка нестандартной памяти (после слияния `feature-tui-python`) +## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller -Диапазон: `<заполнить после merge>..<текущий HEAD>` -Сравнение: `<заполнить после merge>` +Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` + незакоммиченные изменения рабочего дерева (документация) +Сравнение: -> Изменения внесены **поверх** слияния `feature-tui-python → dev` — базовая -> архитектура `service-tui` (экраны, USB/M5-клиенты, оркестратор) приходит -> самим merge-коммитом; здесь только то, что было доработано отдельно после. +> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`). +> **Полностью заменяет предыдущую версию этой записи**: прошивка сторонних +> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` + +> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор +> заменена прямыми вызовами `spsdk` Python API — ничего из старой записи +> больше не описывает текущий код. ### Кратко -- `service-tui` теперь умеет прошивать сторонние/легаси бинарники (платы - с W25Q256/512 вместо штатного W25Q128) через USB SDP, с явной записью - FCB вместо ненадёжного для таких чипов auto-config Flashloader. -- Выбор оператора на `FlashScreen` (файл/память/DCD) запоминается на весь - запуск TUI — ускоряет прошивку партии одинаковых плат. -- Документация (`tools/production/README.md`, `tools/production/DEV_ARCH.md`, - `tools/host/README.md`, `docs/HOW_TO_FLASH.md`, `docs/DEV_ARCH.md`) - синхронизирована с фактическим состоянием кода. +- Прошивка в `service-tui` переведена с subprocess-обёртки над + `nxpimage`/`sdphost`/`blhost` на прямые вызовы `spsdk` Python API + (`McuBoot`/`SDP`/`HabImage`) — `app/flash_backend.py`, провалидировано + byte-exact на живом железе (macOS + Windows). `tools/host/flash_usb.py` + остаётся отдельным dev-CLI для `just host::flash*`, TUI его больше не + вызывает ни субпроцессом, ни как библиотеку. +- Обрыв USB во время прошивки/chip erase теперь надёжно типизируется во + всех трёх наблюдавшихся на железе сценариях (`SPSDKConnectionError`, + `SPSDKTimeoutError`, `False`-по-таймауту без исключения) и даёт оператору + единое понятное сообщение вместо «Непредвиденная ошибка». +- Собран первый standalone-бандл (PyInstaller, onedir) — alpha, вручную + протестирован на macOS и Windows. +- Документация (`tools/production/README.md`+`docs/DEV_ARCH.md`, корневые + `docs/*`, все `bsp/*/README.md`, корневой `README.md`) синхронизирована + с фактическим состоянием кода после всех фаз миграции. ### Добавлено -- `tools/host/flash_usb.py` — `write_fcb_explicit()` + флаг `--fcb-path`: - буквальная запись 512-байтного FCB-блоба (`write-memory 0x60000000`) - вместо magic option word `0xF000000F`. Штатный `--firmware`-путь - (`firmware_test`/`bootloader`/`app`) не тронут — работает как раньше. -- `tools/production/app/models.py` — `FcbVariant` (`W25Q128` покрывает и - W25Q64, `W25Q512` — и W25Q256) и `FlashPreset` (липкий выбор оператора). -- `tools/production/app/flasher.py` — `_build_custom_hab()`: сборка - HAB-образа на лету через `nxpimage hab export` из «сырого» бинарника - (без FCB/IVT/DCD) в `custom_binaries/`, с опциональным `DCDFilePath`; - стриминг вывода `nxpimage` в UI-лог, а не только в `logger.debug`. - `list_custom_binaries()` + `SERVICE_CUSTOM_BINARIES_DIR` — резолв - директории кастомных бинарей (внешняя, не пакуется в PyInstaller). -- `tools/production/app/screens/flash.py` — `Select` по `custom_binaries/`, - `Select` по `FcbVariant`, `Switch` DCD вместо свободного текстового - `Input`; предзаполнение из `FlashPreset` при создании экрана. -- `tools/production/app/app.py` — `ServiceApp._last_flash_preset`, - прокидывается в новый `FlashScreen` при каждом `DeviceDetected(FLASHING)`. +- `tools/production/app/flash_backend.py` — синхронное ядро прошивки на + spsdk: `detect_sdp`/`detect_cdc`, `load_flashloader`, `flash`, + `erase_chip`, `build_custom_hab` (`HabImage` вместо `nxpimage` CLI), + `write_fcb_explicit`/`write_fcb_auto`. Zero Textual/asyncio импортов, + тестируется без event loop. +- `tools/production/app/usb_ports.py` — `resolve_serial_port()` по VID:PID + (имя порта не переносимо между перевтыкиваниями). +- Иерархия `FlashBackendError`/`ConnectionLostError`/`DeviceNotFoundError`/ + `FlashLoaderTimeoutError`/`HabBuildError` с полем `connection_lost` — + различает физический обрыв USB от логической ошибки прошивки без + парсинга текста сообщения. +- `tools/production/tests/test_flash_backend.py` — вырос до 45 unit-тестов + backend'а, включая обе ветки обрыва USB (`SPSDKTimeoutError`, + `False`-по-таймауту + вариант B через `detect_sdp()`) и golden-тест + byte-exact сборки HAB. +- Кнопка «✕ Выйти из приложения» на `WaitingScreen`. +- `tools/production/service_tui.spec` — PyInstaller spec (onedir). +- `tools/production/docs/RELEASE_ROADMAP.md` — дорожная карта Фаз + 4a→4b→5→6 с принятыми решениями (Р10–Р12) и статусом гейтов. ### Изменено -- `tools/host/flash_usb.py`, `erase_chip()` — таймаут `blhost` - `flash-erase-all` увеличен до `-t 200000` (W25Q512 стирается заметно - дольше W25Q128, дефолтного таймаута не хватало). `flash-erase-region` - (обычная прошивка) не тронут — там стирается пара секторов, масштаб иной. -- `tools/production/app/app.tcss` — `#flash-target-group` ограничен по - высоте (`max-height: 18`, свой скролл), `#flash-log` защищён - `min-height: 6` — разросшаяся custom-группа больше не сжимает лог - прошивки до нечитаемого состояния. +- `tools/production/app/flasher.py` — переведён с subprocess + (`flash_usb.py` через `uv run`) на `asyncio.to_thread`-обёртку над + `flash_backend.py`; сборка кастомного HAB — через `HabImage` в отдельном + потоке, а не subprocess `nxpimage`. +- `tools/production/app/main.py` — логирование: root по умолчанию `INFO` + (было `DEBUG`), `spsdk`/`libusbsio` принудительно приглушены до + `WARNING` независимо от root; полный DEBUG — через + `SERVICE_LOG_LEVEL=DEBUG`. +- `tools/production/app/screens/flash.py` — троттлинг записи в + `#flash-log` для фазы `write` (раз на 10%, ~10 строк вместо ~135) без + потери плавности прогресс-бара. +- `bsp/sd/src/sd.c` — `bsp_sd_init()`/`bsp_sd_deinit()` теперь делают + аппаратный `USDHC_Reset()` + полный `memset(&g_sd, ...)` перед + повторной инициализацией: без этого non-blocking host driver SDK мог + оставаться в состоянии ожидания транзакции от предыдущей + diagnostic-сессии, и следующий `f_mount()` в тесте `usd` блокировался + навсегда. + +### Исправлено + +- Обёртка обрыва USB расширена с `SPSDKConnectionError` на + `(SPSDKConnectionError, SPSDKTimeoutError)` — второй тип не наследует + первый, но реально прилетает на read-фазе после write. +- Вариант B для команд, возвращающих `False` без исключения + (`flash_erase_all`/`flash_erase_region`/`write_memory`): при `False` + выполняется быстрый `detect_sdp()` — устройство пропало с шины → + `ConnectionLostError`, устройство на месте → обычная `FlashBackendError`. +- Баг «File not found» для bootloader/app/firmware_test при резолве путей + прошивки (Фаза 4a). +- Unit-тест моки (`test_cli.c`, `test_bsp_can.c`, `test_firmware_runner.c`, + stub-хедеры `fsl_clock.h`/`version.h`) — фиксы после рефакторинга + `cli.c`/`test_runner.c`. + +### Тесты + +- `test_flash_backend.py` — вырос до 45 тестов, включая гейт по + `SPSDKTimeoutError` и переклассификации erase-таймаута (вариант B). ### Документация -- `tools/production/README.md` — мокап `FlashScreen` под факт (Select/Select/ - Switch), новый workflow «Прошивка стороннего бинарника», `SERVICE_CUSTOM_BINARIES_DIR` - в примере `.env`. -- `tools/production/DEV_ARCH.md` — новый §8 (конвейер кастомной прошивки, - `FlashPreset`, явная запись FCB, обоснование отказа от auto-config для - W25Q256/512 и от полноценного авто-батч-режима прошивки). -- `docs/HOW_TO_FLASH.md` — §1.5, сноска в сравнительной таблице способов - прошивки (FCB «не нужен» верно только для W25Q128). -- `docs/DEV_ARCH.md` — `tools/production/` добавлен в дерево структуры - репозитория (отсутствовал ранее). -- `tools/host/README.md` — актуализирован статус `dcd/*.bin` (`w25q512_fdcb.bin` - теперь используется), указатель на `service-tui` как способ прошивки - нестандартной памяти. +- `tools/production/README.md`/`tools/production/docs/DEV_ARCH.md` — + полностью пересмотрены под факт: убраны все следы subprocess/`nxpimage`/ + `flash_usb.py` из описания архитектуры прошивки; добавлены §6.2 + (обработка обрыва USB), §14 (PyInstaller/frozen-резолв путей), §15 + (логирование); зафиксирован разрыв между закоммиченным + `service_tui.spec` (`datas` только `../shared`) и фактическим + содержимым уже собранных релизных бандлов в `dist/`. +- `docs/testing/PROTOCOL.md` — версия `0.1.0`→`0.1.2`, добавлена команда + `get_version` и события `test_list`/`uid_response`/`version_response`, + матрица тестов исправлена (убраны никогда не существовавшие `uart_ttl`/ + `uart_iso`, добавлен реальный `mqs`), поток Display дополнен шагами + ротации (`display_rot0`/`display_rot_base`). +- `docs/testing/host/HOST_CREATE_TEST.md` — был байт-в-байт дубликатом + `docs/HOW_TO_DEBUG.md` (копипаст-баг, минимум с 2026-06-23); переписан + как реальный гайд по добавлению host-теста. +- `docs/HOW_TO_FLASH.md` (§1.5 под факт spsdk-конвейера), `docs/DEV_ARCH.md` + (в дереве `tools/hil/` недоставало `04_test_button.py`), + `docs/testing/hil/HIL_CREATE_TEST.md` (пример `loaded_` без `m5` + вводил в заблуждение — питание таргета всегда идёт через M5, не только + сигнальные реле) — актуализированы. +- `bsp/usb_cdc/README.md` (VID/PID был заявлен как заглушка `0x1234:0x0001`, + реально прошит `0x1996:0x00AD`), `bsp/uart_host/README.md` (в списке API + отсутствовали реальные `bsp_uart_host_deinit/rx_available/rx_flush`), + `bsp/can/README.md` (несуществующие в коде `bsp_can.c`/`can_mock.h`/ + `bsp_can_rx_cb_t`) — исправлены по сверке с заголовками. +- `bsp/mqs/{mqs.c,mqs.h,mqs_amp.c}` — докстринги приведены в соответствие + с кодом (были «SAI1»/«16 кГц», реально SAI3/12 кГц — подтверждено + сверкой с `bsp/generated/clock_config.c`); `bsp/provisioning/provisioning.h` + — докстринг порядка байт UID исправлен на соответствующий реализации + (`provisioning.c` пишет CFG0 первым, докстринг утверждал обратное). +- Корневой `README.md` — `firmware/bootloader/`/`firmware/tft_app/` + помечены как запланированные, а не готовые (директорий не существует, + `add_subdirectory()` закомментирован в корневом `CMakeLists.txt`); + добавлен ранее отсутствовавший раздел «Инструменты (`tools/`)» — + `tools/production/` (service-tui) нигде не упоминался. ### Известные ограничения -- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решение - писать FCB явно снимает вопрос архитектурно, но не подтверждает и не - опровергает надёжность auto-config как таковую. -- Полноценный режим массового программирования (авто-прошивка по факту - детекта USB, без подтверждения оператора) рассмотрен и отклонён — в - SDP/Flashloader-режиме нет способа прочитать UID платы для идентификации. -- Standalone-упаковка (`PyInstaller`) для этого функционала ещё не - реализована — см. `tools/production/RELEASE_PLAN.md`. +- `service_tui.spec` не включает `datas` для `spsdk`/`dcd/*.bin`/ + `pyproject.toml`, хотя уже собранные alpha-бандлы их содержат — спек + нужно синхронизировать перед следующей сборкой релиза. +- `pyusb` в `pyproject.toml` — мёртвая зависимость (Р7 перевёл детект на + `spsdk`/`serial.tools.list_ports`), кандидат на удаление. +- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили + не полагаться на него вообще, FCB для кастомных бинарей всегда пишется + явно. +- Массовое программирование (авто-прошивка по факту детекта SDP, без + подтверждения оператора) рассмотрено и отклонено — в SDP/Flashloader-режиме + нет способа прочитать UID платы для идентификации. ## [2026-06-29] — Этапы 6г–7: MQS, HIL pytest firmware_test, Provisioning diff --git a/FIRST_RELEASE_PLAN.md b/FIRST_RELEASE_PLAN.md new file mode 100644 index 0000000..50afdbd --- /dev/null +++ b/FIRST_RELEASE_PLAN.md @@ -0,0 +1,202 @@ +# План первого релиза в GitHub + +> Цель: в GitHub Releases появляется тег с прикреплёнными бинарниками — +> `firmware_test` (HAB-образ) и `service-tui` (standalone-бандлы под macOS и +> Windows). Этот документ — практический чек-лист «как довести до кнопки +> Publish», а не повторение инженерных фаз `RELEASE_ROADMAP.md`. + +## Как связаны два артефакта + +`firmware_test` (C, i.MX RT1052, версия `0.1.2` из +`firmware/test/CMakeLists.txt`) и `service-tui` (Python/Textual, версия +`0.2.0` из `tools/production/pyproject.toml`) — независимо версионируемые +проекты, но релиз одного без другого бесполезен сервисному инженеру: +`service-tui` — это инструмент, которым он *прошивает* плату диагностической +прошивкой, и HAB-образ `firmware_test` кладётся внутрь бандла TUI как +`firmware//firmware_test_hab.bin` (см. `just host::package-tui`, +`tools/production/docs/DEV_ARCH.md` §14). Поэтому релиз собирается как один +комплект, даже если версии независимые. + +Известное ограничение (задокументировано в `README.md`/`DEV_ARCH.md`): +Release-сборка `firmware_test` нестабильна (проблема с FCB/clock), поэтому +в бандл TUI кладётся **Debug**-образ (`FIRMWARE_BUILD_TYPE=Debug`). В релиз +GitHub имеет смысл положить оба HAB-образа отдельными assets (Debug — как +основной для TUI, Release — с пометкой «experimental», для тех, кто прошивает +через `tools/host/flash_usb.py` вручную), либо только Debug — см. открытый +вопрос в шаге 1.5. + +--- + +## Текущее состояние (снимок на момент написания плана) + +| Область | Состояние | +| --- | --- | +| `firmware_test` | Собирается, HAB-образ генерируется (`just build::hab-firmware-test-{debug,release}`), Release нестабилен | +| `service-tui` | v0.2.0, PyInstaller onedir, alpha-бандлы уже вручную собраны и прогнаны на живом железе macOS+Windows (коммиты `c694258`/`bfe4dd6`) | +| `service_tui.spec` | **Устарел относительно того, чем реально собраны протестированные alpha-бандлы** — не содержит `datas` для `spsdk`, `dcd/*.bin`, `pyproject.toml` (задокументировано в `DEV_ARCH.md` §14 и `CHANGELOG.md`「Известные ограничения」) | +| `tools/production/dist/service-tui-v0.2.0-{macos,windows}/` | Закоммичены в git (906 файлов, ~96 МБ суммарно) и **устарели относительно HEAD** — собраны до коммитов `2dbe3e6`/`22c4077`/`31e3237` (фиксы моков тестов, рефакторинг докстрингов) | +| CI (`.github/workflows/ci.yml`) | Только `build`+`test` в devcontainer на `ubuntu-latest`; не собирает `service-tui`, нет macOS/Windows раннеров, нет release-пайплайна, нет тегов в репозитории | +| `just/ci.just` | Есть рецепт `release` (→ `just build::hab-all-release`) — только firmware, ничего про упаковку TUI или публикацию на GitHub | +| Ветки | `feature-tui-monolith` на 15 коммитов впереди `dev`, ещё не смёржена; в репозитории также есть `main` — политика, какая ветка режет релизы, явно не зафиксирована | + +--- + +## Шаг 1 — Закрыть блокирующие долги перед тегом + +Без этого CI-сборка (шаг 3) не будет соответствовать тому, что уже +провалидировано на железе, — а «релиз, который не воспроизводим из +исходников» хуже отсутствия релиза. + +1. **Актуализировать `tools/production/service_tui.spec`** — добавить + `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` если понадобится), + `collect_dynamic_libs("libusbsio")`, `datas` для + `tools/host/dcd/{dcd.bin,w25q128_fdcb.bin,w25q512_fdcb.bin,ivt_flashloader.bin}` + и `pyproject.toml`. Ориентир — реальное содержимое уже собранных + alpha-бандлов в `dist/` (их можно инспектировать перед удалением из git, + см. следующий пункт). +2. **Убрать `tools/production/dist/` из git**: `git rm -r --cached + tools/production/dist` + добавить `tools/production/dist/` в + `.gitignore`. Собранные бандлы — это build-артефакты, их место в GitHub + Release assets или CI-артефактах, не в истории репозитория. +3. **(Дёшево, но не блокирует)** Убрать мёртвую зависимость `pyusb` из + `tools/production/pyproject.toml` — детект давно переведён на + `spsdk`/`serial.tools.list_ports` (Р7), ни один модуль `app/` её не + импортирует. +4. **Пересобрать бандлы локально** с исправленным spec из актуального HEAD + (`just host::package-tui` на macOS и на Windows) и повторить хотя бы + дымовой прогон чек-листа Гейта 5 из `RELEASE_ROADMAP.md` (детект SDP → + прошивка `firmware_test` → диагностика → выход). Полный деструктивный + чек-лист (обрыв USB и т.п.) уже пройден на предыдущей сборке — здесь + цель убедиться, что исправленный spec не сломал состав бандла, а не + повторять всё с нуля. +5. **Открытый вопрос:** класть ли в релиз Release-сборку `firmware_test` + вообще (сейчас нестабильна) — по умолчанию план предполагает **только + Debug**-образ как основной asset, Release не публикуется до починки + FCB/clock-проблемы. Требует подтверждения. + +--- + +## Шаг 2 — Версия и тег + +1. **Схема тега** — `firmware_test` (0.1.2) и `service-tui` (0.2.0) + версионируются независимо. Предлагается: тег вида `vX.Y.Z` = версия + `service-tui` (это главный продукт релиза для сервисного инженера), + версия `firmware_test` указывается в описании релиза отдельной строкой. + Альтернатива — раздельные теги (`tui-v0.2.0` + `firmware-v0.1.2`), если + в будущем оба проекта должны релизиться независимо друг от друга. + **Требует подтверждения**, план ниже считает первый вариант. +2. **Ветка релиза** — в репозитории есть и `dev`, и `main`, при этом + `main` в `git log` не встречается в истории `feature-tui-monolith`/`dev` + (нужно свериться отдельно, если `main` уже используется под что-то + другое). Рекомендация: смёржить `feature-tui-monolith → dev`, затем + `dev → main`, тег ставить на `main` — так `main` остаётся точкой, + соответствующей опубликованным релизам, а `dev` — интеграционной веткой. + **Требует подтверждения**, если у проекта другая договорённость про + `main`. +3. **`CHANGELOG.md`** — закрыть секцию `[Не выпущено] — service-tui: ...` + → `[YYYY-MM-DD] — v0.2.0`, вычеркнуть из «Известные ограничения» то, что + закрывается шагом 1 (spec-расхождение, `pyusb`). + +--- + +## Шаг 3 — CI: собрать релизные бинарники автоматически + +Текущий `.github/workflows/ci.yml` собирает только `firmware_test` на +`ubuntu-latest` внутри devcontainer — этого недостаточно для +кросс-платформенной упаковки `service-tui`. Нужен отдельный workflow, +не смешанный с обычным PR-циклом (см. `just/ci_workflow.md`, «Шаг 3 — +выделить release workflow»). + +Новый `.github/workflows/release.yml`, триггер — тег `v*` (плюс +`workflow_dispatch` для тестового прогона без публикации): + +| Job | Раннер | Что делает | +| --- | --- | --- | +| `firmware` | `ubuntu-latest` (тот же devcontainer-подход, что в `ci.yml`) | `just ci::release` → `hab-all-release` (по факту нужен только `firmware_test`, Debug+Release); выгрузить `firmware_test_hab.bin` (оба типа) как артефакт | +| `service-tui-macos` | `macos-latest` | скачать firmware-артефакт из job `firmware`; `uv sync` в `tools/production`; `just host::package-tui`; заархивировать `dist/service-tui-vX.Y.Z-macos/` | +| `service-tui-windows` | `windows-latest` | то же самое, PowerShell-совместимые команды (`just`/`uv` доступны на Windows) | +| `publish-release` | `ubuntu-latest`, `needs: [firmware, service-tui-macos, service-tui-windows]` | скачать все артефакты, создать GitHub Release через `gh release create` / `softprops/action-gh-release@v2`, прикрепить `firmware_test_hab.bin` (Debug, + Release с пометкой experimental, если решение по шагу 1.5 — «класть оба»), `service-tui-vX.Y.Z-macos.zip`, `service-tui-vX.Y.Z-windows.zip` | + +Важные нюансы: + +- Firmware для бандла TUI собирается **один раз** в job `firmware` и + передаётся в macOS/Windows job'ы артефактом — пересобирать ARM-прошивку + на каждом раннере отдельно избыточно (и на macOS/Windows раннерах нет + подготовленного devcontainer/toolchain). +- Аппаратные гейты (детект SDP на живой плате, деструктивные сценарии + обрыва USB) **CI выполнить не может** — GitHub-hosted раннеры не видят + реальное USB-устройство. Это ручной шаг, который уже пройден один раз + вручную (коммиты «MacOS tested»/«Windows tested») и должен повторяться + вручную перед каждым релизом, пока не поднят self-hosted HIL-раннер + (см. `just/ci_workflow.md`, «Шаг 5»). План релиза это не блокирует, но + release notes должны явно фиксировать, что сборка прошла ручную проверку + на железе, а не только CI. + +--- + +## Шаг 4 — Ручные шаги перед Publish + +1. Скачать `service-tui-vX.Y.Z-{macos,windows}.zip`, собранные именно CI + (не локальную сборку из шага 1.4) — прогнать сокращённый чек-лист Гейта + 5: детект SDP → прошивка `firmware_test` → диагностика на обеих ОС. + Цель — убедиться, что CI-сборка не разошлась с уже провалидированной + локальной. +2. Обновить `tools/production/README.md`/корневой `README.md` — ссылка на + релиз/инструкция «откуда скачать сервисному инженеру». + +## Шаг 5 — Публикация + +```bash +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +— триггерит `release.yml`. Проверить, что все 3 asset'а прикрепились и +release notes корректны (описание можно сгенерировать из секции +`CHANGELOG.md` за этот релиз + `gh release create --generate-notes` как +дополнение). Если Гейт 6 (`RELEASE_ROADMAP.md`) закрыт не полностью +(например, POST-1 сознательно отложен — это нормально, он и заявлен как +пост-релизный) — релиз всё равно можно публиковать как обычный, а не +pre-release, POST-1 не блокирует v1 по замыслу roadmap. + +--- + +## Шаг 6 — Развитие CI после первого релиза (не блокирует, но логично заложить сразу) + +Эти пункты уже зафиксированы в `just/ci_workflow.md` («Рекомендуемые +следующие шаги»), возвращаемся сюда после первого релиза: + +- `lint` job (`just ci::lint` сейчас заглушка) — `clang-format --dry-run + --Werror` + `clang-tidy`. +- Coverage (`just ci::_coverage` уже есть, но не подключён в workflow). +- Публикация devcontainer image в GHCR — сократит время `firmware` job в + `release.yml` и обычном `ci.yml` (сейчас образ пересобирается в каждой + job, даже с layer-кэшем). +- Self-hosted HIL lane — единственный способ когда-нибудь автоматизировать + то, что сейчас в шаге 4 делается руками. + +--- + +## Сводная последовательность + +``` +Шаг 1 (spec + dist из git + пересборка) + │ +Шаг 2 (тег/ветка/CHANGELOG — решения по открытым вопросам) + │ +Шаг 3 (release.yml: firmware → macOS/Windows package → publish) + │ +Шаг 4 (ручная проверка CI-бинарников на железе) + │ +Шаг 5 (git tag → publish) + │ +Шаг 6 (lint/coverage/GHCR/HIL — после релиза, не блокирует) +``` + +## Открытые вопросы, требующие решения пользователя + +| # | Вопрос | Где всплывает | +| --- | --- | --- | +| 1 | Класть ли Release-сборку `firmware_test` в релиз (сейчас нестабильна) | Шаг 1.5 | +| 2 | Схема тега — один `vX.Y.Z` (=версия TUI) или раздельные теги firmware/TUI | Шаг 2.1 | +| 3 | Тег ставится на `main` (после `dev → main`) или сразу на `dev`/на самой feature-ветке | Шаг 2.2 | diff --git a/README.md b/README.md index a79e6d8..e713fb7 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,19 @@ | Проект | Путь | Описание | | ------------------- | ---------------------- | -------------------------------------------------------------------------------------- | -| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | -| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | -| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | +| Тестовая прошивка (✅ реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | +| Загрузчик (⏳ запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | +| Production прошивка (⏳ запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | + +--- + +## Инструменты (`tools/`) + +| Инструмент | Путь | Назначение | +| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| Сервисный TUI | `tools/production/` | Диагностика и прошивка готовых плат сервисным инженером (Textual, standalone-бинарь). [README](tools/production/README.md) | +| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке (`sdphost`/`blhost`/`nxpimage`/`pyOCD`). [README](tools/host/README.md) | +| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов (pyOCD + M5StampPLC). [README](tools/hil/README.md) | --- @@ -65,7 +75,8 @@ just host::debug-server # GDB-сервер для отладки | NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored | | Unity, fff, SEGGER RTT | vendored | | pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` | -| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` | +| spsdk (nxpimage, blhost, sdphost — dev-CLI) | `tools/host/uv.lock` | +| spsdk (McuBoot/SDP/HabImage — прямой Python API), Textual | `tools/production/uv.lock` | Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей). diff --git a/bsp/can/README.md b/bsp/can/README.md index d6e851e..342f9af 100644 --- a/bsp/can/README.md +++ b/bsp/can/README.md @@ -65,7 +65,7 @@ bsp_status_t bsp_can_set_filter(uint8_t idx, uint32_t id, uint32_t mask, bool is_extended); bsp_status_t bsp_can_accept_all(void); -bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_cb_t cb, void *p_ctx); +bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t cb, void *p_ctx); ``` **Коды возврата `bsp_can_send()`:** @@ -147,7 +147,7 @@ bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true); add_host_test( NAME test_bsp_can SOURCES can/test_bsp_can.c - ${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_can.c + ${PROJECT_SOURCE_DIR}/bsp/can/src/can.c ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c INCLUDES ${PROJECT_SOURCE_DIR}/bsp/can/include @@ -160,7 +160,7 @@ add_host_test( **Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`: ```c -#include "can_mock.h" +#include "can_mocks.h" void setUp(void) { CAN_MOCK_RESET_ALL(); } diff --git a/bsp/can/src/can.c b/bsp/can/src/can.c index 57fb3e6..e0bac61 100644 --- a/bsp/can/src/can.c +++ b/bsp/can/src/can.c @@ -161,18 +161,15 @@ static uint32_t poll_rx_mailboxes(void) uint8_t mb_idx = RX_MB_FIRST + i; - /* Проверяем флаг готовности MB. */ uint64_t mb_flag = (uint64_t) 1U << mb_idx; if (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, mb_flag) == 0U) { continue; } - /* Читаем фрейм из MB. */ flexcan_frame_t sdk_frame; status_t sdk_status = FLEXCAN_ReadRxMb(BSP_CAN_BASE, mb_idx, &sdk_frame); - /* Очищаем флаг. */ FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, mb_flag); if ((sdk_status == kStatus_Success) || (sdk_status == kStatus_FLEXCAN_RxOverflow)) @@ -180,7 +177,6 @@ static uint32_t poll_rx_mailboxes(void) bsp_can_frame_t bsp_frame; frame_from_sdk(&sdk_frame, &bsp_frame); - /* Сериализуем фрейм побайтово в ring buffer. */ ring_buffer_write(&g_s_rx_ring, (const uint8_t *) &bsp_frame, sizeof(bsp_frame)); received++; } @@ -222,16 +218,13 @@ bsp_status_t bsp_can_init(const bsp_can_config_t *p_config) return BSP_ERR_PARAM; } - /* Если уже инициализирован — сначала деинициализируем. */ if (g_s_initialized) { bsp_can_deinit(); } - /* Инициализация ring buffer. */ ring_buffer_init(&g_s_rx_ring, g_s_rx_ring_storage, RX_RING_SIZE); - /* Конфигурация FlexCAN. */ flexcan_config_t flexcan_cfg; FLEXCAN_GetDefaultConfig(&flexcan_cfg); @@ -306,7 +299,6 @@ bsp_status_t bsp_can_set_filter(uint8_t index, uint32_t can_id, uint32_t mask, b uint8_t mb_idx = RX_MB_FIRST + index; - /* Конфигурация RX MB. */ flexcan_rx_mb_config_t rx_mb_cfg; rx_mb_cfg.type = kFLEXCAN_FrameTypeData; @@ -422,14 +414,12 @@ bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms) flexcan_frame_t sdk_frame; frame_to_sdk(p_frame, &sdk_frame); - /* Записать фрейм в TX MB. */ status_t wr_status = FLEXCAN_WriteTxMb(BSP_CAN_BASE, TX_MB_IDX, &sdk_frame); if (wr_status != kStatus_Success) { return BSP_ERR_BUSY; } - /* Ждать завершения передачи с таймаутом. */ uint64_t tx_flag = (uint64_t) 1U << TX_MB_IDX; uint32_t start_ms = bsp_tick_get_ms(); @@ -442,7 +432,6 @@ bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms) } } - /* Очистить флаг завершения. */ FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, tx_flag); return BSP_OK; @@ -471,16 +460,13 @@ bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms) for (;;) { - /* Опросить все активные MB, сложить в ring buffer. */ poll_rx_mailboxes(); - /* Попробовать извлечь фрейм. */ if (try_dequeue_frame(p_frame)) { return BSP_OK; } - /* Проверить таймаут. */ uint32_t elapsed = bsp_tick_get_ms() - start_ms; if (elapsed >= timeout_ms) { diff --git a/bsp/mqs/include/bsp/mqs.h b/bsp/mqs/include/bsp/mqs.h index 09a6240..76dd50c 100644 --- a/bsp/mqs/include/bsp/mqs.h +++ b/bsp/mqs/include/bsp/mqs.h @@ -1,10 +1,10 @@ /** * @file bsp/mqs.h - * @brief BSP: Medium Quality Sound (MQS) — SAI1 + eDMA + MQS. + * @brief BSP: Medium Quality Sound (MQS) — SAI3 + eDMA + MQS. * - * Слой абстракции над SAI1/eDMA/MQS для монофонического аудио-выхода. + * Слой абстракции над SAI3/eDMA/MQS для монофонического аудио-выхода. * Физически на плате выведен один канал (MQS_RIGHT, GPIO_AD_B0_04); - * SAI1 требует стерео-буфер — оба канала всегда идентичны. + * SAI3 требует стерео-буфер — оба канала всегда идентичны. * * Режимы использования: * - firmware_test: bsp_mqs_play_blocking() — синхронная подача @@ -38,14 +38,14 @@ extern "C" * Параметры аудио-потока * ----------------------------------------------------------------------- */ -/** Частота дискретизации, Гц. Небольшое отклонение (~0.5%) из-за - * источника SAI1_CLK_ROOT (System PLL PFD2, не Audio PLL). */ +/** Частота дискретизации, Гц. Источник — SAI3_CLK_ROOT (Audio PLL / 8 / 8), + * делитель MCLK подобран точно (8), отклонения нет. */ #define BSP_MQS_SAMPLE_RATE_HZ (44100U) /** Разрядность PCM. MQS поддерживает только 16 бит. */ #define BSP_MQS_BIT_WIDTH (16U) -/** Количество каналов в буфере. SAI1+MQS требует стерео; правый == левый. */ +/** Количество каналов в буфере. SAI3+MQS требует стерео; правый == левый. */ #define BSP_MQS_CHANNELS (2U) /** Байт на один моно-сэмпл (16 бит → 2 байта). */ @@ -73,11 +73,11 @@ extern "C" * ----------------------------------------------------------------------- */ /** - * @brief Инициализация MQS-подсистемы: SAI1, eDMA, DMAMUX, MQS. + * @brief Инициализация MQS-подсистемы: SAI3, eDMA, DMAMUX, MQS. * - * Включает тактирование SAI1 (kCLOCK_Sai1), настраивает SAI1 в режиме + * Включает тактирование SAI3 (kCLOCK_Sai3), настраивает SAI3 в режиме * TX Master, 16 бит, стерео, 44100 Гц, инициализирует eDMA канал 0 - * (DMAMUX source kDmaRequestMuxSai1Tx) и MQS-модуль. + * (DMAMUX source kDmaRequestMuxSai3Tx) и MQS-модуль. * * Пин GPIO_AD_B0_04 (MQS_RIGHT) уже замультиплексирован в BOARD_InitPins(). * MQS oversample (×32) уже выставлен в BOARD_BootClockRUN(). @@ -88,7 +88,7 @@ extern "C" bsp_status_t bsp_mqs_init(void); /** - * @brief Деинициализация: остановить DMA, сбросить SAI1 и MQS. + * @brief Деинициализация: остановить DMA, сбросить SAI3 и MQS. * * Безопасно вызывать даже если воспроизведение уже завершилось. * После вызова модуль требует повторного bsp_mqs_init(). @@ -152,7 +152,7 @@ extern "C" * ----------------------------------------------------------------------- */ /** - * @brief Инициализация усилителя: PWM4 SM0, 16 кГц, duty 50%. + * @brief Инициализация усилителя: PWM4 SM0, 12 кГц, duty 50%. * * Настраивает XBARA1 (fault disable), PWM4 submodule 0 channel A. * Вызывать до bsp_mqs_play() — без ШИМ на VOLUME усиление равно нулю. diff --git a/bsp/mqs/src/mqs.c b/bsp/mqs/src/mqs.c index ac539d3..e3ff324 100644 --- a/bsp/mqs/src/mqs.c +++ b/bsp/mqs/src/mqs.c @@ -1,19 +1,21 @@ /** * @file bsp_mqs.c - * @brief BSP MQS: SAI1 TX + eDMA + MQS для MIMXRT1052CVJ5B. + * @brief BSP MQS: SAI3 TX + eDMA + MQS для MIMXRT1052CVJ5B. * * Тактирование: - * SAI1_CLK_ROOT = SysPLL × (18/27) / (SAI1_CLK_PRED+1=4) / (SAI1_CLK_PODF+1=2) - * ≈ 63 529 411 Гц (BOARD_BOOTCLOCKRUN_SAI1_CLK_ROOT) + * Audio PLL = 24 МГц × (30 + 66/625) = 722.534 МГц + * SAI3_CLK_ROOT = Audio PLL / 8 / 8 = 11 289 600 Гц + * (kCLOCK_Sai3Mux=2, Sai3PreDiv=7, Sai3Div=7, + * BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT) * Bit clock = 44100 × 16 × 2 = 1 411 200 Гц - * MCLK делитель = 63 529 411 / 1 411 200 ≈ 45.0 (погрешность ~0.5 %) + * MCLK делитель = 11 289 600 / 1 411 200 = 8 (точно, без погрешности) * * MQS oversample = 32, уже выставлен в BOARD_BootClockRUN() через * IOMUXC_MQSConfig(IOMUXC_GPR, kIOMUXC_MqsPwmOverSampleRate32, 0). * * Пин: GPIO_AD_B0_04 → MQS_RIGHT — замультиплексирован в BOARD_InitPins(). * - * eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai1Tx. + * eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai3Tx. * Канал 0 зарезервирован за bsp_mqs. Прочие модули — каналы 1+. * * SAI API (SDK 2.4.7 / fsl_sai.h, fsl_sai_edma.h 2.7.3): @@ -47,17 +49,17 @@ #define MQS_SAI_CLOCK_GATE kCLOCK_Sai3 #define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT -/** eDMA канал, выделенный под SAI1 TX. */ +/** eDMA канал, выделенный под SAI3 TX. */ #define MQS_DMA_CHANNEL (0U) -/** DMAMUX запрос для SAI1 TX. */ +/** DMAMUX запрос для SAI3 TX. */ #define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx /** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */ #define MQS_DMA_IRQ_PRIORITY (5U) #define MQS_HMCLK_GATE kCLOCK_Mqs /** - * FIFO watermark — половина глубины FIFO SAI1. + * FIFO watermark — половина глубины FIFO SAI3. * FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает * глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную * латентность DMA: запрос формируется когда в FIFO остаётся место для @@ -108,11 +110,6 @@ static void mqs_edma_callback(I2S_Type *p_base, sai_edma_handle_t *p_handle, sta /* -------------------------------------------------------------------------- * Публичный API * ----------------------------------------------------------------------- */ -/* - * AUDIO PLL setting: Frequency = Fref * (DIV_SELECT + NUM / DENOM) - * = 24 * (32 + 768/1000) - * = 786.432 MHz - */ bsp_status_t bsp_mqs_init(void) { @@ -121,7 +118,7 @@ bsp_status_t bsp_mqs_init(void) return BSP_OK; } - /* --- Тактирование SAI1 --- */ + /* --- Тактирование SAI3 --- */ CLOCK_EnableClock(MQS_SAI_CLOCK_GATE); /* --- Тактирование MQS (CCGR0[CG2]) --- */ @@ -132,10 +129,10 @@ bsp_status_t bsp_mqs_init(void) IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false); IOMUXC_MQSEnable(IOMUXC_GPR, true); - /* --- SAI1: базовая инициализация (снимает reset, включает clock gate) --- */ + /* --- SAI3: базовая инициализация (снимает reset, включает clock gate) --- */ SAI_Init(MQS_SAI_BASE); - /* --- SAI1 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */ + /* --- SAI3 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */ sai_transceiver_t sai_cfg; SAI_GetLeftJustifiedConfig(&sai_cfg, kSAI_WordWidth16bits, kSAI_Stereo, @@ -159,7 +156,7 @@ bsp_status_t bsp_mqs_init(void) EDMA_Init(DMA0, &dma_cfg); EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL); - /* --- DMAMUX: канал 0 → SAI1 TX --- */ + /* --- DMAMUX: канал 0 → SAI3 TX --- */ DMAMUX_Init(DMAMUX); DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE); DMAMUX_EnableChannel(DMAMUX, MQS_DMA_CHANNEL); @@ -188,7 +185,7 @@ void bsp_mqs_deinit(void) } SAI_TransferTerminateSendEDMA(MQS_SAI_BASE, &s_sai_tx_handle); - /* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll). + /* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll) для SAI3. * SAI_TxSoftwareReset() отсутствует в данной версии SDK. */ SAI_TxReset(MQS_SAI_BASE); IOMUXC_MQSEnable(IOMUXC_GPR, false); diff --git a/bsp/mqs/src/mqs_amp.c b/bsp/mqs/src/mqs_amp.c index b3fd481..0671b03 100644 --- a/bsp/mqs/src/mqs_amp.c +++ b/bsp/mqs/src/mqs_amp.c @@ -9,7 +9,7 @@ * аудио-сигнала на входе (MQS_RIGHT через RC-фильтр → SOUND_OUT). * * Управление громкостью: - * PWM4 SM0 PWM_A, частота 16 кГц, центрально-симметричный режим. + * PWM4 SM0 PWM_A, частота 12 кГц, центрально-симметричный режим. * duty 0% → DC_VOL ≈ 0 В → усиление минимально (тишина). * duty 50% → DC_VOL ≈ 2.5 В → номинальная громкость. * duty 100%→ DC_VOL ≈ 5 В → максимальное усиление. @@ -22,7 +22,7 @@ * Тактирование: * IPG clock = AHB/4 = 600/4 = 150 МГц. * PWM prescaler = /16 → PWM clock = 9.375 МГц. - * Fpwm = 16000 Гц (центрально-симметричный режим). + * Fpwm = 9 375 000 / 586 / 2 = 12000 Гц (центрально-симметричный режим). */ #include "bsp/mqs.h" @@ -96,7 +96,7 @@ bsp_status_t bsp_mqs_amp_init(void) /* --- ForceSignal: использовать нормальный PWM-сигнал --- */ PWM_SetupForceSignal(AMP_PWM_BASE, AMP_PWM_SUBMODULE, AMP_PWM_CHANNEL, kPWM_UsePwm); - /* --- PWM-сигнал: 16 кГц, центрально-симметричный, duty 50% --- */ + /* --- PWM-сигнал: 12 кГц, центрально-симметричный, duty 50% --- */ const pwm_signal_param_t PWM_SIGNAL = { .pwmChannel = AMP_PWM_CHANNEL, .dutyCyclePercent = AMP_DEFAULT_DUTY, diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h index d671a1f..a663b4f 100644 --- a/bsp/opto/include/bsp/opto.h +++ b/bsp/opto/include/bsp/opto.h @@ -12,7 +12,6 @@ * Пин LOW (тока нет) → BSP_OPTO_STATE_INACTIVE * * Режимы каналов (bsp_opto_ch_mode_t): - * ы * BSP_OPTO_MODE_LEVEL — IN1, IN2 * Детектирование уровня с программным дебаунсом. * ISR переключает направление прерывания (RISING↔FALLING) после каждого фронта, diff --git a/bsp/provisioning/include/bsp/provisioning.h b/bsp/provisioning/include/bsp/provisioning.h index 4607b9a..b3eea7d 100644 --- a/bsp/provisioning/include/bsp/provisioning.h +++ b/bsp/provisioning/include/bsp/provisioning.h @@ -25,9 +25,8 @@ * @brief Прочитать уникальный идентификатор чипа из OCOTP. * * Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]). - * Результат записывается в big-endian порядке: p_uid[0] — старший байт CFG1, - * p_uid[7] — младший байт CFG0. Hex-строка совпадает с визуальным порядком слов - * в Reference Manual (MIMXRT1052RM Table 46-2). + * Результат записывается в нативном порядке байт (little-endian на Cortex-M7): + * p_uid[0..3] = CFG0 (UID[31:0]), p_uid[4..7] = CFG1 (UID[63:32]). * * Функция выполняет OCOTP_Init() и включает clock gate перед чтением. * Clock gate остаётся открытым после вызова (паттерн проекта). diff --git a/bsp/uart_host/README.md b/bsp/uart_host/README.md index 5ce90d6..dde9926 100644 --- a/bsp/uart_host/README.md +++ b/bsp/uart_host/README.md @@ -44,12 +44,16 @@ flowchart TD ```c bsp_status_t bsp_uart_host_init(uint32_t baud); +void bsp_uart_host_deinit(void); bsp_status_t bsp_uart_host_write(const uint8_t *p_data, size_t len); bsp_status_t bsp_uart_host_write_str(const char *p_str); size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms); int32_t bsp_uart_host_read_byte(uint32_t timeout_ms); + +size_t bsp_uart_host_rx_available(void); /* байт в RX-буфере прямо сейчас */ +void bsp_uart_host_rx_flush(void); /* сбросить содержимое RX-буфера */ ``` `bsp_uart_host_read()` возвращает фактически прочитанное количество байт — diff --git a/bsp/uart_host/include/bsp/uart_host.h b/bsp/uart_host/include/bsp/uart_host.h index 7ad62dd..5a66e74 100644 --- a/bsp/uart_host/include/bsp/uart_host.h +++ b/bsp/uart_host/include/bsp/uart_host.h @@ -26,7 +26,7 @@ /* -------------------------------------------------------------------------- */ /* Константы */ -/* -------------------------------------------------s------------------------- */ +/* -------------------------------------------------------------------------- */ /** Передать в timeout_ms чтобы ждать данные бесконечно. */ #define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX) diff --git a/bsp/uart_host/src/uart_host.c b/bsp/uart_host/src/uart_host.c index de8e245..0dcab89 100644 --- a/bsp/uart_host/src/uart_host.c +++ b/bsp/uart_host/src/uart_host.c @@ -84,17 +84,14 @@ bsp_status_t bsp_uart_host_init(uint32_t baud_rate) return BSP_ERR_INIT; } - /* Инициализация кольцевого буфера. */ if (!ring_buffer_init(&g_s_rx_ring, g_s_rx_buf, BSP_UART_HOST_RX_BUFFER_SIZE)) { /* Размер не степень двойки — ошибка конфигурации. */ return BSP_ERR_INIT; } - /* Тактирование LPUART1. */ CLOCK_EnableClock(kCLOCK_Lpuart1); - /* Настройка периферии. */ lpuart_config_t config; LPUART_GetDefaultConfig(&config); config.baudRate_Bps = baud_rate; @@ -191,7 +188,6 @@ size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms) continue; } - /* Буфер пуст — проверяем таймаут. */ if (timeout_ms == 0U) { break; diff --git a/bsp/usb_cdc/README.md b/bsp/usb_cdc/README.md index b07bd32..7697c57 100644 --- a/bsp/usb_cdc/README.md +++ b/bsp/usb_cdc/README.md @@ -19,7 +19,9 @@ COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Испол Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s). PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`. -**VID/PID**: `0x1234` / `0x0001` — placeholder, заменить на производственные. +**VID/PID**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же +идентификатор, что `tools/production/` (service-tui) использует для +детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`). --- diff --git a/bsp/usb_cdc/include/bsp/usb_cdc.h b/bsp/usb_cdc/include/bsp/usb_cdc.h index e7c0d11..0257cba 100644 --- a/bsp/usb_cdc/include/bsp/usb_cdc.h +++ b/bsp/usb_cdc/include/bsp/usb_cdc.h @@ -36,7 +36,7 @@ extern "C" * * @pre board_hw_init() вызван (MPU настроен, NonCacheable регион активен). * - * @return BSP_OK при успехе, BSP_ERR_HW при ошибке инициализациистека. + * @return BSP_OK при успехе, BSP_ERR_HW при ошибке инициализации стека. */ bsp_status_t bsp_usb_cdc_init(void); diff --git a/bsp/usb_cdc/src/usb_cdc.c b/bsp/usb_cdc/src/usb_cdc.c index e634677..7036929 100644 --- a/bsp/usb_cdc/src/usb_cdc.c +++ b/bsp/usb_cdc/src/usb_cdc.c @@ -566,8 +566,6 @@ bsp_status_t bsp_usb_cdc_init(void) USB_DeviceIsrEnable(); - /* FIXME:Задержка для стабилизации DP pull-down. */ - //SDK_DelayAtLeastUs(USB_ATTACH_DELAY_US, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY); bsp_delay(USB_ATTACH_DELAY_US / 1000); USB_DeviceRun(g_usbDeviceHandle); diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 61bd5f8..dc822f5 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -210,6 +210,7 @@ flowchart LR │ ├── 01_test_uart.py ← HIL тест bsp_uart_host (без M5) │ ├── 02_test_opto.py ← HIL тест bsp_opto (через M5StampPLC) │ ├── 03_test_can.py ← HIL тест bsp_can +│ ├── 04_test_button.py ← HIL тест bsp_button (интерактивный, оператор) │ ├── 05_test_usb_cdc.py ← HIL тест USB CDC (bsp_usb_cdc, UART CLI) │ ├── 06_test_firmware_opto.py ← HIL тест opto через firmware_test CDC │ ├── 06_test_firmware_can.py ← HIL тест CAN через firmware_test CDC diff --git a/docs/HOW_TO_FLASH.md b/docs/HOW_TO_FLASH.md index d47c9cb..33546f4 100644 --- a/docs/HOW_TO_FLASH.md +++ b/docs/HOW_TO_FLASH.md @@ -81,16 +81,21 @@ auto-config не подтверждена — см. 1.5. `service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не в этом репозитории (например, старые платы с W25Q512), тем же способом -(USB SDP), но с двумя отличиями от штатного пути: +(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная +реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает +in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`, +`McuBoot`, `SDP`), без единого subprocess: - HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на - лету через `nxpimage`, а не заранее через `just build::hab-*` -- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`, - буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config - для 4-байтной адресации не проверялся, решили на него не полагаться + лету через `HabImage` (spsdk), а не заранее через `just build::hab-*` +- FCB пишется **явно** (`mboot.write_memory()` с готовым блобом + `tools/host/dcd/w25qXXX_fdcb.bin`, буквальная запись вместо + `configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не + проверялся, решили на него не полагаться -Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь -(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему +Подробности конвейера — в [tools/production/docs/DEV_ARCH.md](../tools/production/docs/DEV_ARCH.md), +§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через +`just host::flash`, что через `service-tui`) не меняется и по-прежнему использует auto-config Flashloader, как описано в 1.4. --- diff --git a/docs/testing/PROTOCOL.md b/docs/testing/PROTOCOL.md index 579c2b5..7a5ea15 100644 --- a/docs/testing/PROTOCOL.md +++ b/docs/testing/PROTOCOL.md @@ -4,7 +4,13 @@ > > Документ описывает протокол обмена между диагностической прошивкой > (`firmware_test`) и хостовым ПО сервисного инженера. -> Актуален для: `firmware_test v0.1.0+`, `protocol.h v2`. +> Актуален для: `firmware_test v0.1.2+`, `protocol.h v2`. +> +> Полный справочник по каждому тесту (потоки, коды `detail`, таблица HIL +> реле) — в [firmware/test/README.md](../../firmware/test/README.md) и +> [firmware/test/src/tests/README.md](../../firmware/test/src/tests/README.md). +> Этот документ — сжатый протокольный обзор с точки зрения хостового ПО +> (TUI/pytest), а не полное описание тест-логики. --- @@ -79,7 +85,7 @@ sequenceDiagram participant T as Таргет Note over T: прошивка загружена через USB SDP - T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} + T-->>H: {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} H->>T: {"type":"cmd","cmd":"ping"} T-->>H: {"type":"pong"} @@ -169,6 +175,17 @@ sequenceDiagram ← {"ok":false,"error":"UID_READ_ERR"} ``` +### `get_version` — чтение версии прошивки + +```json +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + +Дублирует значение `"fw"` из `session_start` — полезно, если хост +подключился уже после того, как `session_start` был отправлен (может быть +пропущен, это одноразовое событие сразу после старта). + ### `run_selected` — запуск подмножества тестов Запускает тесты по списку ID. Порядок выполнения — по реестру таргета, @@ -199,7 +216,7 @@ sequenceDiagram ### `session_start` ```json -{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} +{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} ``` ### `test_begin` @@ -222,6 +239,11 @@ sequenceDiagram `detail` — ASCII-строка до 95 символов. При `pass` — пустая. +### `test_list` + +Ответ на `list_tests` — массив дескрипторов теста (`id`, `name`, +`critical`, `requires_hil`), см. пример в разделе `list_tests` выше. + ### `progress` ```json @@ -230,6 +252,11 @@ sequenceDiagram Промежуточные шаги внутри теста. Используется в `usd`. +### `uid_response` / `version_response` + +Ответы на `get_uid`/`get_version` — см. описание соответствующих команд +выше. + ### `confirm_request` ```json @@ -270,25 +297,30 @@ sequenceDiagram | `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID | | `LINE_TOO_LONG` | Входящая строка превысила 128 байт | | `BUSY` | Таргет выполняет тест, новая команда отклонена | +| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) | --- ## Матрица тестов -| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | -| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ | -| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | -| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | -| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | -| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) | -| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) | -| `can` | CAN | HIL | ❌ | ✅ | ❌ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | -| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ | -| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | +Порядок — как в реестре `k_registry[]` (`test_runner.c`); полная версия с +кодами `detail` и HIL-таблицей реле — в +[firmware/test/README.md §Матрица тестов](../../firmware/test/README.md#матрица-тестов). + +| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | +| --------- | ------------------- | ------------------ | -------- | -------- | --------------------- | +| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | +| `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ | +| `usd` | microSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | +| `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ (6 шагов, см. ниже) | +| `buttons` | Test Buttons | interactive | ❌ | ❌ | ✅ (нажать кнопки) | +| `opto` | Opto Inputs | HIL | ❌ | ✅ | ❌ (авто, 6 шагов) | +| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ (авто, 2 шага) | +| `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ (слышимость тона) | **Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive** — -требует `confirm_request`; **HIL** — требует M5StampPLC. +требует `confirm_request`, отвечает оператор; **HIL** — требует M5StampPLC, +confirm автоматический (без оператора). --- @@ -320,6 +352,10 @@ sequenceDiagram ### Display (RGB888) +Шесть шагов: Red → Green → Blue → White, затем два ротационных (диагностика +непропаянных LR/UD пинов на TFT7/8/10). Тест прерывается на **первом** +неподтверждённом шаге. + ```mermaid sequenceDiagram participant H as Хост @@ -333,10 +369,36 @@ sequenceDiagram T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000} H->>T: {"type":"confirm","id":"display_blue","confirmed":true} T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000} - H->>T: {"type":"confirm","id":"display_white","confirmed":false} - T-->>H: {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"} + H->>T: {"type":"confirm","id":"display_white","confirmed":true} + T-->>H: {"type":"confirm_request","id":"display_rot0","prompt":"Слева КРАСНЫЙ, справа СИНИЙ?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"display_rot0","confirmed":true} + T-->>H: {"type":"confirm_request","id":"display_rot_base","prompt":"Красный/синий поменялись сторонами?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"display_rot_base","confirmed":true} + T-->>H: {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} ``` +При отказе/таймауте на любом шаге: `status:"fail"`, +`detail:" not confirmed"` (например, `"display_white not confirmed"`). + +### MQS Audio Out + +Таргет ~4с играет мелодию через MQS + усилитель, затем запрашивает +подтверждение слышимости — единственный тест с аудио-confirm: + +```mermaid +sequenceDiagram + participant H as Хост + participant T as Таргет + + T-->>H: {"type":"test_begin","id":"mqs",...} + Note over T: ~4с воспроизведение тона (A4, затем E5) + T-->>H: {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"mqs_tone","confirmed":true} + T-->>H: {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +``` + +Отказ/таймаут → `status:"fail"`, `detail:"operator: no sound"`. + ### Кнопки ```mermaid @@ -393,32 +455,15 @@ firmware/test/src/ ├── test_usd.c ├── test_display.c ├── test_buttons.c + ├── test_opto.c ├── test_can.c - ├── test_uart_ttl.c - ├── test_uart_iso.c - └── test_opto.c + └── test_mqs.c ``` ### Добавление нового теста -1. Создать `firmware/test/src/tests/test_foo.c`. -2. Объявить дескриптор: - -```c -const test_module_t k_test_foo = { - .id = "foo", - .name = "Foo Peripheral", - .critical = false, - .requires_hil = false, - .pre_confirm_prompt = NULL, - .init = NULL, - .run = test_foo_run, - .deinit = NULL, -}; -``` - -1. Добавить `&k_test_foo` в реестр `test_runner.c`. -2. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета. +Пошаговый гайд с шаблонами (self-тест, интерактивный, pre-confirm) — +[firmware/test/README.md §Как добавить новый тест](../../firmware/test/README.md#как-добавить-новый-тест). --- diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 8139258..5272bcb 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -166,12 +166,18 @@ ls build/target-debug/tests/target//test_.elf ## Шаг 5 — `conftest.py`: добавить фикстуры -### Базовый тест (без M5) +### Любой тест — фикстура загрузки всегда зависит от `m5` + +M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а +не только сигнальными реле — поэтому `loaded_` зависит от `m5` **во всех +случаях**, даже если сам тест не использует реле для сигналов (например, +`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается +подключиться к обесточенной плате. ```python -# 1. Фикстура загрузки +# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD @pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest) -> None: +def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target//test_.elf", @@ -184,26 +190,10 @@ _UART_FIXTURE_MAP = { } ``` -### Тест с M5 - -```python -# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF -@pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: - _load_elf( - request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", - ) - -# 2. UART-фикстура — та же одна строка -_UART_FIXTURE_MAP = { - ... - "uart_": "loaded_", -} -``` - -**Правило:** если тест управляет железом через M5 — `loaded_` должен явно -зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания. +Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_` +(она всегда одна и та же), а в том, использует ли сам **тест-кейс** +`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами +помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже). --- diff --git a/docs/testing/host/HOST_CREATE_TEST.md b/docs/testing/host/HOST_CREATE_TEST.md index 1fc921d..a05b283 100644 --- a/docs/testing/host/HOST_CREATE_TEST.md +++ b/docs/testing/host/HOST_CREATE_TEST.md @@ -1,232 +1,193 @@ -# Отладка прошивок через SWD + GDB +# Добавление нового host unit-теста -## Обзор архитектуры +Пошаговый гайд для разработчика. Полный справочник по Unity/FFF API, +структуре stub-хедеров и типичным ловушкам — в +[tests/host/README.md](../../../tests/host/README.md). Этот документ — +только про шаги добавления нового теста в сборку. -Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это -позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, -не проводя USB-пробник внутрь Docker. +--- + +## Обзор стека ```mermaid flowchart LR - subgraph Host["Хост (macOS / Linux)"] - DS["just host::debug-server\npyocd gdbserver :3333"] - ML["MCU-Link (CMSIS-DAP)"] - DS --> ML + subgraph DC["Devcontainer (единственное место запуска)"] + C["tests/host/<dir>/test_<name>.c\nUnity [+ fff]"] + CP["CMakePresets.json\nhost-debug / host-release"] + JB["just/build.just\ntest-host"] + C --> CP --> JB end - - subgraph DC["Devcontainer"] - CD["cortex-debug\n(VSCode F5)"] - GDB["arm-none-eabi-gdb\nсимволы из .elf"] - CD --> GDB - end - - Board["MIMXRT1052\nFlash / SDRAM\nSEGGER RTT буфер"] - - GDB -->|"TCP host.docker.internal:3333"| DS - ML -->|"SWD"| Board ``` -**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера -GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker, -резолвится в IP хост-машины. +Host-тесты компилируются `clang-17` **на хосте** (не ARM GCC), исполняются +как обычные нативные бинарники под `ctest`. Никакого железа не требуется — +в отличие от HIL-тестов (см. [../hil/HIL_CREATE_TEST.md](../hil/HIL_CREATE_TEST.md)). --- -## Компоненты +## Шаг 0 — Определить категорию модуля -### На хосте +| Категория | Инструментарий | Пример | +| ----------------------------------------- | ------------------ | ----------------------------------- | +| **A** — платформонезависимый | Только Unity | `protocol.c`, `test_runner.c`, `ring_buffer.c` | +| **B** — BSP-модуль (зависит от NXP SDK) | Unity + fff + stub-хедеры | `bsp/led`, `bsp/opto`, `bsp/can`, `bsp/button` | -| Компонент | Роль | Источник | -| --------------------------------- | ------------------------------- | -------------------------- | -| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` | -| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате | -| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` | -| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` | -| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` | -| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool | - -### В devcontainer - -| Компонент | Роль | -| -------------------------------------- | ---------------------------------------------- | -| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте | -| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры | -| `.vscode/launch.json` | Конфигурации запуска отладки | -| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом | -| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) | -| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии | - -### Конфигурация - -Параметры отладки задаются в `.env`: - -```bash -GDB_PORT=3333 -PYOCD_TARGET=mimxrt1050_quadspi -PYOCD_FREQUENCY=4000000 -FCB_PATH=tools/host/dcd/w25q128_fdcb.bin -``` +Полное объяснение разницы и структуры — в +[tests/host/README.md §1](../../../tests/host/README.md#1-две-категории-тестируемых-модулей). --- -## Поддерживаемые прошивки - -| Конфигурация VSCode | ELF | Особенности | -| ----------------------------- | ------------------------------- | ---------------------------- | -| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль | -| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление | -| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view | - -Все три — XIP-прошивки, исполняются из QuadSPI NOR Flash (`0x60000000`). - ---- - -## Режимы запуска отладки - -### Режим А — прошивка уже в Flash +## Шаг 1 — Создать тестовый файл ```bash -# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале) -just host::debug-server - -# 2. DevContainer — VSCode -# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5 +mkdir -p tests/host// +touch tests/host//test_.c ``` -GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе -в `main`. Flash не перезаписывается. - -### Режим Б — прошить через SWD, затем отладить - -```bash -# 1. DevContainer -just build::hab-firmware-test-debug - -# 2. Хост -just host::flash-swd-test-debug - -# 3. ⚡ Power cycle платы (обязательно) - -# 4. Хост -just host::debug-server - -# 5. DevContainer — VSCode → 🐛 Debug: firmware_test → F5 -``` - -### Режим В — прошить через USB SDP, затем отладить - -```bash -# 1. DevContainer -just build::build-firmware-test-debug - -# 2. Хост — перевести плату в SDP-режим, затем: -just host::flash-test-debug - -# 3. Хост -just host::debug-server - -# 4. DevContainer — VSCode → 🐛 Debug: firmware_test → F5 -``` - ---- - -## Почему flash через SWD требует FCB - -При USB SDP ROM-загрузчик инициализирует FlexSPI по DCD из HAB-образа — FCB -не нужен. При SWD flash-алгоритм pyOCD пишет в NOR Flash напрямую. При -cold-start Boot ROM сначала читает FCB по адресу `0x60000000`, конфигурирует -FlexSPI, и только потом ищет IVT. Без FCB бутлоадер не стартует. - -`flash_swd.py` решает это, собирая образ перед записью: - -```bash -0x60000000 w25q128_fdcb.bin (512 байт) — FCB -0x60000200 0xFF × 3584 байт — padding -0x60001000 firmware_test_hab.bin — IVT + DCD + код -``` - -Весь диапазон `0x60000000–0x6000FFFF` — один 64KB сектор: стирается и -записывается за одну транзакцию. - ---- - -## RTT-логи - -SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`). -После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0. -`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF. +### Шаблон — категория A (без моков) ```c -#include "SEGGER_RTT.h" -SEGGER_RTT_printf(0, "value = %d\n", value); +#include "unity.h" +#include "<модуль>.h" /* тестируемый модуль */ + +void setUp(void) { /* сброс состояния если нужен */ } +void tearDown(void) { } + +void test_something(void) +{ + TEST_ASSERT_EQUAL(expected, actual); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_something); + return UNITY_END(); +} +``` + +### Шаблон — категория B (с fff-фейками) + +```c +#include "unity.h" +#include "fff.h" + +DEFINE_FFF_GLOBALS; /* ровно один раз на файл */ + +/* 1. Stub-хедер с типами NXP SDK */ +#include "fsl_gpio.h" + +/* 2. Фейки для функций, которые вызывает тестируемый модуль */ +FAKE_VOID_FUNC(GPIO_PinInit, GPIO_Type *, uint32_t, const gpio_pin_config_t *); +FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t); + +/* 3. Тестируемый модуль — ПОСЛЕ фейков */ +#include "bsp/.h" + +void setUp(void) +{ + RESET_FAKE(GPIO_PinInit); + RESET_FAKE(GPIO_PinWrite); + FFF_RESET_HISTORY(); +} + +void tearDown(void) { } + +void test_something(void) +{ + TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_something); + return UNITY_END(); +} +``` + +Если тестируемому модулю не хватает stub-хедера (новый SDK-вызов) — +добавить минимальные типы/сигнатуры в `tests/host/mocks/` (только то, что +реально используется — не копировать весь SDK-хедер). + +--- + +## Шаг 2 — Зарегистрировать в `tests/host/CMakeLists.txt` + +```cmake +# категория A — платформонезависимый, без MOCKS +add_host_test( + NAME test_ + SOURCES /test_.c + ${PROJECT_SOURCE_DIR}/<путь-к-модулю>/.c + INCLUDES ${PROJECT_SOURCE_DIR}/<путь-к-инклюдам> +) + +# категория B — BSP-модуль, нужны MOCKS +add_host_test( + NAME test_ + SOURCES /test_.c + ${PROJECT_SOURCE_DIR}/bsp//src/.c + INCLUDES ${PROJECT_SOURCE_DIR}/bsp//include + ${PROJECT_SOURCE_DIR}/bsp/common/include + MOCKS ${BSP_MOCKS_DIR} +) +``` + +`add_host_test()` — вспомогательная CMake-функция, определённая в начале +того же файла (`NAME`/`SOURCES`/`INCLUDES`/`MOCKS`). Каждый тест — свой +исполняемый файл; `MOCKS` подключает `tests/host/mocks/` в include path +**раньше** реального SDK, `INCLUDES` — явные пути, специфичные для теста +(без скрытых глобальных путей). Если модуль использует `bsp_uart_host` через +готовый мок — смотри пример `uart_host_mock_example` в том же файле. + +Если тест компилируется с seam-макросом (как `test_runner.c` с +`-DUNIT_TEST`, см. `firmware/test/README.md` §UNIT_TEST seam) — добавить: + +```cmake +target_compile_definitions(test_ PRIVATE UNIT_TEST) ``` --- -## FreeRTOS task view - -Конфигурация `🐛 Debug: tft_app (FreeRTOS)` включает `"rtos": "FreeRTOS"` — -cortex-debug разбирает структуры планировщика и показывает вкладку `RTOS` -с таблицей задач: имя, состояние, использование стека, приоритет. - ---- - -## Просмотр регистров периферии - -Вкладка `Peripherals` показывает все блоки MIMXRT1052 по SVD-файлу -`bsp/generated/startup/MIMXRT1052.xml`. Значения обновляются при каждой паузе. - ---- - -## Ограничения - -**MCU-Link монопольный ресурс.** `debug-server` и `flash-swd` не могут -работать одновременно. Перед `flash-swd` остановите сервер (Ctrl+C). - -**HIL-тесты vs отладка.** pyOCD также используется для HIL. Перед -`just host::hil-run` остановите GDB-сервер. - -**Power cycle после flash-swd обязателен.** VECTRESET не реинициализирует -FlexSPI — только полное отключение питания гарантирует корректный cold-start. - -**Только Debug-сборки.** Release компилируется с `-O2` без DWARF-символов. - ---- - -## Быстрый старт (первый запуск) +## Шаг 3 — Собрать и прогнать ```bash -# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux): -# "runArgs": ["--add-host=host.docker.internal:host-gateway"] +# конфигурация (один раз или после изменения CMakeLists) +cmake --preset host-debug -# 2. Залить прошивку -just host::flash-test-debug +# сборка + тесты одной командой +just build::test-host -# 3. Хост — запустить GDB-сервер -just host::debug-server +# конкретный тест с полным выводом Unity +ctest --preset host-debug-test -R test_ -V -# 4. DevContainer — VSCode -# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5 +# напрямую — без обёртки CTest +./build/host-debug/tests/host/test_ +``` + +`just build::test-host` собирает под пресетом `host-debug` (`clang-17`, +без ARM-специфики) и прогоняет весь набор через CTest. `host-release` +собирает тот же набор с оптимизациями — используется в CI как +дополнительный гейт. + +--- + +## Чеклист + +```bash +[ ] tests/host//test_.c — тест-файл (категория A или B) +[ ] tests/host/mocks/*.h — новый stub-хедер, если модуль + использует ранее не замоканный SDK-вызов +[ ] tests/host/CMakeLists.txt — add_host_test(...) для нового теста +[ ] just build::test-host — зелёная сборка + прогон ``` --- -## Дерево файлов отладки +## Справочник -```bash -. -├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH -├── .vscode/ -│ ├── launch.json # cortex-debug конфигурации (3 проекта) -│ └── tasks.json # preLaunchTask: build:*-debug -├── bsp/generated/startup/ -│ └── MIMXRT1052.xml # SVD — регистры периферии -├── just/ -│ └── host.just # debug-server, flash-swd-* -└── tools/ - ├── hil/ # uv-проект с pyocd - └── host/ - ├── flash_swd.py # FCB + HAB → Flash через pyOCD - └── dcd/ - └── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI -``` +Полный API Unity (assertion-макросы), fff (создание фейков, `custom_fake`, +проверка вызовов), работа со stub-хедерами и типичные ловушки (dangling +pointer из `arg_history`, `static`-функции, `ScopeMismatch`-аналоги для +host-тестов) — в [tests/host/README.md](../../../tests/host/README.md). diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md deleted file mode 100644 index 7fa9d46..0000000 --- a/firmware/test/PLAN.md +++ /dev/null @@ -1,488 +0,0 @@ -# firmware_test — План разработки - -> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified). - ---- - -## Контекст проекта - -**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации). -Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика. - -**Стенд:** - -- Хост подключается через USB CDC ACM — единственный канал firmware_test -- HIL-тесты управляются через M5StampPLC (опционально) -- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно - ---- - -## Текущий статус - -| Компонент | Статус | Примечание | -| ------------------------------ | ------ | ------------------------------------------------ | -| `bsp_usb_cdc` | ✅ | HIL тест пройден | -| firmware_test скелет | ✅ | `main.c` + `cli.c` | -| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | -| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | -| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | -| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | -| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | -| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | -| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified | -| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified | -| `test_opto` | ✅ | Этап 6б, hardware-verified | -| `test_can` | ✅ | Этап 6в, hardware-verified | -| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | -| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | -| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | -| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified | -| Provisioning | ⬜ | Этап 7 | -| TUI сервисного инженера | ⬜ | Этап 8 | - ---- - -## Матрица тестов — итоговая - -| ID | Название | Critical | HIL | Тип | BSP | Статус | -| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ | -| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | -| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | -| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | -| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | -| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | -| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ | -| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | - -**Убранные тесты (закрытые решения):** - -- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется -- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно - ---- - -## Закрытые архитектурные решения - -> Не пересматривать без явного запроса. - -### Этапы 1–5 (ранее зафиксированные) - -- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test. -- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`. -- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует. -- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`. -- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7). -- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`. -- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL. -- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP. - -### Этап 6 (новые решения) - -- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL). - TUI фильтрует HIL-тесты если M5StampPLC не подключён. -- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста. - TUI строит UI динамически, не хардкодит список тестов. -- **`run_selected`:** запуск произвольного подмножества тестов по списку ID. - Порядок выполнения — как в реестре таргета, не как в запросе. - Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI. -- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request` - от HIL-теста TUI командует M5, получает результат, отправляет confirm. -- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты - недоступны (серые в UI, не входят в `run_selected`). -- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. -- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен - один канал. Буфер всегда стерео (L+R идентичны). -- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая), - `confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL. - `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`. -- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`. - Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M. - Нарушение порядка приводит к щелчку при старте или отсутствию звука. -- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking), - параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с. -- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно - выставлять в `true`. При инициализации через designated initializers без явного - указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит - на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого - прогона). Воспроизводится только при cold reset. -- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на - `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate - может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` - перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым — - закрывать не нужно, LPUART1 тактируется с минимальным потреблением. - `bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`. -- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при - старте, может быть пропущено). Проверяет живость через `ping → pong`. -- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина - без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется - в `test_opto.c` после settle — обходит race condition когда чётное число ISR - при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`. -- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm, - но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`. -- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`. - Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`, - `bsp_opto_force_read()` читает финальное состояние пина напрямую. - -### Этап 8 (TUI решения) - -- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost). - Оператор сам переставляет перемычку BOOT — это ок, документируется. -- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130) - или CDC firmware_test (session_start) — и показывает соответствующий экран. -- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты, - работает в SSH-сессии, вписывается в uv-экосистему. -- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/` - и `tools/production/`. - ---- - -## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН - -### 6а — Расширение протокола ✅ - -**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md` - -#### Новая команда `list_tests` - -```json -→ {"type":"cmd","cmd":"list_tests"} -← {"type":"test_list","tests":[ - {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, - {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, - {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, - {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, - {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, - {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, - {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, - {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true} - ]} -``` - -#### Новая команда `run_selected` - -```json -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} -← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi",...} -← {"type":"test_begin","id":"display",...} -← {"type":"test_result","id":"display",...} -← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"} -``` - -Если хотя бы один ID не найден в реестре: - -```json -← {"ok":false,"error":"UNKNOWN_TEST"} -``` - -**Реализация в `test_runner.c`:** - -- Новый режим `RUNNER_MODE_SELECTED` -- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc -- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция - -### 6б — test_opto.c ✅ - -**Файл:** `firmware/test/src/tests/test_opto.c` - -6 шагов, попарно ACTIVE/INACTIVE для трёх каналов: - -| Шаг | confirm_request id | M5 действие | Проверка | -| --- | ------------------- | ----------- | -------------------------------- | -| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` | -| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` | -| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` | -| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` | -| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` | -| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` | - -- Init: `bsp_opto_init()` единым вызовом для всех каналов -- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`) -- FAIL при несоответствии: `detail = " state mismatch: expected ACTIVE got INACTIVE"` -- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг - -### 6в — test_can.c ✅ - -**Файл:** `firmware/test/src/tests/test_can.c` - -2 шага, оба направления независимо: - -**Шаг 1 — RX (M5 → таргет):** - -```bash -confirm_request("can_rx_ready") -→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF]) -→ TUI: confirm(true) -→ таргет: bsp_can_receive(&frame, 500 мс) -→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF] -→ FAIL если timeout или несовпадение -``` - -**Шаг 2 — TX (таргет → M5):** - -```bash -bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс) -confirm_request("can_tx_verify") -→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data -→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет -→ FAIL если confirmed=false или timeout -``` - -- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует -- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` - -### 6г — bsp_mqs + test_mqs.c ✅ - -**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c` - -**bsp_mqs:** - -- SAI3 + eDMA (DMA0 канал 0) + MQS периферия -- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT` -- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358 -- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`, - `bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume` - -**test_mqs:** - -- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида -- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле -- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL -- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг) -- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL` - -### 6д — HIL pytest для firmware_test ✅ - -**Файлы:** - -``` -tools/hil/conftest.py ← фикстура firmware_cdc -tools/hil/06_test_firmware_opto.py -tools/hil/06_test_firmware_can.py -``` - -**Фикстура `firmware_cdc`:** - -```python -@pytest.fixture(scope="module") -def firmware_cdc(m5): - """ - Открывает USB CDC порт firmware_test. - firmware_test уже прошит в Flash (не загружается pyOCD). - Проверяет живость через ping → pong. - """ -``` - -**`FirmwareCdcClient`** — тонкий клиент: - -- `send_cmd(cmd_dict)` — отправить JSON команду -- `wait_event(type, timeout_s)` — ждать события нужного типа -- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}` -- `run_test(id)` — запустить тест, вернуть test_result dict - -**Justfile:** - -```bash -hil-firmware-opto → pytest 06_test_firmware_opto.py -v -hil-firmware-can → pytest 06_test_firmware_can.py -v -``` - ---- - -## Этап 7 — Provisioning - -### Что нужно - -1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp` -2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary` -3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста -4. Записывать статус в Flash (первый сектор после прошивки, вне XIP) - -### BSP (предварительно) - -```c -/* bsp/provisioning/include/bsp/provisioning.h */ -bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */ -``` - -### Открытые вопросы — Этап 7 - -- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID? -- [ ] Нужна ли защита от повторного provisioning (write-once)? - ---- - -## Этап 8 — TUI сервисного инженера - -### Стек технологий - -| Компонент | Выбор | Обоснование | -| ------------- | ------------- | ----------------------------------------------------- | -| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема | -| Serial | pyserial | Уже в стеке (tools/hil) | -| Прошивка | spsdk | sdphost + blhost, уже в tools/host | -| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом | - -### Структура приложения - -```bash -tools/production/ -├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv -├── uv.lock -├── main.py ← точка входа -├── app/ -│ ├── tui.py ← Textual App, экраны, layout -│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test -│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py) -│ ├── flasher.py ← USB SDP обёртка над spsdk -│ ├── orchestrator.py ← confirm_request → M5 action → confirm response -│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses) -└── README.md - -tools/shared/ -└── m5_agent.py ← общая M5-логика для hil/ и production/ -``` - -### Два режима работы - -**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) - -``` -┌─ Прошивка платы ─────────────────────────────────┐ -│ Обнаружен BootROM (SDP режим) │ -│ │ -│ Что прошить? │ -│ ◉ firmware_test (диагностика) │ -│ ○ Production (bootloader + tft_app) │ -│ │ -│ Файл: [/path/to/firmware_test_hab.bin ···] │ -│ │ -│ [ Прошить ] │ -│ │ -│ ████████████░░░░░░ 64% Запись во Flash... │ -└────────────────────────────────────────────────────┘ -``` - -**Режим B — Диагностика** (триггер: session_start получен по CDC) - -``` -┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ -│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ -├────────────────────────────────────────────────────┤ -│ Выбор тестов: │ Результаты: │ -│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ -│ ☑ QSPI Flash │ qspi ✓ PASS │ -│ ☑ microSD │ usd ✗ FAIL │ -│ ☑ TFT Display │ mount failed: 5 │ -│ ☑ Кнопки │ display ✓ PASS │ -│ ☑ MQS Audio │ buttons ✓ PASS │ -│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │ -│ ☑ Оптовходы [HIL] │ ... │ -├────────────────────────────────────────────────────┤ -│ [ Запустить выбранные ] [ Все тесты ] │ -│ ████████████████░░░░ 80% Тест: display │ -├────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└────────────────────────────────────────────────────┘ -``` - -### Поведение confirm_request в TUI - -| Тип теста | Источник confirm | Действие TUI | -| -------------------- | ------------------ | --------------------------------------------- | -| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | -| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | - -HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt. - -### Конфигурация (.env) - -```ini -# Существующие переменные (tools/hil/.env): -HIL_VCOM_PORT=/dev/ttyACM0 -HIL_M5_PORT=/dev/ttyACM1 - -# Новые переменные для production TUI: -SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start -SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5 -FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin -PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin -PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin -``` - -### Запуск - -```bash -just host::service-tui # запустить TUI сервисного инженера -just host::service-flash # прошить без TUI (для автоматизации) -``` - -### Процесс работы сервисника - -**Диагностика (firmware_test уже в Flash):** - -```bash -1. Плата в нормальном режиме (BOOT_MOD_1 → GND) -2. Подключить USB к сервисному ПК -3. just host::service-tui → TUI обнаружил session_start → Режим B -4. Выбрать тесты → Запустить → Смотреть результаты -``` - -**Перепрошивка (нужна новая версия firmware_test или production):** - -```bash -1. Перемычка BOOT_MOD_1 → 3V3 -2. Reset, подключить USB -3. TUI обнаружил 1FC9:0130 → Режим A -4. Выбрать бинарь → Прошить -5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B -``` - ---- - -## Порядок реализации - -``` -✅ Этап 1 протокол v2 + runner -✅ Этап 2 bsp_sdram + test_sdram -✅ Этап 3 bsp_qspi_flash + test_qspi -✅ Этап 4 bsp_sd + test_usd -✅ Этап 5 display + buttons -✅ Этап 6а протокол: list_tests + run_selected -✅ Этап 6б test_opto.c + hardware верификация -✅ Этап 6в test_can.c + hardware верификация -✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) -✅ Этап 6е HIL pytest: 06_test_firmware_opto.py -✅ Этап 6ж HIL pytest: 06_test_firmware_can.py -✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация - -⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ - -⬜ Этап 8а tools/production/ скелет + models + clients -⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) -⬜ Этап 8в Экран прошивки (flasher + SDP автодетект) -⬜ Этап 8г Provisioning в TUI -⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики) - -⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру -``` - ---- - -## Зависимости между этапами - -``` -✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs) - ↓ - ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) - ↓ - ⬜ 7 (provisioning) - ↓ - ⬜ 8 (TUI) -``` \ No newline at end of file diff --git a/firmware/test/README-1.md b/firmware/test/README-1.md index 8d3fa1e..ce7aefc 100644 --- a/firmware/test/README-1.md +++ b/firmware/test/README-1.md @@ -1,10 +1,12 @@ # firmware_test -> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по -> рекламации. Запускается сервисным инженером через USB CDC ACM без -> предварительной прошивки загрузчика. -> -> Версия прошивки: `0.1.0` | Протокол: v2 +> Диагностическая прошивка для плат TFT индикаторов, вернувшихся по +> рекламации. Загружается на таргет сервисным инженером через USB (через BootROM IMXRT1052). +> +>Версия прошивки: `0.1.2` | Протокол: v2 +> +>Версия — из `project(firmware_test VERSION X.Y.Z)` в `CMakeLists.txt` +> (см. [Версионирование](#версионирование)) --- @@ -27,6 +29,7 @@ - [Как добавить новый тест](#как-добавить-новый-тест) - [Host unit-тесты](#host-unit-тесты) - [Версионирование](#версионирование) +- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты) --- @@ -45,7 +48,7 @@ just build::hab-firmware-test-debug # Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset just host::flash-test-debug -# Или через SWD (power cycle после) +# Или через SWD just host::flash-swd-test-debug ``` @@ -66,7 +69,7 @@ screen /dev/ttyACM0 После подключения таргет сразу присылает: ```json -{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} +{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} ``` Проверка связи: @@ -81,7 +84,7 @@ screen /dev/ttyACM0 ```json → {"type":"cmd","cmd":"run","id":"sdram"} ← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ``` Запуск всех тестов: @@ -110,12 +113,12 @@ just build::test-host │ JSON-lines, 1 строка = 1 сообщение ▼ [Плата MIMXRT1052 с firmware_test] - │ GPIO / LPUART / SEMC / FlexSPI / USDHC + │ GPIO / LPUART / SEMC / FlexSPI / USDHC / SAI(MQS) ▼ -[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto] +[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, MQS, Opto] ▲ [M5StampPLC — управление внешними сигналами для HIL тестов] - (реле → EXT_IN1/IN2, RS_RX, CAN, UART echo) + (реле → EXT_IN1/IN2, RS_RX; CAN loopback) ``` **Принцип разделения ответственности:** @@ -123,8 +126,8 @@ just build::test-host - Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`). - Хост — тонкий клиент: отправляет команды, отображает события, управляет интерактивными шагами через `confirm`. -- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок - не фиксирован. +- Тесты **атомарны**: инженер запускает один тест, подмножество или все сразу — + порядок не фиксирован. --- @@ -136,6 +139,8 @@ firmware/test/ └── src/ ├── main.c — инициализация BSP, главный цикл │ + ├── version.h.in — шаблон версии (CMake → generated/version.h) + │ ├── cli.h / cli.c — IO-слой │ буферизация строк, парсинг "type", │ диспатч на test_runner / protocol @@ -149,20 +154,23 @@ firmware/test/ │ ├── test_runner.h / .c — реестр + state machine │ IDLE → PRE_CONFIRM → RUNNING → IDLE - │ test_runner_wait_confirm() для display + │ test_runner_wait_confirm() для in-run confirm │ └── tests/ ├── test_sdram.c — SDRAM 32 MB (self) - ├── test_qspi.c — QSPI Flash 8 MB (self) - ├── test_usd.c — uSD SDIO (interactive) - ├── test_display.c — Display RGB888 (interactive) - ├── test_buttons.c — Test_But_1/2 (interactive) + ├── test_qspi.c — QSPI Flash W25Qxx (self) + ├── test_usd.c — microSD SDIO (interactive, pre-confirm) + ├── test_display.c — Display RGB888 (interactive, in-run confirm) + ├── test_buttons.c — Test_But_1/2 (interactive, физическое нажатие) + ├── test_opto.c — Opto-in EXT_IN1/IN2 + RS_RX (HIL) ├── test_can.c — CAN loopback (HIL) - ├── test_uart_ttl.c — UART TTL (HIL) - ├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL) - └── test_opto.c — Opto-in EXT_IN1/IN2 (HIL) + └── test_mqs.c — MQS Audio Out (interactive, in-run confirm) ``` +> Порядок файлов в `tests/` — как в `CMakeLists.txt`. Порядок **выполнения** +> тестов определяется реестром `k_registry[]` в `test_runner.c` +> (см. [Матрица тестов](#матрица-тестов)). + --- ### Граф зависимостей @@ -175,8 +183,9 @@ main.c ├── bsp_usb_cdc (USB CDC ACM, единственный транспорт) ├── cli.c │ └── bsp_usb_cdc (read / write) - │ └── protocol.c (send_error, send_pong) - │ └── test_runner.c (run_single, run_all, on_confirm) + │ └── bsp_provisioning (bsp_prov_read_uid — для get_uid) + │ └── protocol.c (send_error, send_pong, send_uid/version_response) + │ └── test_runner.c (run_single, run_all, run_selected, send_list, on_confirm) ├── protocol.c │ └── cli.c (cli_send) │ └── bsp_tick (bsp_tick_get_ms — для uptime) @@ -188,26 +197,28 @@ main.c └── tests/*.c (тест-модули через реестр) ``` -**BSP-зависимости тест-модулей:** +**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`): -| Тест | BSP модуль | -| --------------- | ------------------------------ | -| `test_sdram` | `bsp_sdram` | -| `test_qspi` | `bsp_qspi` | -| `test_usd` | `bsp_usd` | -| `test_display` | существующий display BSP | -| `test_buttons` | `bsp_button` ✅ | -| `test_can` | `bsp_can` ✅ | -| `test_uart_ttl` | `bsp_uart_host` ✅ | -| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ | -| `test_opto` | `bsp_opto` ✅ | +| Тест | BSP модуль | +| -------------- | ------------------------------ | +| `test_sdram` | `bsp_sdram` | +| `test_qspi` | `bsp_qspi_flash` | +| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) | +| `test_display` | `bsp_display` | +| `test_buttons` | `bsp_button` | +| `test_opto` | `bsp_opto` (rs_as_gpio=true) | +| `test_can` | `bsp_can` | +| `test_mqs` | `bsp_mqs` | + +> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного +> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`). --- ### State machine test_runner ```bash - cmd: run / run_all + cmd: run / run_all / run_selected │ ▼ ┌─────────────────────────────────────┐ @@ -232,9 +243,9 @@ main.c confirmed=false → SKIP │ │ timeout → SKIP │ │ │ │ - run: IDLE ──┘ │ - run_all: следующий тест в реестре ───┘ - run_all done: protocol_send_summary() + run: IDLE ──┘ │ + run_all / run_selected: следующий тест ───┘ + done: protocol_send_summary() ``` **Ключевые свойства state machine:** @@ -242,10 +253,12 @@ main.c - `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`. Пока тест выполняется, новые команды получают `BUSY`. - `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов - (display). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`. + (display, mqs). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`. USB-стек остаётся живым, confirm приходит без возврата в главный цикл. -- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают - `SKIP` немедленно, `summary.overall = "fail"`. +- `run_selected` работает по той же машине над маской выбранных тестов + (`g_s_selected[]`); порядок — по реестру, не по порядку в запросе. +- `critical=true` + `FAIL` в `run_all`/`run_selected` → все оставшиеся тесты + получают `SKIP` немедленно, `summary.overall = "fail"`. --- @@ -258,7 +271,7 @@ main.c | Интерфейс | USB CDC ACM, разъём J2 | | Кодировка | UTF-8 | | Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | -| Максимальная длина строки | 128 байт включая `\n` | +| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) | | CR+LF | Принимается (таргет отбрасывает `\r`) | Нет хэндшейка, нет sequence number, нет подтверждений доставки. @@ -273,7 +286,7 @@ main.c │ │ │ [USB SDP: прошивка загружена] │ │ [CDC ACM: порт открыт] │ - │◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически + │◄─── {"type":"session_start","fw":"0.1.2",...} │ автоматически │ │ │──── {"type":"cmd","cmd":"ping"} ─────────────►│ │◄─── {"type":"pong"} │ @@ -290,7 +303,8 @@ main.c ``` `session_start` отправляется **автоматически** при каждом старте, до получения -первой команды. Хост должен быть готов принять его сразу после открытия порта. +первой команды. Хост должен быть готов принять его сразу после открытия порта +(либо не полагаться на него — фикстуры HIL проверяют живость через `ping`). --- @@ -310,7 +324,7 @@ main.c ```json → {"type":"cmd","cmd":"run","id":"sdram"} ← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ``` Если `id` не найден: @@ -324,13 +338,58 @@ main.c ```json → {"type":"cmd","cmd":"run_all"} ← {"type":"test_begin","id":"sdram",...} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} ← ... (остальные тесты) ... ← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"} ``` +#### `run_selected` — запуск подмножества тестов + +```json +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} +← {"type":"test_begin","id":"sdram",...} +← {"type":"test_result","id":"sdram","status":"pass",...} +← {"type":"test_begin","id":"opto",...} +← {"type":"test_result","id":"opto","status":"pass",...} +← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} +``` + +Порядок выполнения — по реестру таргета, не по порядку в запросе. Если хотя +бы один ID не найден — вся команда отклоняется (`UNKNOWN_TEST`), не запускается +ничего. + +#### `list_tests` — получить реестр тестов + +```json +→ {"type":"cmd","cmd":"list_tests"} +← {"type":"test_list","tests":[ + {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, + ... остальные ... + ]} +``` + +Хост (TUI) использует ответ для динамического построения списка тестов; +`requires_hil=true` тесты недоступны при отсутствии M5StampPLC. + +#### `get_uid` — прочитать UID чипа + +```json +→ {"type":"cmd","cmd":"get_uid"} +← {"type":"uid_response","uid":"A1B2C3D4E5F60011"} +``` + +`uid` — 8 байт (`BSP_PROV_UID_LEN`) big-endian, 16 hex-символов без +разделителей. При ошибке чтения: `{"ok":false,"error":"UID_READ_ERR"}`. + +#### `get_version` — прочитать версию прошивки + +```json +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + #### `confirm` — ответ оператора на интерактивный шаг ```json @@ -347,35 +406,19 @@ main.c #### `session_start` ```json -{ - "type": "session_start", - "fw": "0.1.0", - "target": "IMXRT1052", - "uptime_ms": 0 -} +{ "type":"session_start", "fw":"0.1.2", "target":"IMXRT1052", "uptime_ms":0 } ``` #### `test_begin` ```json -{ - "type": "test_begin", - "id": "sdram", - "name": "SDRAM 32 MB", - "critical": true -} +{ "type":"test_begin", "id":"sdram", "name":"SDRAM 32 MB", "critical":true } ``` #### `test_result` ```json -{ - "type": "test_result", - "id": "sdram", - "status": "pass", - "ms": 312, - "detail": "" -} +{ "type":"test_result", "id":"sdram", "status":"pass", "ms":15304, "detail":"" } ``` | `status` | Смысл | @@ -384,37 +427,39 @@ main.c | `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | | `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше | -Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`. +Примеры `detail`: `"addr=0x80200001 exp=0x02 got=0xFF"`, `"JEDEC: mfr=0xFF exp=0xEF"`. + +#### `test_list` + +Ответ на `list_tests` — массив дескрипторов (`id`, `name`, `critical`, +`requires_hil`). #### `confirm_request` ```json -{ - "type": "confirm_request", - "id": "display_red", - "prompt": "Экран залит красным цветом?", - "timeout_ms": 15000 -} +{ "type":"confirm_request", "id":"display_red", "prompt":"Screen is solid red?", "timeout_ms":15000 } ``` Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете. Хост может дублировать countdown для UX. +#### `uid_response` / `version_response` + +Ответы на `get_uid` / `get_version` (см. соответствующие команды выше). + #### `summary` ```json -{ - "type": "summary", - "passed": 6, - "failed": 1, - "skipped": 0, - "overall": "fail" -} +{ "type":"summary", "passed":6, "failed":1, "skipped":0, "overall":"fail" } ``` `"overall":"fail"` — если хотя бы один `critical` тест провален. `"overall":"pass"` — все `critical` тесты прошли (non-critical могут fail). +#### `pong` + +Ответ на `ping`: `{"type":"pong"}`. + --- ### Ошибки протокола @@ -425,43 +470,60 @@ main.c ← {"ok":false,"error":"UNKNOWN_TEST"} — "id" не найден в реестре ← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт ← {"ok":false,"error":"BUSY"} — таргет выполняет тест +← {"ok":false,"error":"UID_READ_ERR"} — bsp_prov_read_uid() вернул ошибку ``` --- ### Интерактивные тесты -#### uSD — вставить карту +#### microSD — вставить карту (pre-confirm) ```bash -← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000} -→ {"type":"confirm","id":"usd_insert","confirmed":true} +← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} +→ {"type":"confirm","id":"usd","confirmed":true} ← {"type":"test_begin","id":"usd",...} -← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""} +← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} ``` -Если оператор отказался или таймаут: +confirm id для pre-confirm равен id теста (`usd`) — механизм `pre_confirm_prompt` +использует `mod->id`. Отказ или таймаут 30 с → `SKIP`. -```bash -← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"} -``` +#### Display RGB888 — подтвердить цвета и ротацию (in-run confirm) -#### Display RGB888 — подтвердить цвета - -Четыре шага R/G/B/W. Итог — AND всех подтверждений. +Шесть шагов: Red → Green → Blue → White, затем два ротационных +(диагностика непропаянных LR/UD пинов). Тест прерывается на **первом** +неподтверждённом шаге (короткое замыкание, не сбор всех ответов). ```bash ← {"type":"test_begin","id":"display",...} -← {"type":"confirm_request","id":"display_red","prompt":"Экран красный?","timeout_ms":15000} +← {"type":"confirm_request","id":"display_red","prompt":"Screen is solid red?","timeout_ms":15000} → {"type":"confirm","id":"display_red","confirmed":true} ← {"type":"confirm_request","id":"display_green",...} → {"type":"confirm","id":"display_green","confirmed":true} ← {"type":"confirm_request","id":"display_blue",...} → {"type":"confirm","id":"display_blue","confirmed":true} ← {"type":"confirm_request","id":"display_white",...} -→ {"type":"confirm","id":"display_white","confirmed":false} -← {"type":"test_result","id":"display","status":"fail","ms":22103, - "detail":"display_white not confirmed"} +→ {"type":"confirm","id":"display_white","confirmed":true} +← {"type":"confirm_request","id":"display_rot0","prompt":"Screen: left RED, right BLUE?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot0","confirmed":true} +← {"type":"confirm_request","id":"display_rot_base","prompt":"Left RED and right BLUE swapped sides?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot_base","confirmed":true} +← {"type":"test_result","id":"display","status":"pass",...} +``` + +При отказе/таймауте: `status:"fail"`, `detail:" not confirmed"`. + +#### MQS Audio — подтвердить слышимость тона (in-run confirm) + +Таргет ~4 с играет мелодию (A4, затем E5) через MQS + LM4875M, затем запрашивает +подтверждение: + +```bash +← {"type":"test_begin","id":"mqs",...} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} +→ {"type":"confirm","id":"mqs_tone","confirmed":true} +← {"type":"test_result","id":"mqs","status":"pass",...} ``` #### Кнопки — нажать физически @@ -472,36 +534,44 @@ main.c ```bash ← {"type":"test_begin","id":"buttons",...} -← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} [таргет ждёт bsp_button — без JSON confirm от хоста] -← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000} -← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""} +← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000} +← {"type":"test_result","id":"buttons","status":"pass",...} ``` +Таймаут 10 с → `SKIP` (не FAIL). + +> Полные потоки всех тестов (SDRAM/QSPI/opto/CAN, коды `detail`, HIL pytest) — +> в справочнике по тестированию `README_TESTING.md`. + --- ## Матрица тестов -| ID | Название | Тип | Critical | M5 HIL | Confirm | -| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- | -| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | -| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | -| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm | -| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() | -| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only | -| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | -| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ | -| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | +Порядок — как в реестре `k_registry[]` (`test_runner.c`). + +| № | ID | Название | Тип | Critical | M5 HIL | Confirm | +| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- | +| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | +| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ | +| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm | +| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() | +| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only | +| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) | +| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) | +| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() | **Типы confirm:** -- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`, - ждёт JSON-ответ через state machine (асинхронно). +- **pre_confirm** — `test_runner` отправляет `confirm_request` до вызова `run()`, + ждёт JSON-ответ через state machine (асинхронно). id = id теста. - **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`, блокируется до ответа (синхронно). -- **prompt only** — `protocol_send_confirm_request()` отправляется как UI-подсказка, +- **prompt only** — `protocol_send_confirm_request()` как UI-подсказка, хост не отвечает JSON, таргет ждёт физического события. +- **авто (HIL)** — confirm генерирует не оператор, а хост-оркестратор, командуя + M5StampPLC (см. `README_TESTING.md`). --- @@ -540,13 +610,13 @@ static test_result_t test_foo_run(void) return result; } -const test_module_t k_test_foo = { - .id = "foo", /* короткий ASCII-ключ */ +const test_module_t K_TEST_FOO = { + .id = "foo", /* короткий ASCII-ключ */ .name = "Foo Peripheral", .critical = false, /* true → run_all стопится при fail */ - .requires_hil = false, /* true → нужен M5StampPLC */ - .pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */ - .init = NULL, /* bsp_foo_init если нужен */ + .requires_hil = false, /* true → нужен M5StampPLC */ + .pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */ + .init = NULL, /* bsp_foo_init если нужен */ .run = test_foo_run, .deinit = NULL, }; @@ -558,15 +628,19 @@ const test_module_t k_test_foo = { ```c /* Forward declarations */ -extern const test_module_t k_test_sdram; -extern const test_module_t k_test_foo; /* ← добавить */ +extern const test_module_t K_TEST_SDRAM; +extern const test_module_t K_TEST_FOO; /* ← добавить */ static const test_module_t *const k_registry[] = { - &k_test_sdram, - &k_test_foo, /* ← добавить */ + &K_TEST_SDRAM, + ... + &K_TEST_FOO, /* ← добавить */ }; ``` +> При росте реестра выше `TEST_REGISTRY_MAX_SIZE` (`test_module.h`) сборка +> упадёт на `_Static_assert` в `test_runner.c` — увеличить константу. + ### Шаг 3 — Добавить в CMakeLists.txt **Файл:** `firmware/test/CMakeLists.txt` @@ -578,7 +652,6 @@ add_executable( src/cli.c src/protocol.c src/test_runner.c - src/tests/test_sdram.c src/tests/test_foo.c # ← добавить ... ) @@ -592,24 +665,18 @@ target_link_libraries( ### Шаг 4 — Обновить матрицу тестов -Добавить строку в таблицу в этом README. +Добавить строку в таблицу в этом README (и, если есть протокольный поток — +в `README_TESTING.md`). ### Шаблоны для разных типов тестов #### Self-тест с инициализацией ```c -static void test_foo_init(void) -{ - bsp_foo_init(); -} +static void test_foo_init(void) { bsp_foo_init(); } +static void test_foo_deinit(void) { bsp_foo_deinit(); } -static void test_foo_deinit(void) -{ - bsp_foo_deinit(); -} - -const test_module_t k_test_foo = { +const test_module_t K_TEST_FOO = { .id = "foo", .init = test_foo_init, .run = test_foo_run, @@ -622,7 +689,6 @@ const test_module_t k_test_foo = { ```c #include "test_runner.h" /* test_runner_wait_confirm() */ -#include "protocol.h" /* protocol_send_confirm_request() */ static test_result_t test_foo_run(void) { @@ -650,13 +716,13 @@ static test_result_t test_foo_run(void) #### Тест с pre_confirm (вставить карту, подключить кабель) ```c -const test_module_t k_test_foo = { +const test_module_t K_TEST_FOO = { .id = "foo", .pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK", .run = test_foo_run, ... }; -/* test_runner сам отправит confirm_request перед вызовом run() */ +/* test_runner сам отправит confirm_request (id = "foo") перед вызовом run() */ ``` --- @@ -727,34 +793,34 @@ void test_run_all_critical_fail_skips_remaining(void) | `cli_process()` | `FAKE_VOID_FUNC(cli_process)` | | `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению | -> **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель -> на стековую переменную внутри `execute_test()`. После возврата указатель -> инвалиден — используй `custom_fake` с `s_captured = *p_result` пока стек жив. - --- ## Версионирование -`FIRMWARE_TEST_VERSION` в `protocol.h` — единственная точка правды о версии. -Поле `"fw"` в `session_start` несёт эту строку. +Текущая версия ПО задается с помощью `project(firmware_test VERSION X.Y.Z)` +в `firmware/test/CMakeLists.txt`. CMake прокидывает её через +`configure_file(src/version.h.in → generated/version.h)`, откуда `protocol.h` +берёт `FIRMWARE_TEST_VERSION_STR`: -При несовместимых изменениях протокола (новое обязательное поле, изменение -семантики) — bumping версии + обновление этого документа. +``` +CMakeLists.txt: project(firmware_test VERSION 0.1.2) + │ configure_file(@ONLY) + ▼ +generated/version.h: FIRMWARE_TEST_VERSION_STR = "0.1.2" + │ + ▼ +protocol.h: #define FIRMWARE_TEST_VERSION FIRMWARE_TEST_VERSION_STR + │ + ▼ +session_start / version_response: "fw":"0.1.2" +``` -Хост должен сверять `"fw"` при подключении и предупреждать оператора при -несовпадении ожидаемой версии. +`version.h` генерируется, **не** редактируется вручную. Менять версию — +только в `CMakeLists.txt`. ---- +Хост может запросить версию явно (`get_version` → `version_response`) или +прочитать её из `session_start`, и предупредить оператора при несовпадении +с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле, +смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`. -## Архитектурные решения (закрыты) - -> Не пересматривать без явного запроса. - -| Решение | Обоснование | -| -------------------------------------------- | ------------------------------------------------------------------ | -| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) | -| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен | -| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC | -| IR и RTC — не реализуются | Вне scope рекламационной диагностики | -| Тесты атомарны | Инженер сам решает что проверять | -| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики | +## diff --git a/firmware/test/src/cli.c b/firmware/test/src/cli.c index fc12a60..c3372b4 100644 --- a/firmware/test/src/cli.c +++ b/firmware/test/src/cli.c @@ -191,7 +191,7 @@ static size_t parse_string_array(const char *p_array_start, char (*p_out_bufs)[T { return 0U; } - p++; /* пропустить '[' */ + p++; size_t count = 0U; while (count < max_items) @@ -285,8 +285,6 @@ static void handle_cmd_run_selected(const char *p_line) /** * @brief Обработать сообщение {"type":"cmd",...}. - * - * Команды: ping → pong, run_all → test_runner, run → test_runner. */ static void handle_cmd(const char *p_line) { diff --git a/firmware/test/src/protocol.c b/firmware/test/src/protocol.c index 4fc3b62..37e0d5a 100644 --- a/firmware/test/src/protocol.c +++ b/firmware/test/src/protocol.c @@ -115,7 +115,6 @@ void protocol_send_confirm_request(const confirm_params_t *p_params) void protocol_send_test_list(const test_module_t *const *p_pp_registry, size_t count) { - /* Заголовок массива */ cli_send("{\"type\":\"test_list\",\"tests\":["); for (size_t i = 0U; i < count; i++) diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index cf53770..e9674a4 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -91,7 +91,7 @@ static volatile bool g_s_confirm_value; static char g_s_pending_confirm_id[RUNNER_CONFIRM_ID_SIZE]; static uint32_t g_s_confirm_deadline_ms; -/* Счётчики итога (используются только в RUNNER_MODE_ALL) */ +/* Счётчики итога (ALL и SELECTED; в SINGLE не используются) */ static uint8_t g_s_passed; static uint8_t g_s_failed; static uint8_t g_s_skipped; diff --git a/firmware/test/src/tests/README-2.md b/firmware/test/src/tests/README-2.md index 6738adf..0f4a221 100644 --- a/firmware/test/src/tests/README-2.md +++ b/firmware/test/src/tests/README-2.md @@ -1,10 +1,15 @@ # firmware_test — Руководство по тестированию -Тестовая прошивка входного контроля платы MIMXRT1052CVJ5B. +Тестовая прошивка входного контроля платы TFT индикатора. Транспорт: USB CDC ACM (J2). Протокол: JSON-lines v2, один JSON-объект на строку. Загружается в RAM через BootROM USB SDP — без предварительной прошивки загрузчика. -> Версия прошивки: `0.1.4` (`FIRMWARE_TEST_VERSION` в `protocol.h`) +> Версия прошивки: `0.1.2` — из `project(firmware_test VERSION X.Y.Z)` в +> `CMakeLists.txt` (через `version.h.in` → `version.h` → +> `FIRMWARE_TEST_VERSION_STR` в `protocol.h`). +> +> Архитектура, сборка, добавление тестов и host unit-тесты — в основном +> `README.md`. Этот документ — протокольные потоки по каждому тесту. --- @@ -17,8 +22,10 @@ | `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | | `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | | `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | -| `cmd` | `{"type":"cmd","cmd":"list_tests"}` | Получить реестр тестов с метаданными | | `cmd` | `{"type":"cmd","cmd":"run_selected","tests":["sdram","qspi"]}` | Запустить подмножество тестов | +| `cmd` | `{"type":"cmd","cmd":"list_tests"}` | Получить реестр тестов с метаданными | +| `cmd` | `{"type":"cmd","cmd":"get_uid"}` | Прочитать UID чипа (OCOTP) | +| `cmd` | `{"type":"cmd","cmd":"get_version"}` | Прочитать версию прошивки | | `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос | ### Исходящие события (target → host) @@ -30,8 +37,10 @@ | `test_begin` | `id`, `name`, `critical` | Тест запущен | | `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | | `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | -| `progress` | `test`, `step`, `status` | Прогресс внутри теста | +| `progress` | `test`, `step`, `status` | Прогресс внутри теста (usd) | | `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` / `run_selected` | +| `uid_response` | `uid` (16 hex, 8 байт big-endian) | Ответ на `get_uid` | +| `version_response` | `fw` (`X.Y.Z`) | Ответ на `get_version` | | `pong` | — | Ответ на `ping` | | `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | @@ -39,12 +48,14 @@ **Коды ошибок в `error`:** -| Код | Причина | -| -------------- | --------------------------------------------------------------- | -| `BUSY` | Предыдущий тест ещё выполняется | -| `UNKNOWN_TEST` | ID теста не найден в реестре | -| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) | -| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда | +| Код | Причина | +| --------------- | --------------------------------------------------------------- | +| `BUSY` | Предыдущий тест ещё выполняется | +| `UNKNOWN_TEST` | ID теста не найден в реестре | +| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) | +| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда | +| `LINE_TOO_LONG` | Входящая строка превысила 128 байт (`CLI_LINE_BUF_SIZE`) | +| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) | --- @@ -54,7 +65,7 @@ После подключения немедленно отправляет `session_start`: ```bash -← {"type":"session_start","fw":"0.1.4","target":"IMXRT1052","uptime_ms":1108} +← {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":1108} ``` ### Проверка канала (ping) @@ -64,6 +75,19 @@ ← {"type":"pong"} ``` +### Идентификация платы (get_uid / get_version) + +```bash +→ {"type":"cmd","cmd":"get_uid"} +← {"type":"uid_response","uid":"A1B2C3D4E5F60011"} + +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + +`uid` — 8 байт (`BSP_PROV_UID_LEN`) big-endian, 16 hex-символов без разделителей. +При ошибке чтения UID: `{"ok":false,"error":"UID_READ_ERR"}`. + --- ## SDRAM — контроль оперативной памяти @@ -179,7 +203,7 @@ sequenceDiagram | Время выполнения | < 1 с после вставки карты | Оператор вставляет карту по запросу. Тест запускается только после подтверждения. -Отказ или таймаут 30 с → `SKIP`. +Отказ или таймаут 30 с → `SKIP`. confirm id для pre-confirm равен id теста (`usd`). Пять шагов с `progress`-событиями: card detect → mount → write 4 KB → read/compare → unmount. @@ -246,7 +270,8 @@ sequenceDiagram **pre-confirm отсутствует** — `test_begin` отправляется сразу после `run`. -Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL): +Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL); +тест прерывается на первом неподтверждённом шаге: - **Этап 1 (все дисплеи):** Red → Green → Blue → White - **Этап 2 (TFT7/8/10):** паттерн Red/Blue + горизонтальный флип — диагностика непропаянных LR/UD пинов @@ -379,8 +404,9 @@ sequenceDiagram | Тип | Interactive (in-run confirm) | | Время выполнения | ~4 с воспроизведение + до 15 с на confirm | -Тест воспроизводит мелодию (~4 с: нота A4 затем E5) через MQS-выход -(`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M. +Тест воспроизводит мелодию (~4 с: нота A4 затем E5, стерео PCM16 44100 Гц) +через MQS-выход (`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M. +Буфер — статический в некэшируемой секции (OCRAM NonCacheable), L == R. Оператор подтверждает слышимость тона. ```mermaid @@ -422,147 +448,6 @@ sequenceDiagram --- -## Запуск всего набора (run_all) - -Тесты запускаются строго в порядке реестра. При провале критичного теста -(`sdram` или `qspi`) все последующие тесты получают `SKIP` с `detail:"critical test failed"`. - -```bash -→ {"type":"cmd","cmd":"run_all"} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} -← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} -← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} -← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} - ... оператор вставляет карту и подтверждает ... -← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} - ...progress events... -← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} -← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} - ...confirm цикл 6 шагов... -← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} -← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} -← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} - ... -← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} -← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} -← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} - ... оператор слышит и подтверждает ... -← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} -← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} - ...confirm цикл 6 HIL шагов... -← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} -← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} - ...confirm цикл 2 HIL шагов... -← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} -← {"type":"summary","passed":8,"failed":0,"skipped":0,"overall":"pass"} -``` - -**SKIP-каскад при critical fail:** - -```bash -← {"type":"test_result","id":"sdram","status":"fail","ms":1203,"detail":"addr=0x80200001..."} -← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi","status":"skip","ms":0,"detail":"critical test failed"} -← {"type":"test_begin","id":"usd",...} -← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} - ... -← {"type":"summary","passed":0,"failed":1,"skipped":7,"overall":"fail"} -``` - ---- - -## Реестр тестов — порядок выполнения - -| № | ID | Название | Critical | HIL | Тип | -| --- | --------- | ------------------ | -------- | --- | ---------------------------- | -| 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test | -| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test | -| 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | -| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | -| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | -| 6 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) | -| 7 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | -| 8 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | - ---- - -## Диагностика — строки detail - -| Тест | Значение `detail` | Диагноз | -| --------- | ----------------------------------------------- | ------------------------------------------- | -| `sdram` | `addr=0x... exp=0x.. got=0x..` | Сбой ячейки по адресу | -| `sdram` | `SEMC not ready — DCD failed?` | DCD не инициализировал SEMC | -| `qspi` | `JEDEC: mfr=0xFF exp=0xEF` | Чип не отвечает / не пропаян | -| `qspi` | `JEDEC: unknown cap=0x..` | Неизвестный тип чипа | -| `qspi` | `erase verify failed at 0x...` | Сектор не стирается | -| `qspi` | `rw mismatch at 0x... exp=0x.. got=0x..` | Ошибка записи или чтения | -| `qspi` | `addr alias: 0x... mirrors 0x... (3-byte wrap)` | Dedicated 4-byte opcodes не работают | -| `usd` | `no card detected` | Карта не вставлена в слот | -| `usd` | `mount failed: ` | `f_mount()` вернул FRESULT N | -| `usd` | `write failed: ` | `f_write()` вернул FRESULT N | -| `usd` | `compare failed at offset ` | Данные после чтения не совпадают | -| `display` | `display init failed` | `bsp_display_init()` вернул ошибку | -| `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | -| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | -| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | -| `mqs` | `mqs play error` | SAI3/DMA не запустился | -| `mqs` | `operator: no sound` | Нет звука / усилитель не работает | -| `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | -| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | -| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | -| `can` | `rx data mismatch: got XX XX XX XX` | Данные фрейма не совпадают | -| `can` | `tx failed: bsp_can_send returned ` | TX timeout или шина недоступна | -| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета | -| любой | `confirm timeout` | pre-confirm не получен за 30 с | -| любой | `operator declined` | Получен `"confirmed":false` | -| любой | `critical test failed` | Предшествующий критичный тест провалился | - ---- - -## list_tests — получить реестр тестов - -```bash -→ {"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":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, - {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true} - ]} -``` - -TUI использует этот ответ для динамического построения списка тестов. -HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC не подключён. - ---- - -## run_selected — запустить подмножество тестов - -```bash -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} -← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} - ...confirm цикл 6 шагов (HIL)... -← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} -← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} -``` - -Порядок выполнения — как в реестре таргета, не как в запросе. -Если хотя бы один ID не найден — вся команда отклоняется: - -```bash -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","unknown_test"]} -← {"ok":false,"error":"UNKNOWN_TEST"} -``` - ---- - ## Оптоизолированные входы (HIL) | Параметр | Значение | @@ -583,7 +468,8 @@ HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC н | 6 | `opto_rs_inactive` | RLY2 OFF | `BSP_OPTO_CH_RS == INACTIVE` | HIL confirm полностью автоматический — TUI командует M5 и отправляет confirm -без участия оператора. +без участия оператора. После confirm тест выжидает settle (~30 мс, перекрывает +debounce 10 мс) и читает состояние через `bsp_opto_force_read()`. ```bash → {"type":"cmd","cmd":"run","id":"opto"} @@ -625,6 +511,150 @@ HIL confirm полностью автоматический — TUI команд --- +## list_tests — получить реестр тестов + +```bash +→ {"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":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, + {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, + {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false} + ]} +``` + +TUI использует этот ответ для динамического построения списка тестов. +HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC не подключён. +Порядок в ответе — порядок реестра `k_registry[]` (см. ниже). + +--- + +## run_selected — запустить подмножество тестов + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 шагов (HIL)... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} +``` + +Порядок выполнения — как в реестре таргета, не как в запросе. +Если хотя бы один ID не найден — вся команда отклоняется: + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","unknown_test"]} +← {"ok":false,"error":"UNKNOWN_TEST"} +``` + +--- + +## Запуск всего набора (run_all) + +Тесты запускаются строго в порядке реестра. При провале критичного теста +(`sdram` или `qspi`) все последующие тесты получают `SKIP` с `detail:"critical test failed"`. + +```bash +→ {"type":"cmd","cmd":"run_all"} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} +← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} + ... оператор вставляет карту и подтверждает ... +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} + ...progress events... +← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} +← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} + ...confirm цикл 6 шагов... +← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} +← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} + ... +← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 HIL шагов... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} + ...confirm цикл 2 HIL шагов... +← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} +← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} + ... оператор слышит и подтверждает ... +← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +← {"type":"summary","passed":8,"failed":0,"skipped":0,"overall":"pass"} +``` + +**SKIP-каскад при critical fail:** + +```bash +← {"type":"test_result","id":"sdram","status":"fail","ms":1203,"detail":"addr=0x80200001..."} +← {"type":"test_begin","id":"qspi",...} +← {"type":"test_result","id":"qspi","status":"skip","ms":0,"detail":"critical test failed"} +← {"type":"test_begin","id":"usd",...} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} + ... +← {"type":"summary","passed":0,"failed":1,"skipped":7,"overall":"fail"} +``` + +--- + +## Реестр тестов — порядок выполнения + +Порядок соответствует `k_registry[]` в `test_runner.c`. + +| № | ID | Название | Critical | HIL | Тип | +| --- | --------- | ------------------ | -------- | --- | ---------------------------- | +| 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test | +| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test | +| 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | +| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | +| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | +| 6 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | +| 7 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | +| 8 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) | + +--- + +## Диагностика — строки detail + +| Тест | Значение `detail` | Диагноз | +| --------- | ----------------------------------------------- | ------------------------------------------- | +| `sdram` | `addr=0x... exp=0x.. got=0x..` | Сбой ячейки по адресу | +| `sdram` | `SEMC not ready — DCD failed?` | DCD не инициализировал SEMC | +| `qspi` | `JEDEC: mfr=0xFF exp=0xEF` | Чип не отвечает / не пропаян | +| `qspi` | `JEDEC: unknown cap=0x..` | Неизвестный тип чипа | +| `qspi` | `erase verify failed at 0x...` | Сектор не стирается | +| `qspi` | `rw mismatch at 0x... exp=0x.. got=0x..` | Ошибка записи или чтения | +| `qspi` | `addr alias: 0x... mirrors 0x... (3-byte wrap)` | Dedicated 4-byte opcodes не работают | +| `usd` | `no card detected` | Карта не вставлена в слот | +| `usd` | `mount failed: ` | `f_mount()` вернул FRESULT N | +| `usd` | `write failed: ` | `f_write()` вернул FRESULT N | +| `usd` | `compare failed at offset ` | Данные после чтения не совпадают | +| `display` | `display init failed` | `bsp_display_init()` вернул ошибку | +| `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | +| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | +| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | +| `mqs` | `mqs play error` | SAI3/DMA не запустился | +| `mqs` | `operator: no sound` | Нет звука / усилитель не работает | +| `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | +| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | +| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | +| `can` | `rx data mismatch: got XX XX XX XX` | Данные фрейма не совпадают | +| `can` | `tx failed: bsp_can_send returned ` | TX timeout или шина недоступна | +| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета | +| любой | `confirm timeout` | pre-confirm не получен за 30 с | +| любой | `operator declined` | Получен `"confirmed":false` | +| любой | `critical test failed` | Предшествующий критичный тест провалился | + +--- + ## HIL pytest — автоматическая верификация через firmware_test CDC Два файла тестируют `test_opto` и `test_can` через реальный CDC-протокол v2. diff --git a/tools/host/flash_swd.py b/tools/host/flash_swd.py index 81213ac..c2eb833 100644 --- a/tools/host/flash_swd.py +++ b/tools/host/flash_swd.py @@ -163,7 +163,6 @@ def main() -> int: ) args = parser.parse_args() - # ── Проверить FCB ───────────────────────────────────────────────────────── if not args.fcb.exists(): print(f" ❌ FCB not found: {args.fcb}", file=sys.stderr) print( @@ -174,7 +173,6 @@ def main() -> int: ) return 1 - # ── Найти HAB-образ ─────────────────────────────────────────────────────── hab_name = HAB_NAMES[args.firmware] hab_path = BUILD_DIR / args.build_type / hab_name @@ -195,7 +193,6 @@ def main() -> int: print(f" Target : {args.target}") print(f" Frequency : {args.frequency} Hz\n") - # ── Собрать объединённый образ ──────────────────────────────────────────── image = build_full_image(args.fcb, hab_path) if args.dry_run: @@ -205,7 +202,6 @@ def main() -> int: print(f"\n Dry run — image saved to {out}") return 0 - # ── Записать во Flash через pyOCD ───────────────────────────────────────── with tempfile.NamedTemporaryFile( suffix=f"_{args.firmware}_full.bin", delete=False ) as tmp: diff --git a/tools/host/flash_usb.py b/tools/host/flash_usb.py index 76e8b6a..cd02696 100644 --- a/tools/host/flash_usb.py +++ b/tools/host/flash_usb.py @@ -387,7 +387,6 @@ def main() -> None: ), ) - # Группа: что прошивать (взаимоисключающие варианты) target_group = parser.add_mutually_exclusive_group() target_group.add_argument( "--firmware", @@ -420,9 +419,6 @@ def main() -> None: args = parser.parse_args() - # Валидация: --firmware требует --build-type (уже есть default, но запомним) - # --bin-path: build-type игнорируется - # --erase-chip: несовместим с --ram-only if args.erase_chip and args.ram_only: parser.error("--erase-chip несовместим с --ram-only") diff --git a/tools/production/README.md b/tools/production/README.md index 12b4553..e310f2d 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -1,10 +1,10 @@ # service-tui — TUI сервисного инженера TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. -Написано на Python + [Textual](https://textual.textualize.io/). Работает на MacOS, Windows. +Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. > Внутреннее устройство, диаграммы архитектуры и заметки для разработчиков — -> в [DEV_ARCH.md](docs/DEV_ARCH.md) +> в [DEV_ARCH.md](DEV_ARCH.md). --- @@ -17,16 +17,19 @@ TUI-приложение для диагностики и прошивки пл ```bash ┌────────────────────────────────────────────────────┐ -│ service_tool vX.Y.Z │ +│ service_tool v0.2.0 │ │ │ │ [LOGO_ART] │ │ │ │ Подключите плату индикатора к USB... ⠋ │ │ │ -│ [ ✕ Выйти из приложения ] │ +│ [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` +Версия читается из `pyproject.toml` — при бампе версии мокап выше не нужно +обновлять руками, TUI подставит актуальную сама. + При потере соединения на любом другом экране сессия разрывается полностью — TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над подсказкой на 4 секунды появляется строка `⚠ <причина>` (например, @@ -56,37 +59,30 @@ TUI не пытается восстановить прежнее состоян │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ -│ │ ▶ Прошивка: firmware_test │ │ +│ │ ▶ Сборка HAB-образа (HabImage)... │ │ +│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` -**Если после «Загрузить» появилась ошибка, а плата всё ещё видна на этом же -экране** — это ожидаемо: логическая ошибка (не найден файл, не подошёл -формат) не выкидывает на экран ожидания, потому что плата физически -подключена. Прочитайте сообщение в логе, поправьте выбор и нажмите -«Загрузить» ещё раз. На экран ожидания TUI переключает только при реальном -физическом обрыве USB. +Лог виден постоянно (не только во время прошивки), прогресс-бар — только +во время активной операции (скрыт в простое), без числового `%` — только +полоса и построчный лог в реальном времени. Панель выбора прошивки +ограничена по высоте и скроллится сама, если разрастается (варианты +"Другое") — лог снизу гарантированно не сжимается меньше 6 строк. **"Другое" — для бинарников, собранных не в этом репозитории.** В -`custom_binaries/` кладётся бинарник — сырой (код + таблица векторов, без -FCB/IVT/DCD) либо уже готовый HAB-образ, в зависимости от источника. TUI -сама достраивает недостающее на лету: +`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без +FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до сборки HAB). +TUI сама собирает из него загружаемый образ на лету, in-process через +Python API `spsdk` (без вызова внешних CLI-утилит): -1. Собирает загружаемый HAB-образ (добавляет IVT, +DCD — если включён - тумблер "Использует SDRAM") -2. В Flash пишется явный FCB под выбранную память платы (не тот же +1. `HabImage` (spsdk) — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") +2. в Flash пишется явный FCB под выбранную память платы (не тот же auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для - W25Q256/512 он ненадёжен, см. `DEV_ARCH.md §8.4`) -3. Образ прошивается стандартным адресом - -**Нужен ли тумблер DCD — зависит от конкретного бинарника, не от того, в -каком виде он получен.** Одна и та же связка `bootloader + tft_app` не -требует DCD, а часть кастомных/легаси образов (например, старый загрузчик, -используемый на производстве) требует его независимо от формата файла. Если -не уверены, нужен ли конкретному образу DCD — уточните у того, кто его -предоставил, прежде чем прошивать. + W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`) +3. образ прошивается с `0x60001000`, как обычно **Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не нужно выставлять заново на каждой следующей плате: прошили одну, вынули @@ -105,8 +101,8 @@ Production/Custom этот шаг не нужен). │ Переведите плату в нормальный режим: │ │ BOOT_MOD_1 → GND → Reset │ │ │ -│ Автопереход через: 40с │ │ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ +│ Автопереход через: 40с │ └────────────────────────────────────────────────────┘ ``` @@ -137,7 +133,6 @@ Production/Custom этот шаг не нужен). ``` Что важно знать: - - **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать все"/"Снять все". - **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена @@ -157,7 +152,7 @@ Production/Custom этот шаг не нужен). --- -## Рабочие процессы сервисного инженера +## Рабочие процессы сервисника ### Диагностика (firmware_test уже прошит) @@ -184,12 +179,11 @@ Production/Custom этот шаг не нужен). Для плат старых ревизий и любых образов, собранных не в этом репозитории. ```bash -1. Положить бинарник (сырой или уже HAB, см. раздел выше) в custom_binaries/ +1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/ (или в директорию из SERVICE_CUSTOM_BINARIES_DIR) 2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen -3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD, если - конкретно этот образ его требует (уточнить у источника файла) -4. Загрузить — TUI сама соберёт HAB-образ (если нужно) и запишет правильный FCB +3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости +4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB 5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже подставлен, останется нажать «Загрузить» ``` @@ -204,9 +198,30 @@ Production/Custom этот шаг не нужен). --- +## Известные ограничения + +- **Одна плата на столе одновременно.** В SDP/Flashloader-режиме плату + нельзя идентифицировать по UID — авто-прошивка по факту детекта без + подтверждения оператора убрала бы последний шанс заметить, что в руках + не та плата. Массового программирования (несколько плат параллельно) + нет и не планируется в этом виде — см. `docs/DEV_ARCH.md`, §8. +- **Циклический прогон тестов** (повторный автозапуск набора без ручного + нажатия на каждую итерацию) — отложен на пост-релиз, не входит в текущую + версию. + +--- + ## Конфигурация (`.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 @@ -215,7 +230,7 @@ SERVICE_CDC_PID=00ad SERVICE_M5_VID=303a SERVICE_M5_PID=4001 -# Директория с кастомными бинарниками для FlashScreen → "Другое". +# Директория с сырыми кастомными бинарниками для FlashScreen → "Другое". # По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом # с main.py в dev-режиме); создаётся автоматически при старте. # SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries @@ -224,19 +239,14 @@ SERVICE_M5_PID=4001 # Release временно нестабилен — по умолчанию Debug. FIRMWARE_BUILD_TYPE=Debug -# Уровень логирования. По умолчанию INFO (плюс WARNING принудительно для -# шумных модулей spsdk/libusbsio). DEBUG — полный лог, включая построчные -# HID-дампы каждой команды spsdk (для диагностики проблем прошивки). -# SERVICE_LOG_LEVEL=DEBUG - # Опционально: путь к директории лога TUI # SERVICE_LOG_DIR=/tmp -``` -> `.env` не загружается в упакованном (frozen) приложении — standalone-бинарь -> работает на встроенных значениях по умолчанию. Переменные окружения (не -> `.env`-файл) по-прежнему действуют и во frozen-режиме, если их выставить -> перед запуском. +# Уровень логирования. По умолчанию — INFO, при этом spsdk/libusbsio +# принудительно приглушены до WARNING. DEBUG включает полный дамп, +# включая сырые HID-пакеты spsdk (много строк на одну прошивку). +# SERVICE_LOG_LEVEL=DEBUG +``` --- @@ -245,56 +255,55 @@ FIRMWARE_BUILD_TYPE=Debug ### Из монорепозитория (разработчик) ```bash -just host::service-setup # установить/обновить зависимости tools/production/ +just host::service-setup # установить зависимости tools/production/ just host::service-tui # запустить TUI ``` ### Standalone-бинарь (сервисник) -Распаковать `service-tui-vX.Y.Z-.zip` в любую директорию и запустить -`service_tui` (`service_tui.exe` на Windows). Файл самодостаточен — не -требует установленного Python, `uv`, драйверов (Zadig/WinUSB) или сетевого -доступа. - -### Сборка релизного бандла (разработчик) - ```bash -just build::hab-all-release # или hab-all-debug — собрать HAB-образы заранее -just host::package-tui # → tools/production/dist/service-tui-vX.Y.Z-/ +just host::package-tui +# → tools/production/dist/service-tui-vX.Y.Z-/ ``` -Устройство бандла (`_internal/`, `firmware/`, `custom_binaries/`) и детали -сборки (`service_tui.spec`) — в [DEV_ARCH.md §14](DEV_ARCH.md#14-упаковка-pyinstaller-фаза-5). - -> Если на Windows `package-tui` падает с `Permission denied` на шаге -> переименования — закройте запущенный `service_tui.exe` от предыдущей -> сборки и повторите (см. `DEV_ARCH.md §14.4`). +Бандл (PyInstaller, onedir) самодостаточен — прошивка идёт напрямую через +spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` ни +субпроцессом, ни как-либо иначе. Не нужны ни Zadig, ни установленный +Python/uv на машине сервисника. Структура бандла и резолв путей во frozen — +см. [DEV_ARCH.md](docs/DEV_ARCH.md), §14. --- ## Зависимости -| Пакет | Версия | Назначение | -| --------------- | ------ | -------------------------------------------------------------------------------------- | -| `textual` | ≥ 0.80 | TUI фреймворк | -| `pyserial` | ≥ 3.5 | USB CDC ACM (firmware_test) + Serial (M5StampPLC) | -| `spsdk` | 3.7.0 | прошивка in-process: SDP, McuBoot, HabImage | -| `python-dotenv` | ≥ 1.0 | загрузка `.env` | -| `pyinstaller` | ≥ 6.0 | сборка standalone-бандла | -| `pyusb` | ≥ 1.0 | не используется текущей детект-логикой (см. `DEV_ARCH.md §2`), оставлен в зависимостях | +| Пакет | Версия | Назначение | +| --------------- | ------- | -------------------------------------------------------- | +| `textual` | ≥ 0.80 | TUI фреймворк | +| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | +| `pyusb` | ≥ 1.0 | не используется в коде (детект SDP/CDC идёт через `spsdk`) — исторический остаток, кандидат на удаление из `pyproject.toml` | +| `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 его не +вызывает (см. [DEV_ARCH.md](docs/DEV_ARCH.md), §1/§8). --- ## Логирование ```bash -tools/production/service_tui.log ← по умолчанию (рядом с main.py в dev, - рядом с exe во frozen) -$SERVICE_LOG_DIR/service_tui.log ← если задан в .env / окружении +tools/production/service_tui.log ← по умолчанию (dev) / рядом с exe (frozen) +$SERVICE_LOG_DIR/service_tui.log ← если задан в .env ``` -Уровень по умолчанию: `INFO` для модулей приложения (`WARNING` для `textual` -и принудительно для шумных модулей `spsdk`/`libusbsio`, которые на `DEBUG` -печатают построчные HID-дампы каждой команды). Полный `DEBUG` — через -`SERVICE_LOG_LEVEL=DEBUG`. TUI не пишет в stdout — Textual захватывает -терминал. +Уровень по умолчанию — `INFO`; `textual`, `spsdk` и `libusbsio` понижены до +`WARNING` независимо от root (иначе прошивка даёт ~135 строк сырых +HID-пакетов на одну операцию). `SERVICE_LOG_LEVEL=DEBUG` включает полный +DEBUG везде, включая эти модули — используется при разборе проблем на +железе. TUI не пишет в stdout — Textual захватывает терминал. diff --git a/tools/production/app/app.py b/tools/production/app/app.py index 1319d5b..80b44ed 100644 --- a/tools/production/app/app.py +++ b/tools/production/app/app.py @@ -50,8 +50,6 @@ class ServiceApp(App): self.push_screen(WaitingScreen()) # ── Переходы между экранами ─────────────────────────────────────────────── - - @on(WaitingScreen.DeviceDetected) @on(WaitingScreen.DeviceDetected) def _on_device_detected(self, event: WaitingScreen.DeviceDetected) -> None: if event.mode == AppMode.FLASHING: @@ -97,7 +95,6 @@ class ServiceApp(App): @on(DiagScreen.DiagDone) def _on_diag_done(self, event: DiagScreen.DiagDone) -> None: - """После диагностики — отключиться, вернуться в Waiting.""" self._disconnect() self.switch_screen(WaitingScreen(disconnect_reason=event.reason)) diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py index aa01df0..e5a6613 100644 --- a/tools/production/app/firmware_client.py +++ b/tools/production/app/firmware_client.py @@ -33,11 +33,9 @@ from .models import TestInfo logger = logging.getLogger(__name__) -# Таймаут чтения одной строки (сек) _READLINE_TIMEOUT_S = 0.1 -# Таймаут ping→pong при подключении (сек) _PING_TIMEOUT_S = 5.0 -# Таймаут ожидания событий теста (сек) — длиннее самого долгого теста (SDRAM ~15 с) +# Длиннее самого долгого теста (SDRAM ~15 с) _TEST_EVENT_TIMEOUT_S = 120.0 diff --git a/tools/production/app/m5_client.py b/tools/production/app/m5_client.py index cd0c4fe..8ffe395 100644 --- a/tools/production/app/m5_client.py +++ b/tools/production/app/m5_client.py @@ -30,7 +30,6 @@ import serial.tools.list_ports logger = logging.getLogger(__name__) -# VID/PID M5StampPLC _M5_VID = int(os.environ.get("SERVICE_M5_VID", "0x303A"), 16) _M5_PID = int(os.environ.get("SERVICE_M5_PID", "0x4001"), 16) diff --git a/tools/production/app/orchestrator.py b/tools/production/app/orchestrator.py index 95a1d06..a65e07a 100644 --- a/tools/production/app/orchestrator.py +++ b/tools/production/app/orchestrator.py @@ -51,12 +51,10 @@ from .models import ConfirmRequest, TestResult, TestStatus logger = logging.getLogger(__name__) -# Задержки для HIL _RELAY_ON_S = 0.15 _RELAY_OFF_S = 0.50 -# Карта confirm_id → реле M5 для opto-теста -# Формат: confirm_id → (relay_num, target_state) +# confirm_id → (relay_num, target_state) _OPTO_RELAY_MAP: dict[str, tuple[int, bool]] = { "opto_in1_active": (3, True), "opto_in1_inactive": (3, False), @@ -66,7 +64,6 @@ _OPTO_RELAY_MAP: dict[str, tuple[int, bool]] = { "opto_rs_inactive": (2, False), } -# CAN параметры _CAN_RX_ID = 0x100 _CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF] _CAN_TX_ID = 0x200 @@ -80,13 +77,13 @@ _TIMEOUT_EVENT_TYPE = "_timeout" class OrchestratorEventType(Enum): - TEST_BEGIN = auto() # тест начался - TEST_RESULT = auto() # тест завершился + TEST_BEGIN = auto() + TEST_RESULT = auto() TEST_PROGRESS = auto() # внутришаговый прогресс долгого теста (usd и т.п.) CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone) CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm) - SUMMARY = auto() # итог всей сессии + SUMMARY = auto() ERROR = auto() # ошибка протокола, M5, или обрыв по таймауту diff --git a/tools/production/app/screens/diag/test_list.py b/tools/production/app/screens/diag/test_list.py index 068f8f9..ff1da21 100644 --- a/tools/production/app/screens/diag/test_list.py +++ b/tools/production/app/screens/diag/test_list.py @@ -24,7 +24,6 @@ class TestListPanel(Widget): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - # test_id → Checkbox для быстрого доступа self._checkboxes: dict[str, Checkbox] = {} # test_id → True если HIL-тест недоступен без M5 (постоянное состояние, # не зависящее от прогона). Отдельно от Checkbox.disabled, который diff --git a/tools/production/app/screens/flash.py b/tools/production/app/screens/flash.py index 45608d4..c4e0b32 100644 --- a/tools/production/app/screens/flash.py +++ b/tools/production/app/screens/flash.py @@ -173,9 +173,8 @@ class FlashScreen(Screen, ConnectionWatcherMixin): self._stop_connection_watch() def _check_sdp_present(self) -> bool: - # Не считаем потерей соединения, если идёт активная операция — - # flash_usb.py сам обработает реальный обрыв через subprocess. - # обрыв в этом случае обнаружит и обработает сам flash_backend + # Не считаем потерей соединения, если идёт активная операция — обрыв + # в этом случае обнаружит и обработает сам flash_backend # (ConnectionLostError, см. Фазу 4), не watcher. if self._flashing: return True diff --git a/tools/production/docs/DEV_ARCH.md b/tools/production/docs/DEV_ARCH.md index a7b4655..f402ea9 100644 --- a/tools/production/docs/DEV_ARCH.md +++ b/tools/production/docs/DEV_ARCH.md @@ -6,42 +6,41 @@ > взаимодействия с firmware/M5, экранную архитектуру Textual, известные > особенности фреймворка. > Пользовательская документация (экраны, запуск, конфигурация, -> рабочие процессы сервисника) — в [README.md](../README.md). +> рабочие процессы сервисника) — в [README.md](README.md). --- ## 1. Структура проекта -## 1. Структура проекта - ```bash tools/production/ -├── main.py ← точка входа -├── pyproject.toml ← зависимости uv -├── dist/ ← дистрибутивы программы (PyInstaller) +├── main.py ← точка входа: логирование (Р12) + ServiceApp().run() +├── pyproject.toml ← зависимости uv (включая spsdk==3.7.0) ├── uv.lock -├── service_tui.spec ← PyInstaller spec -├── custom_binaries/ ← runtime, создаётся автоматически; -│ сырые/готовые бинарники для FlashScreen → «Другое» -└── app/ ← implicit namespace package - │ - │ +├── service_tui.spec ← PyInstaller spec (Фаза 5, onedir) +├── custom_binaries/ ← runtime, gitignored, создаётся автоматически +│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое» +├── tests/ +│ └── test_flash_backend.py ← unit-тесты flash_backend.py (45 тестов, без event loop) +├── spike/ ← Фаза 0, де-риск spsdk API (в релиз не идёт) +└── app/ ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов ├── app.tcss ← единый файл стилей для всех экранов ├── models.py ← все типы данных (dataclass/Enum) ├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen - ├── firmware_client.py ← async USB CDC клиент firmware_test - ├── m5_client.py ← async M5StampPLC клиент - ├── flash_backend.py ← spsdk 3.7.0 in-process: SDP, McuBoot, HabImage - ├── flasher.py ← async-обёртка над flash_backend для Textual workers - ├── usb_ports.py ← резолвер serial-портов по VID:PID + ├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8) + ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8) + ├── usb_ports.py ← resolve_serial_port() — резолв COM/tty по VID:PID (Р8) + ├── flash_backend.py ← синхронное ядро прошивки: прямой spsdk API (McuBoot/SDP/HabImage), + │ zero Textual/asyncio импортов, тестируется без event loop + ├── flasher.py ← async-обёртка над flash_backend.py (asyncio.to_thread) ├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты ├── widgets/ │ ├── __init__.py │ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов └── screens/ ├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen - ├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер, кнопка «Выйти» + ├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер причины возврата ├── flash.py ← FlashScreen — прошивка / chip erase ├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки ├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB @@ -52,6 +51,15 @@ tools/production/ └── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown ``` +> **Разделение dev-CLI / production-TUI:** `tools/host/flash_usb.py` (subprocess +> sdphost/blhost, используется just-рецептами `just host::flash*`) и +> `app/flash_backend.py` (прямой spsdk Python API) — две независимые +> реализации одной и той же логики прошивки. `flash_backend.py` — прямой +> порт `flash_usb.py` на spsdk API (см. заголовок модуля), но TUI больше не +> вызывает `flash_usb.py` ни субпроцессом, ни как библиотеку. `tools/host/` +> используется TUI только как источник статичных data-блобов +> (`tools/host/dcd/*.bin`) в dev-режиме — см. §8. + --- ## 2. Концепция @@ -63,31 +71,48 @@ graph LR subgraph app["app/"] FC["firmware_client.py\nUSB CDC ACM, UTF-8"] M5["m5_client.py\nSerial JSON-lines, UTF-8"] - FL["flasher.py + flash_backend.py\nspsdk in-process: SDP/McuBoot/HabImage"] + FL["flasher.py\nasyncio.to_thread мост"] + FB["flash_backend.py\nspsdk: McuBoot/SDP/HabImage"] OR["orchestrator.py\nconfirm/progress/timeout router"] end TUI --> FC & M5 & FL & OR + FL --> FB end subgraph Board["Плата TFT (MIMXRT1052)"] FW["firmware_test\n(USB CDC)"] ROM["BootROM SDP\n(1FC9:0130)"] + FLD["Flashloader\n(15A2:0073, RAM-резидент)"] end subgraph HIL["HIL стенд (опционально)"] M5HW["M5StampPLC\nRLY1–4 + CAN"] end - FC |"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW - FL |"spsdk (libusbsio HID)\nVID:PID 1FC9:0130 / 15A2:0073"| ROM - M5 |"JSON-lines\nSerial"| M5HW + FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW + FB -->|"SDP.write_file + jump_and_run\n(spsdk.sdp)"| ROM + ROM -.->|"загружает ivt_flashloader.bin"| FLD + FB -->|"McuBoot: erase/write_memory/reset\n(spsdk.mboot)"| FLD + M5 <-->|"JSON-lines\nSerial"| M5HW M5HW -->|"RLY1–4"| Board ``` -> **Детект USB:** `flash_backend.detect_sdp()`/`detect_cdc()` используют spsdk -> напрямую (`SdpUSBInterface.scan()` / `MbootUSBInterface.scan()`, HID-транспорт -> через `libusbsio`). CDC firmware_test и M5StampPLC резолвятся через -> `pyserial` (`usb_ports.py::resolve_serial_port()`, `m5_client.py`). +> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` делегируют в +> `flash_backend.detect_sdp()`/`detect_cdc()` (Р7 — `spsdk`-сканеры +> `SdpUSBInterface.scan()`/`serial.tools.list_ports`, БЕЗ `pyusb`: BootROM +> SDP не создаёт serial-порт на macOS, но `spsdk` видит его нативно через +> HID/libusbsio без Zadig на Windows). `usb_ports.resolve_serial_port()` +> резолвит CDC-порты (firmware_test, M5) по VID:PID, а не по имени порта — +> имя не переносимо между перевтыкиваниями (см. Р8, `usb_ports.py`). +> M5StampPLC детектируется отдельно в `m5_client.py` (VID/PID из `.env` — +> см. раздел 5). +> +> **`flash_backend.py` не вызывает `tools/host/flash_usb.py`** ни +> субпроцессом, ни как библиотеку — это прямой порт той же логики на +> spsdk Python API (`McuBoot`/`SDP`/`HabImage` вместо `sdphost`/`blhost`/ +> `nxpimage` CLI), провалидированный байт-в-байт на живом железе (Фаза 0). +> `flash_usb.py` остаётся независимым dev-CLI для `just host::flash*` — +> см. §8 (конвейер сборки) и §6.2 (обработка ошибок прошивки). --- @@ -105,7 +130,7 @@ stateDiagram-v2 WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC FLASHING --> POST_FLASH : firmware_test прошит успешно - FLASHING --> WAITING : Production/Custom прошит,\nили потеря USB (в простое ИЛИ во время операции) + FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с @@ -117,13 +142,6 @@ stateDiagram-v2 `ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на сообщения `DeviceDetected`/`FlashDone`/`DiagDone`. -> **Важная деталь, не показанная на диаграмме**: -> `FLASHING --> WAITING` по стрелке «ошибка» срабатывает **только** при -> физическом обрыве USB (`FlashResult.connection_lost=True`). Логическая -> ошибка (файл не найден, битый custom-бинарь) — плата на месте, экран -> остаётся на `FLASHING` (нет перехода состояния вообще, поэтому на -> диаграмме это не отдельная стрелка). См. §6. - --- ## 4. Обработка confirm_request @@ -163,7 +181,9 @@ flowchart TD **Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно одним событием `SUMMARY` — настоящим от firmware или синтетическим -(`aborted: true`), если чтение порта оборвалось по таймауту. +(`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии +зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда +(исторический баг, см. `CHANGELOG.md`). --- @@ -191,49 +211,85 @@ flowchart TD PID — при детекте ориентироваться на `just host::m5-scan`, а не на документацию, если она когда-либо разойдётся с кодом. +**Важно на будущее:** документация (`HIL_BENCH.md`/`HIL_HOW_TO.md`) местами не +успевает за изменениями `agent.py`. При любых будущих изменениях протокола +агента (новые команды, смена формата ответа) — сверяться напрямую через +`grep` по `tools/hil/m5/agent.py`, а не полагаться только на документацию. + --- ## 6. Мониторинг соединения и разрыв сессии +### 6.1 Простой (`ConnectionWatcherMixin`) + `ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к `FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине. - **На `FlashScreen`** — проверка приостановлена во время активной - прошивки/erase (`self._flashing == True`). + прошивки/erase (обрыв в этом случае обнаруживает сам `flash_backend.py`, + см. §6.2, — не watcher). - **На `DiagScreen`** — проверка приостановлена во время прогона тестов (обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не просто исчезновение устройства из списка). +- При срабатывании — `ConnectionLost` message → экран постит + `FlashDone(success=False, target=None)` / `DiagDone(reason=...)` → + `ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает + на `WaitingScreen(disconnect_reason=...)`. `FlashDone` в этой ветке не несёт + `preset` — «липкий» выбор (см. §8) сохраняется отдельно, в момент нажатия + «Загрузить», а не при завершении прошивки. +- `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем + продолжает обычный автодетект. -**Три независимых механизма детекта обрыва**: - -1. **`ConnectionWatcherMixin` в простое** — периодический опрос шины. -2. **`flash_backend.py` во время активной операции** — spsdk бросает - `SPSDKConnectionError`/`SPSDKTimeoutError` (оба ловятся явным кортежем - `_CONNECTION_LOST_EXCEPTIONS` — `SPSDKTimeoutError` НЕ наследует - `SPSDKConnectionError`, оба - потомки `SPSDKError` -3. **Вариант B** — некоторые команды spsdk (`flash_erase_all`, - `write_memory` и т.п.) при таймауте не бросают исключение, а тихо - возвращают `False`. `_fail_command()` в этом случае сам проверяет - `_sdp_still_present()`: плата пропала с шины → `ConnectionLostError`; - плата на месте → обычная `FlashBackendError`. - -Оба механизма 2 и 3 транслируются в `Flasher.flash()`/`erase_chip()` как -`FlashResult(ok: bool, connection_lost: bool)` — **не голый `bool`**. Это -принципиально для `FlashScreen`: - -- `connection_lost=True` → `FlashDone(target=None, error_message=...)` → - `ServiceApp` переключает на `WaitingScreen(disconnect_reason=...)`. -- `connection_lost=False` → **экран не покидает себя**. - Плата физически на месте, сообщение об ошибке уже в `#flash-log`, кнопки - разблокированы (`_set_busy(False)`) — оператор может поправить выбор - (другой файл, другой вариант памяти) и повторить, не выдёргивая USB. - -`WaitingScreen` показывает причину возврата баннером на 4 секунды, затем -продолжает обычный автодетект. - -**Cессия никогда не восстанавливается** — после +Архитектурное решение: **сессия никогда не восстанавливается** — после разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует -заново с нуля. +диагностику с нуля. + +### 6.2 Во время активной операции (`FlashBackendError`, Фаза 4/4a) + +Пока `FlashScreen._flashing == True`, watcher приглушён (§6.1) — обрыв в +этот момент обнаруживает сам `flash_backend.py` через иерархию исключений: + +``` +FlashBackendError (connection_lost: bool = False) +├── ConnectionLostError (connection_lost = True — обёртка +│ над SPSDKConnectionError/SPSDKTimeoutError) +├── DeviceNotFoundError (SDP не найден ДО начала операции) +├── FlashLoaderTimeoutError (Flashloader не поднялся за 10с) +└── HabBuildError (сборка HAB не удалась, к USB не относится) +``` + +- **`_CONNECTION_LOST_EXCEPTIONS = (SPSDKConnectionError, SPSDKTimeoutError)`** + — оба варианта прилетают на обрыве USB (`SPSDKTimeoutError` — потомок + `SPSDKError`, но **не** `SPSDKConnectionError`; read-фаза после write может + отдать голый таймаут вместо connection error). Ловятся кортежем на всех + точках отказа: `load_flashloader`, `flash` (основная + `ram_only` ветки), + `erase_chip`. +- **Вариант B для команд, возвращающих `False` без исключения** (Р10, + `_fail_command()`): `flash_erase_region`/`flash_erase_all`/`write_memory` + иногда просто возвращают `False` вместо исключения. В этом случае + `_fail_command()` выполняет быстрый `_sdp_still_present()` (обёрнутый в + `try/except` — любая ошибка самой проверки трактуется как «устройства + нет», т.к. шина к этому моменту уже нестабильна): устройство пропало → + `ConnectionLostError`, устройство на месте → обычный `FlashBackendError` с + текстом ошибки операции. Проверка добавляется **только в error-путь**, на + happy path не влияет. +- **`Flasher._run_flash_op()`** (`flasher.py`) конвертирует + `FlashBackendError` обратно в `FlashResult(ok=False, + connection_lost=exc.connection_lost)` + событие `FlashProgress(phase="error")`. + Отдельный `except Exception` — safety net на любое непредвиденное + исключение (гарантирует `ok=False` вместо зависших кнопок); `_format_error_message()` + добавляет префикс «Соединение с платой потеряно» для `connection_lost=True`. +- **`FlashScreen`** различает результат (см. docstring `FlashDone`, §9): + `connection_lost=True` → `WaitingScreen` (тот же маркер `target=None`, что + и watcher-детект в простое, текст ошибки прокидывается через + `FlashDone.error_message`); `connection_lost=False` → плата на месте, + экран остаётся на `FlashScreen` (иначе `WaitingScreen` почти мгновенно + переоткрывал бы `FlashScreen` заново и уничтожал `#flash-log` раньше, чем + оператор успевал прочитать сообщение об ошибке). + +USB-интерфейс из `load_flashloader()` закрывается в `finally` +(`_close_iface_quiet`) на любом исходе — защита от утечки HID-хэндла в +редком окне «интерфейс получен → USB выдернут → `McuBoot.__enter__` упал». --- @@ -272,14 +328,15 @@ AppFrame { ### 8.1 Проблема -Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются заранее -(`just build::hab-*`) и всегда идут на плату с W25Q128 — для них auto-config -Flashloader достаточен. Для сторонних/легаси бинарников (старые платы, -W25Q256/512) это не так: auto-config Flashloader не документирован как -надёжный для 4-байтной адресации, а сами бинарники приходят «сырыми» (код + -таблица векторов, без FCB/IVT/DCD) либо уже готовым HAB-образом — зависит от -источника. Решение — собирать HAB на лету (если нужно) и писать FCB явно, а -не полагаться на auto-config. +Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются +`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 — +для них auto-config Flashloader (`configure-memory 0xC0000007` → +`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси +бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader +не документирован как надёжный для 4-байтной адресации, а сами бинарники +приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат, +что `inputImageFile` в `hab_*.yaml` до сборки). Решение — собирать HAB +на лету и писать FCB явно, а не полагаться на auto-config. ### 8.2 Модели (`models.py`) @@ -306,44 +363,76 @@ class FlashPreset: одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/ памяти/DCD, нажал «Загрузить», вынул, вставил следующую. -**Нужен ли DCD — implementation-defined, зависит от конкретного бинарника, -не от его формата (сырой/готовый HAB).** Правило «сырой → включить DCD, -готовый HAB → выключить» **неверно как общее правило**: например, в связке -`bootloader + tft_app` сам `bootloader` не требует DCD, а часть кастомных -бинарников (в т.ч. старый загрузчик, используемый на производстве) требует -DCD независимо от того, в каком виде получен файл. Оператор должен знать -по конкретному образу, инициализирует ли он SDRAM самостоятельно — TUI не -может определить это автоматически по содержимому файла. +Рассматривался отдельный режим «массовое программирование» (авто-прошивка +по факту детекта SDP, без нажатия кнопки на каждую плату) — отклонён: +в SDP/Flashloader-режиме нет способа прочитать UID платы, авто-старт без +подтверждения оператора убирает последний шанс заметить, что в руках не та +плата. Оставлена только «липкая» память выбора (этот раздел). -### 8.3 Конвейер сборки (`flasher.py`) +### 8.3 Конвейер сборки — прямые вызовы spsdk (`flash_backend.py`) -```bash -Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) -└── _flash_custom() -├── _build_custom_hab(raw_bin, use_dcd, progress_cb) -│ └── flash_backend.build_custom_hab() — in-process spsdk API: -│ Config (family=mimxrt1050, startAddress=0x60000000, -│ ivtOffset=0x1000, initialLoadSize=0x2000, -│ + DCDFilePath, если use_dcd) → HabImage.export() -└── _run_flash_op(flash_backend.flash, hab_bin, fcb_path=...) -временный HAB-образ удаляется после прошивки -(finally: shutil.rmtree(hab_bin.parent)) +Сборка HAB-образа и прошивка выполняются **in-process** через Python API +`spsdk` — никакого subprocess/CLI (`nxpimage`/`sdphost`/`blhost`), в отличие +от dev-CLI `tools/host/flash_usb.py`, который остаётся отдельной, +независимой реализацией на тех же CLI-утилитах (см. §1, врезка про +разделение dev-CLI/production-TUI). + +``` +Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) [flasher.py] + └── _flash_custom() [flasher.py] + ├── asyncio.to_thread(flash_backend.build_custom_hab, raw_bin, use_dcd) + │ ├── _make_hab_config() — генерирует YAML с АБСОЛЮТНЫМИ путями + │ │ во временном work_dir (tempfile.mkdtemp), + DCDFilePath + │ │ на real_dcd_bin_path() если use_dcd + │ ├── Config.create_from_file() + HabImage.get_validation_schemas_from_cfg() + │ │ + cfg.check() — валидация конфига (spsdk.image.hab.hab_image) + │ ├── HabImage.load_from_config(cfg).export() — сборка байт HAB-образа + │ │ в памяти (та же логика, что nxpimage CLI, см. Фазу 0 — golden-тест + │ │ byte-exact, test_build_custom_hab_bytes_match_golden) + │ └── эмитит progress_cb(phase="hab_build", 0% → 100%) + └── asyncio.to_thread(flash_backend.flash, hab_bin, fcb_path=fcb_blob_path(...)) + ├── load_flashloader() — SDP.write_file()+jump_and_run(), ждёт + │ поднятия McuBoot (wait_for_flashloader, poll scan()) + └── with McuBoot(iface): configure_flexspi() → flash_erase_region() + → write_fcb_explicit(fcb_path) → write_memory(hab_bin) + → reset(reopen=False) + (finally: shutil.rmtree(hab_bin.parent) — временная директория + build_custom_hab() удаляется целиком, не только *.bin) ``` -`dcd/dcd.bin` (`tools/host/dcd/dcd.bin`) — один и тот же файл независимо от -проекта (SEMC/SDRAM-init не зависит от того, что именно исполняется), простой -константный путь, без вариантов. Резолвится через `flash_backend._host_dcd_dir()` -— двухрежимный (dev/frozen), см. §14. +`dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не +зависит от того, что именно исполняется), поэтому просто константный путь, +без вариантов. Пути к data-блобам (`dcd.bin`, `*_fdcb.bin`, +`ivt_flashloader.bin`) резолвятся двухрежимно (`_host_dcd_dir()`, +`flash_backend.py`): в dev — `tools/host/dcd/` (те же файлы, что использует +`flash_usb.py`, отслеживаются в git), в frozen-бандле — +`sys._MEIPASS/data` (см. §14). -### 8.4 Явная запись FCB вместо auto-config +### 8.4 Явная запись FCB вместо auto-config (`write_fcb_explicit`) -`flash_backend.py::write_fcb_explicit()` — `write_memory(0x60000000, fcb_bin)`, -буквальная запись 512-байтного FCB-блоба (tag `FCFB`), а не magic option word -`0xF000000F`. Обязателен для кастомных бинарей — auto-config Flashloader -проверен только для W25Q128 (см. §Известные открытые вопросы). +```python +def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None: + """Пишет буквальный FCB-блоб (512 байт) в Flash[FLASH_BASE] (custom-бинари). -Штатный путь (`firmware_test`/`bootloader`/`app` из `build//`) не -затрагивается — использует auto-config, как и раньше. + См. flash_usb.py::write_fcb_explicit — nxpimage не кладёт FCB в HAB-образ, + поэтому для произвольных чипов нужен явный блоб под конкретный memory chip. + """ +``` + +`mboot.write_memory(FLASH_BASE, data, mem_id=0)` — буквальная запись +512-байтного FCB-блоба (tag `FCFB`), а не magic option word `0xF000000F` +(`write_fcb_auto`, применяется только для штатных +`firmware_test`/`bootloader`/`app`, см. `flash()` в `flash_backend.py`: +параметр `fcb_path=None` → auto-config). Активируется передачей +`fcb_path` в `flash_backend.flash()` — путь без него не тронут: штатная +прошивка идёт через auto-config `write_fcb_auto`, как и раньше. + +Таймаут для `flash_erase_all` (chip erase) увеличен до `ERASE_ALL_TIMEOUT_MS += 200_000` мс (эквивалент `blhost -t 200000`): W25Q512 стирается заметно +дольше W25Q128, дефолтного таймаута McuBoot не хватало. +`flash_erase_region` (стирание пары секторов под FCB+HAB при обычной +прошивке) не трогали — там масштаб на порядки меньше, дефолта достаточно +независимо от чипа. ### 8.5 UI (`flash.py`) @@ -359,13 +448,6 @@ Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) дополнительно защищён `min-height: 6` — лог гарантированно виден даже в худшем случае. -**Троттлинг лога:** прогресс-бар обновляется на каждом -событии `FlashProgress`, но `#flash-log` для фазы `write` пишет только при -пересечении 10%-границы — без этого запись HAB-образа даёт ~135 строк в лог -на одну прошивку. Первая строка фазы (`"Запись <имя> (<размер> байт)"`) всегда -проходит; остальные фазы (`configure`/`erase`/`fcb`/`reset`/`error`) логируются -без троттлинга — их и так немного. - --- ## 9. Архитектура экранов @@ -390,7 +472,7 @@ graph TB subgraph Clients["Клиенты"] FC["FirmwareClient"] M5["M5Client"] - FL["Flasher\n(async) + flash_backend\n(spsdk in-process)"] + FL["Flasher"] end WS -->|"DeviceDetected(FLASHING)"| FS @@ -429,7 +511,7 @@ sequenceDiagram OP->>TUI: запустить service_tui TUI->>WS: push_screen() - WS->>WS: USB poll каждые 1.5 с + WS->>WS: spsdk/list_ports poll каждые 1.5 с OP->>FW: подключить плату USB WS->>TUI: DeviceDetected(DIAGNOSING) @@ -489,7 +571,7 @@ sequenceDiagram --- -## 11. Версионирование firmware и TUI +## 11. Версионирование firmware `firmware_test` версионируется через CMake (`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через @@ -502,10 +584,7 @@ sequenceDiagram отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из `pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata` сознательно не используется — проект не ставится как пакет -(`tool.uv.package = false`), метаданных может не быть. Резолв -`Path(__file__).resolve().parents[2] / "pyproject.toml"` одинаково корректен -в dev и frozen (относительный от модуля, а не абсолютный) — при условии, что -`service_tui.spec` кладёт `pyproject.toml` в корень бандла (см. §14). +(`tool.uv.package = false`), метаданных может не быть. --- @@ -544,9 +623,7 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл `mount_all()`. - **`CSS_PATH` резолвится относительно файла класса**, не относительно корня проекта — постоянно расходится при рефакторинге структуры. Решение: один - `CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. Во frozen - дополнительно требует, чтобы `app.tcss` физически лежал в бандле по тому же - относительному пути (см. §14). + `CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. - **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает ширину по умолчанию равную длине заголовка — длинный контент обрезается независимо от `height` строки. Нужно использовать `add_column(label, @@ -570,112 +647,123 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл --- -## 14. Упаковка +## 14. Упаковка PyInstaller и frozen-резолв путей (Фаза 5) -### 14.1 Структура бандла +`service_tui.spec` собирает standalone-бандл (onedir — старт быстрее, чем +onefile с распаковкой во временную директорию на каждый запуск). Сборка — +через just-рецепт `just host::package-tui` (не `service-build`, см. +README «Запуск»): запускает PyInstaller, докладывает `firmware//*_hab.bin` +из `BUILD_DIR`, переименовывает результат в `dist/service-tui-vX.Y.Z-/` +(версия — из `pyproject.toml`). -```bash +Целевая структура бандла: + +``` service-tui-vX.Y.Z-/ ├── service_tui[.exe] ├── _internal/ │ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data -│ └── ... ← рантайм PyInstaller, libusbsio (из Analysis) +│ └── ... ← рантайм PyInstaller, libusbsio ├── firmware/ -│ └── /firmware_test_hab.bin ← копируется post-build -└── custom_binaries/ ← пустая, для оператора +│ └── /firmware_test_hab.bin +└── custom_binaries/ ← пустая, создаётся оператором/автоматически ``` -Два разных механизма наполнения — не взаимозаменяемы: +Каждый модуль, которому нужен путь к данным, сам решает dev vs frozen через +`getattr(sys, "frozen", False)` — единообразный паттерн по всему `app/`: -- **`_internal/data/`** — через `datas` в `service_tui.spec` - (`collect_data_files("spsdk")` + `tools/host/dcd/*.bin`). Резолвится в - рантайме через `sys._MEIPASS` (для onedir `_MEIPASS` == `_internal/`). -- **`firmware/`** — PyInstaller `datas` физически не может положить файл - вне `_internal/`, поэтому это отдельный **post-build copy-шаг** в - `just host::package-tui` (не часть `.spec`), копирующий `build//*_hab.bin` - в бандл. Резолвится в рантайме через `Path(sys.executable).resolve().parent` - (сиблинг exe, не `_MEIPASS`) — сознательный выбор: HAB-образы должны быть - легко заменяемы без пересборки бандла. -- **`custom_binaries/`** — создаётся дважды, независимо: приложением само - при первом запуске (`flasher.py::_resolve_custom_binaries_dir()`, - `mkdir(exist_ok=True)`) и заодно явно в `package-tui` (`mkdir -p` перед - финальным переименованием) — избыточно, но безвредно, бандл выглядит - «полным» ещё до первого запуска. +| Функция | Dev | Frozen | +| --- | --- | --- | +| `flash_backend._host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS/data` | +| `flash_backend.firmware_hab_path()` | `$BUILD_DIR//*_hab.bin` (репо `build/`) | `/firmware//*_hab.bin` | +| `flasher._resolve_custom_binaries_dir()` | `tools/production/custom_binaries/` | `/custom_binaries/` (override — `SERVICE_CUSTOM_BINARIES_DIR`) | +| `waiting._read_app_version()` | `tools/production/pyproject.toml` | тот же путь — `pyproject.toml` кладётся в `datas` спека (нужен для парсинга версии во frozen) | +| `main._setup_logging()` | рядом с `main.py` | рядом с исполняемым файлом (`sys.executable.parent`) | -### 14.2 Двухрежимный резолв путей (`flash_backend.py`) +`sys.executable` (не `sys._MEIPASS`) — единственный путь, одинаково +работающий и для onefile, и для onedir; `_MEIPASS` для onefile указывает на +временную распаковку, которая исчезает после выхода из процесса. -Все функции, отдающие пути к data-файлам, различают dev/frozen: +Нативный HID-транспорт (`libusbsio`, следствие Р7 — spsdk вместо pyusb) +означает, что `libusb-1.0.*`/Zadig в бандле **не нужны** ни на Windows, ни +на macOS — детект BootROM SDP и Flashloader работает из коробки. -| Функция | Dev | Frozen | -| -------------------------------------------------------------------- | -------------------------------- | ------------------------------------------- | -| `firmware_hab_path()` | `BUILD_DIR`/`build//` | `sys.executable.parent / "firmware"` | -| `_host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS / "data"` | -| `flashloader_bin_path()` / `real_dcd_bin_path()` / `fcb_blob_path()` | производные от `_host_dcd_dir()` | | -| `_resolve_custom_binaries_dir()` (`flasher.py`) | рядом с `main.py` | `sys.executable.parent / "custom_binaries"` | +> **Расхождение spec/факт:** закоммиченный `service_tui.spec` объявляет в +> `datas` только `('../shared', 'shared')` — без `dcd/*.bin`, +> `pyproject.toml` или `spsdk`-данных. Тем не менее уже собранные релизные +> бандлы в `tools/production/dist/service-tui-v0.2.0-{macos,windows}/` +> фактически содержат `_internal/data/{dcd.bin,*_fdcb.bin,ivt_flashloader.bin}`, +> `_internal/pyproject.toml` и `_internal/spsdk/` — то есть сборки, тестировавшиеся +> на железе (Фаза 5, гейт по macOS/Windows), были собраны с более полным +> набором `datas`, чем то, что сейчас лежит в репозитории. `service_tui.spec` +> нужно актуализировать (`collect_data_files("spsdk")`, `tools/host/dcd/*.bin` +> → `data/`, `pyproject.toml`) до следующей сборки релиза — см. «Известные +> открытые вопросы». -`main.py::_setup_logging()` и `.env`-загрузка тоже различают режимы: -лог-файл во frozen пишется рядом с exe (не внутрь `_internal/`); `.env` во -frozen не подгружается вообще (frozen-сборка работает на fallback-константах -в коде, не полагаясь на файл, которого в бандле нет). +--- -### 14.3 `service_tui.spec` — сборка (важные детали) +## 15. Логирование (Р11/Р12, Фаза 4b) -- **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) — известный источник проблем с - загрузкой. +`main.py::_setup_logging()`: -### 14.4 Известные грабли упаковки +- Root-логгер по умолчанию — `INFO` (не `DEBUG`); файл — + `service_tui.log` рядом с исполняемым файлом (или `$SERVICE_LOG_DIR`). +- `SERVICE_LOG_LEVEL=DEBUG` включает полный DEBUG, **включая** портянки + `spsdk`/`libusbsio` (сырые HID TX/RX-пакеты — ~135 строк на одну + прошивку). +- При любом другом значении (или отсутствии переменной) логгеры + `spsdk`, `libusbsio`, `libusbsio.hidapi.dev`, + `spsdk.mboot.protocol.bulk_protocol` принудительно приглушены до + `WARNING`, независимо от уровня root — иначе диагностика `app.*` + тонет в чужом протоколе. +- `textual` отдельно всегда на `WARNING`. -- **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`. +`FlashScreen._on_progress()` (Р11) троттлит **только** запись в +`#flash-log` для фазы `write`: событие логируется раз на каждые 10% +(`progress.percent // 10`), а не на каждый пакет `spsdk` (~135 → +~10 строк). Прогресс-бар при этом обновляется на **каждом** событии — +плавность не теряется, троттлинг влияет только на текстовый лог. --- ## Известные открытые вопросы -- **Release-сборка firmware нестабильна** : работает только с оптимизацией уровня O1 +- **Документация — Фаза 6 (текущая).** Инженерные фазы 0–5 (backend на spsdk, + обработка обрыва USB, троттлинг логов, упаковка PyInstaller) закрыты в + коде; `docs/DEV_ARCH.md`/`README.md` актуализированы этой правкой. Осталось + по `RELEASE_ROADMAP.md` §Фаза 6: `CHANGELOG.md` (не заведён), grep-зачистка + устаревших docstring-упоминаний `flash_usb.py`/`subprocess` в + `app/flash.py` (комментарий `_check_sdp_present`) и `flasher.py` + (docstring модуля упоминает Фазу 2 буквально, что нормально как история + провенанса, но стоит перепроверить при следующей правке этих файлов). +- **`service_tui.spec` не актуализирован под реальные релизные сборки** — + см. §14. Нужно добавить `datas` (`dcd/*.bin`, `pyproject.toml`, + `collect_data_files("spsdk")`) до следующей упаковки релиза. +- **`pyusb` в `pyproject.toml` — мёртвая зависимость.** Р7 перевёл детект + SDP/CDC на `spsdk`/`serial.tools.list_ports`; ни один модуль `app/` больше + не импортирует `usb`/`pyusb`. Кандидат на удаление при следующей + grep-зачистке (Фаза 6). +- **Release-сборка firmware нестабильна** (медленное мигание — подозрение на + проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно + форсирует Debug через `FIRMWARE_BUILD_TYPE`. - **`tools/shared/m5_agent.py`** — сознательно не делался: pytest HIL-окружение и TUI используют независимые M5-клиенты, признано правильным - архитектурным решением, а не техдолгом. (Устаревшая `just host::service-build` - ссылается на несуществующий `tools/shared/` через `--add-data` — рецепт, - скорее всего, нерабочий, кандидат на удаление в пользу `package-tui`.) + архитектурным решением, а не техдолгом. - Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и «копирование UID с экрана» — отложены, не начаты. +- **POST-1 (циклический прогон неинтерактивных тестов на DiagScreen)** — + сознательно отложен на пост-релиз, вне `MONOLITH_APP_PLAN.md` (см. + `RELEASE_ROADMAP.md`). - **Массовое программирование** — решено НЕ делать авто-прошивку по факту детекта SDP (см. §8.2); ограничились «липким» `FlashPreset`. Если в будущем понадобится полный батч-режим — потребуется отдельный предохранитель (задержка с отменой перед стартом), т.к. в SDP-режиме плату нельзя - идентифицировать по UID. + идентифицировать по UID. Связанное ограничение v1 (О3) — предполагается + ровно одна плата на столе одновременно (см. README). - **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили не полагаться на него вообще, для кастомных бинарей FCB всегда пишется - явно (§8.4). Остаётся не до конца понятым, работает ли - `configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос - снят с повестки архитектурным решением, а не исследован до конца. + явно (параметр `fcb_path` в `flash_backend.flash()`, см. §8.4). Остаётся не + до конца понятым, работает ли `configure-memory 0xF000000F` для этих + чипов корректно в принципе — вопрос снят с повестки архитектурным + решением, а не исследован до конца. + \ No newline at end of file diff --git a/tools/production/docs/RELEASE_ROADMAP.md b/tools/production/docs/RELEASE_ROADMAP.md new file mode 100644 index 0000000..3ccee56 --- /dev/null +++ b/tools/production/docs/RELEASE_ROADMAP.md @@ -0,0 +1,467 @@ +# service-tui — Roadmap до релиза v1 (фазы 4a → 4b → 5 → 6) + +> Продолжение `MONOLITH_APP_PLAN.md` (V4). Фазы 0–3 закрыты, Фаза 4 +> закрыта частично — деструктивные гейты на железе вскрыли пробел в +> типизации обрыва USB (см. Фаза 4a). Этот документ — дорожная карта +> оставшегося пути до релиза. +> +> Ветка: `feature-tui-monolith` от `dev`. Каждая фаза = коммит(ы) с +> зелёным гейтом; откат любой фазы не ломает предыдущие. + +--- + +## Статус на входе + +| Фаза | Статус | +| --- | --- | +| 0 — Спайк / де-риск | ✅ Закрыта (⚠В1/⚠В2/Р7/О1 сняты, golden-тест byte-exact) | +| 1 — Backend-модуль | ✅ Закрыта (`flash_backend.py`, `usb_ports.py`, 41 тест) | +| 2 — Пересадка `Flasher` | ✅ Закрыта (async-мост, гейт на железе) | +| 3 — Custom HAB in-process | ✅ Закрыта (де-факто вместе с фазой 1, подтверждена на железе) | +| 4 — Обработка отвала USB | ⚠️ **Частично** — see 4a | +| 4a — Добор типизации обрыва | ⏳ **Следующая** | +| 4b — Сокращение логов | ⏳ | +| 5 — Упаковка PyInstaller | ⏳ | +| 6 — Документация / релиз | ⏳ | + +### Почему Фаза 4 не закрыта + +Деструктивные гейты на железе (macOS) показали: **выдёргивание USB +проявляется тремя разными способами**, а код Фазы 4 корректно +типизирует только один. + +| Проявление в spsdk | Что реально прилетает | Фаза 4 (сейчас) | Пользователь видит | +| --- | --- | --- | --- | +| write-фаза, обрыв при записи | `SPSDKConnectionError` | обёрнут только вокруг `with SDP`/`with McuBoot`, но реальный путь `write_memory` прошёл мимо | «Непредвиденная ошибка» (safety net) | +| read-фаза после write | `SPSDKTimeoutError` (потомок `SPSDKError`, **не** `SPSDKConnectionError`) | не ловится обёрткой обрыва | «Непредвиденная ошибка» (safety net) | +| chip erase по таймауту | `flash_erase_all()` вернул `False` (без исключения) | ветка `if not ok: raise FlashBackendError(...)` | «flash_erase_all вернул False» | + +План Фазы 4 буквально требовал «`McuBootConnectionError`/**таймауты** → +`FlashBackendError(connection_lost=True)`». Таймаут (`SPSDKTimeoutError`) +и `False`-по-таймауту не были покрыты — значит Гейт 4 по букве не пройден. +Это добор, а не новая работа сверх плана. + +> **Важно (UX-надёжность уже работает):** safety net (`except Exception` +> в `flasher.py`) во всех трёх случаях корректно вернул `ok=False`, +> разблокировал кнопки, оставил приложение живым. Проблема +> исключительно в *формулировке* сообщения, не в устойчивости. + +--- + +## Принятые решения этого этапа + +| ID | Решение | +| --- | --- | +| **Р10** | Erase-таймаут (`False` без исключения) переклассифицируется в `ConnectionLostError` **вариантом B**: после `False` выполнить быстрый `detect_sdp()` — если устройство пропало с шины, это обрыв; если на месте — честная ошибка операции. | +| **Р11** | Троттлинг `write`-событий в `#flash-log` — каждые **10%** (≈10 строк вместо ~135). Прогресс-бар обновляется на **каждом** событии (плавность не теряется), в лог пишется прореженно. | +| **Р12** | Логирование: root/`spsdk`/`libusbsio` понижаются до `WARNING` по умолчанию; полный DEBUG доступен через env-переключатель (диагностика не теряется совсем). | +| **О2 (закрыт)** | Состав `firmware/` в релизе — только `firmware_test`; тип сборки (Debug/Release) управляется через `.env` (`FIRMWARE_BUILD_TYPE`), механизм уже реализован в `flasher.py`. | +| **POST-1** | Циклический прогон неинтерактивных тестов (SDRAM/NOR/OPTO/CAN) на `DiagScreen` — **отложен на пост-релиз**, вне `MONOLITH_APP_PLAN.md`. Зафиксирован, чтобы не потерять. | +| **Предложение 2** | Кнопка «Выйти из приложения» на `WaitingScreen` — принято, включается в Фазу 5 (UI-полировка перед упаковкой). | + +--- + +## Фаза 4a — Добор: корректная типизация обрыва USB + +**Цель:** все три проявления обрыва USB дают пользователю единое +понятное сообщение «Соединение с платой потеряно», а не «Непредвиденная +ошибка» / «flash_erase_all вернул False». + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/flash_backend.py` | правки: расширить обёртку обрыва на `SPSDKTimeoutError`; реализовать вариант B для erase | +| `tests/test_flash_backend.py` | правки: тесты на `SPSDKTimeoutError`-путь и на erase-переклассификацию | + +### Содержание + +1. **Обёртка обрыва расширяется** с `SPSDKConnectionError` на общий + родительский случай, покрывающий и `SPSDKTimeoutError`. Оба — + потомки `SPSDKError`, но `SPSDKTimeoutError` **не** наследует + `SPSDKConnectionError`, поэтому текущий `except SPSDKConnectionError` + его пропускает. Ловим оба явным кортежем + `(SPSDKConnectionError, SPSDKTimeoutError)` в трёх местах: + `load_flashloader`, `flash` (основная + ram_only ветки), `erase_chip`. + +2. **Вариант B для erase** (Р10): в `erase_chip` (и в `flash`, где + команды возвращают `False` по тем же причинам) — при `ok == False` + выполнить быстрый `detect_sdp()`; если устройство исчезло с шины → + `ConnectionLostError`, иначе → обычный `FlashBackendError` с прежним + текстом. Проверка `detect_sdp()` добавляется **только в error-путь**, + на happy path не влияет. + +3. **`_format_error_message` в `flasher.py` не трогается** — он уже + корректно даёт префикс «Соединение с платой потеряно» для любого + `connection_lost=True`. Достаточно, чтобы backend правильно поднял + `ConnectionLostError`. + +### Гейт 4a + +- [ ] Юнит-тест: `SPSDKTimeoutError` из `write_memory` → `ConnectionLostError` + (мок). +- [ ] Юнит-тест: `flash_erase_all` → `False` + `detect_sdp()==False` → + `ConnectionLostError`; `False` + `detect_sdp()==True` → + обычный `FlashBackendError` (мок). +- [ ] Существующие 41 тест зелёные (регрессии нет). +- [ ] **Железо (повтор деструктивных сценариев):** + - [ ] Выдернуть USB во время `write-memory` → в `#flash-log` + «Соединение с платой потеряно», не «Непредвиденная ошибка». + - [ ] Выдернуть во время chip erase → то же сообщение. + - [ ] Повторная вставка → прошивка успешна (порт не «занят»). +- [ ] macOS + Windows. + +--- + +## Фаза 4b — Сокращение логов + +**Цель:** лог-файл прошивки уменьшается на порядок; `#flash-log` в TUI +показывает осмысленный прогресс, а не ~135 однотипных строк. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/main.py` | правки: уровни логгеров (Р12) + env-переключатель DEBUG | +| `app/screens/flash.py` | правки: троттлинг `write`-событий в `_on_progress` (Р11) | + +### Содержание + +1. **Уровни логгеров (Р12):** root по умолчанию `INFO`; `spsdk`, + `libusbsio`, `libusbsio.hidapi.dev`, `spsdk.mboot.protocol.bulk_protocol` + → `WARNING` (именно они дают портянки HID-байтов). Полный DEBUG + включается через переменную окружения (например + `SERVICE_LOG_LEVEL=DEBUG`) — точное имя согласовать, не изобретаю. + +2. **Троттлинг `#flash-log` (Р11):** в `flash.py::_on_progress` + прогресс-бар обновляется всегда, а `write_line` в лог для фазы + `write` — только при пересечении 10%-границы (0/10/20/…/100). + Остальные фазы (`configure`/`erase`/`fcb`/`reset`/`done`/`error`/ + `hab_build`) логируются как есть — их немного. + +### Гейт 4b + +- [ ] Лог-файл одной прошивки на порядок короче; DEBUG-портянок + spsdk/libusbsio нет при дефолтном уровне. +- [ ] `SERVICE_LOG_LEVEL=DEBUG` (или согласованное имя) возвращает + полный DEBUG — диагностика доступна. +- [ ] `#flash-log`: ~10 строк прогресса записи вместо ~135, бар + по-прежнему плавный. +- [ ] Регрессия: прошивка/erase/диагностика на железе работают. + +--- + +## Фаза 5 — Упаковка PyInstaller + UI-полировка + +**Цель:** один исполняемый бандл на Windows и macOS, проходящий полный +полевой цикл на чистой машине без Zadig/сети/Python. Плюс кнопка +«Выйти» на `WaitingScreen`. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `tools/production/service_tui.spec` | новый — PyInstaller spec | +| `app/screens/waiting.py` | правки: кнопка «Выйти из приложения» (Предложение 2) | +| just-рецепт | новый — имя задачи согласовать, **не изобретаю** | +| `app/app.tcss` | правки при необходимости — стиль кнопки Quit на Waiting | + +### Содержание spec (из плана V4, §Фаза 5) + +- `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` при необходимости — + документированный NXP механизм для frozen); +- **`collect_dynamic_libs("libusbsio")`** — нативный HID-транспорт + (следствие Р7); `libusb-1.0.*` в бандле **отсутствует**; +- `datas`: `tools/host/dcd/{dcd.bin, w25q128_fdcb.bin, w25q512_fdcb.bin, + ivt_flashloader.bin}` → `data/`; +- `datas`: `firmware//firmware_test_hab.bin` (Type из `.env`, О2); +- `datas`: `pyproject.toml` (для `_read_app_version` во frozen); +- onedir (не onefile — onefile замедляет старт распаковкой); +- резолвер путей backend'а уже готов: frozen → `sys.executable.parent` + (`firmware_hab_path`, `_resolve_custom_binaries_dir`). + +Целевая структура бандла: + +``` +service-tui-vX.Y.Z-/ +├── service_tui[.exe] +├── _internal/ +│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data +│ └── ... ← рантайм PyInstaller, libusbsio +├── firmware/ +│ └── /firmware_test_hab.bin +└── custom_binaries/ ← пустая +``` + +### UI-полировка (Предложение 2) + +Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично +`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`). + +### Гейт 5 (Windows + macOS) + +- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный + полевой цикл — детект SDP → firmware_test → диагностика → + custom (W25Q128 и W25Q512) → chip erase. +- [ ] То же на macOS. +- [ ] Версия на `WaitingScreen` корректна во frozen. +- [ ] Порты резолвятся при перетыкании в другой физический USB-порт + (проверка Р8 на обеих ОС). +- [ ] Кнопка «Выйти» на `WaitingScreen` работает. +- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны — + подтверждено О1) виден во frozen-бандле. + +--- + +## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз + +**Цель:** синхронизировать документацию с реальностью монолита, +провести отложенную зачистку комментариев/grep, собрать релизный +артефакт из тега. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) | +| `CHANGELOG.md` | правки | +| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap | +| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) | +| `HOW_TO_FLASH.md` | правки | +| `tools/production/README.md` | правки | +| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | + +### Содержание + +1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход + по всему коду — актуализировать docstring-провенансы («прямой порт + flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита. + Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто + в `app/`) — либо достигается, либо остаётся осознанно как + документация происхождения (решение по каждому вхождению). +2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess), + нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8), + нативная обработка отвала USB, упаковка одним exe. +3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план + RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1). +4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты), + `flash_backend.py` — production-TUI; независимые реализации (Р2). +5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk. +6. **Ограничение «одна плата на столе»** (О3) — в README. +7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как + запланированную пост-релизную фичу. +8. Тег релиза = версия из `pyproject.toml`. + +### Гейт 6 + +- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5). +- [ ] `just host::flash*`, `incoming`, `production` работают как раньше — + регрессия dev-пути. +- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на + релизном бинаре. +- [ ] POST-1 зафиксирован в бэклоге, не потерян. + +--- + +## Сводная последовательность и зависимости + +``` +4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1 +│ │ │ │ +│ │ │ └── доки, зачистка, тег, регрессия dev-пути +│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting +│ └── уровни логов + троттлинг #flash-log +└── типизация обрыва (SPSDKTimeoutError + erase вариант B) + +Блокеры перед фазами: + 4a: нет — старт сразу + 4b: нет — после 4a + 5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG + 6: все гейты 4a/4b/5 зелёные на железе +``` + +## Открытые мелочи (согласовать по ходу, не блокируют старт 4a) + +| Вопрос | Когда нужен | Предложение | +| --- | --- | --- | +| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) | +| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю | +| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-` (из плана) | + +--- + +## Риски этого этапа + +| Риск | Фаза | Митигация | +| --- | --- | --- | +| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает | +| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель | +| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле | +| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` | +| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет | + +--- + +# Приложение: работа в новом треде + +Этот roadmap рассчитан на продолжение в **новом чате без контекста** +предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы +новый тред стартовал без потерь. + +## A. Какой набор правил к чему применяется + +Проектные правила «Role & Hardware Context» (senior embedded C, i.MX +RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`, +CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`). + +**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в +`tools/production`. Поэтому: + +| Правило | Применимо к Python-работе roadmap? | +| --- | --- | +| Unified diffs, не полные переписывания | ✅ Да | +| «Какой файл / какая функция затронуты» — первым | ✅ Да | +| ASK при неоднозначности/противоречии | ✅ Да | +| Не изобретать just-таски / пути / структуру | ✅ Да | +| Проверять существующие файлы перед правкой | ✅ Да | +| No malloc/free в драйверах и ISR | ❌ C-специфично | +| NXP SDK HAL вместо raw-регистров | ❌ C-специфично | +| Doxygen на public API | ❌ (Python — docstrings, уже используются) | +| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) | +| CMake target_compile_options | ❌ Неприменимо | + +Когда/если roadmap коснётся C-части — C-правила снова в силе. + +## B. Первый вопрос на старте нового треда (не потерять) + +**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip` +предлагается обернуть в `try/except`, и **любую ошибку самой проверки** +(не только «устройство отсутствует») трактовать как обрыв — потому что +проверка и так выполняется только после уже случившегося сбоя, шина +нестабильна, и «не смог проверить» практически всегда означает «платы +нет». Требуется явное подтверждение этой трактовки перед написанием +кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный +`FlashBackendError`.) + +## C. Файлы, которые нужно предоставить — по фазам + +Пути относительно `tools/production/`, если не указано иное. Пометка +**[есть в этом треде]** — файл уже фигурировал и его актуальная версия +известна; в новом треде его всё равно нужно приложить заново. + +### Фаза 4a — типизация обрыва + +| Файл | Зачем | +| --- | --- | +| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B | +| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию | +| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) | +| `app/models.py` | контекст: `FlashProgress` | + +### Фаза 4b — логи + +| Файл | Зачем | +| --- | --- | +| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) | +| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) | +| `.env` / `.env.example` (`tools/production/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными | + +### Фаза 5 — упаковка PyInstaller + UI + +| Файл | Зачем | +| --- | --- | +| `pyproject.toml` (`tools/production/`) | зависимости, версия, `requires-python` — база для spec | +| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта | +| `app/main.py` | entry point для PyInstaller | +| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen | +| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit | +| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) | +| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла | +| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) | +| `project_tree.txt` или `ls -R tools/production` | реальная структура пакета `app/` для spec | +| существующий `.spec`, если уже есть | не изобретать заново | + +### Фаза 6 — документация и релиз + +| Файл | Зачем | +| --- | --- | +| `CHANGELOG.md` | дописать секцию монолита | +| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap | +| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) | +| `HOW_TO_FLASH.md` | актуализировать под TUI-backend | +| `tools/production/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI | +| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | +| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) | +| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) | + +## D. Полный список актуальных файлов монолита (снимок на входе) + +Чтобы в новом треде можно было приложить всё разом, если удобнее не +дробить по фазам. Актуальные (пост-Фаза-4) версии: + +``` +tools/production/ +├── pyproject.toml +├── app/ +│ ├── __init__.py +│ ├── app.py +│ ├── app.tcss +│ ├── main.py (точка входа — фактически в tools/production/main.py, см. pyproject scripts) +│ ├── models.py +│ ├── flasher.py ← Фаза 2/4, актуальная версия +│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест) +│ ├── usb_ports.py ← Фаза 1 +│ ├── firmware_client.py +│ ├── m5_client.py +│ ├── orchestrator.py +│ ├── boot_art.py +│ ├── widgets.py (или widgets/) +│ └── screens/ +│ ├── __init__.py +│ ├── waiting.py +│ ├── flash.py ← Фаза 4 (правлены комментарии) +│ ├── post_flash.py +│ ├── connection_watcher.py +│ └── diag/ +│ ├── __init__.py +│ ├── confirm_panel.py +│ ├── results.py +│ └── test_list.py +├── tests/ +│ ├── __init__.py +│ └── test_flash_backend.py ← 41 тест +├── spike/ (Фаза 0, в релиз не идёт) +│ ├── spike_hab.py +│ ├── spike_flash.py +│ └── spike_readback.py (диагностика Гейта 3, на будущее) +└── custom_binaries/ (пустая, для оператора) + +tools/host/ (dev-CLI, Р2 — НЕ трогается) +├── flash_usb.py +└── dcd/ + ├── ivt_flashloader.bin + ├── dcd.bin + ├── w25q128_fdcb.bin + └── w25q512_fdcb.bin +``` + +> Примечание: `main.py` в `pyproject.toml` прописан как +> `service-tui = "main:main"` — точка входа лежит в +> `tools/production/main.py` (не в `app/`), а `app/app.py` содержит +> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5. + +## E. Что уже решено и не пересматривается (сводка для нового треда) + +- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже). +- **Р10** — erase-таймаут → вариант B (detect_sdp после False). +- **Р11** — троттлинг лога 10%. +- **Р12** — уровни логов + env DEBUG. +- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны. +- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`. +- **О3** — одна плата на столе, ограничение v1. +- **POST-1** — циклический прогон тестов, после релиза. +- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py` + меняются только там, где явно указано в roadmap. +- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе. +- Порядок ревью: один файл за раз, полные файлы для новых/целиком + переписываемых, unified diff для точечных правок. diff --git a/tools/production/tests/test_flash_backend.py b/tools/production/tests/test_flash_backend.py index f9674f8..820c536 100644 --- a/tools/production/tests/test_flash_backend.py +++ b/tools/production/tests/test_flash_backend.py @@ -329,7 +329,6 @@ def test_flash_write_memory_fails(monkeypatch, events, tmp_path): with pytest.raises(fb.FlashBackendError, match="write_memory"): fb.flash(hab_bin, progress_cb=_collector(events)) - # reset/done не должны наступить после ошибки записи assert "reset" not in _phases(events) assert "done" not in _phases(events)