Merge branch 'feature-tui-monolith' into dev
# Conflicts: # tools/production/README.md # tools/production/docs/DEV_ARCH.md
This commit is contained in:
commit
b4c664fe12
39 changed files with 1913 additions and 1501 deletions
189
CHANGELOG.md
189
CHANGELOG.md
|
|
@ -55,86 +55,153 @@
|
||||||
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
|
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
|
||||||
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
||||||
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
||||||
- Следить за развитием BSP: MQS, RGB, bootloader или `tft_app`.
|
- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app` — обе директории всё ещё не заведены.
|
||||||
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
|
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
|
||||||
|
- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA.
|
||||||
|
|
||||||
## [Не выпущено] — service-tui: кастомная прошивка нестандартной памяти (после слияния `feature-tui-python`)
|
## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
|
||||||
|
|
||||||
Диапазон: `<заполнить после merge>..<текущий HEAD>`
|
Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` + незакоммиченные изменения рабочего дерева (документация)
|
||||||
Сравнение: `<заполнить после merge>`
|
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/1801f1beb959d610d31ee3dcd1f91046953117d4...22c40779ef0ec9911031d7a5272c4611b596d3e8>
|
||||||
|
|
||||||
> Изменения внесены **поверх** слияния `feature-tui-python → dev` — базовая
|
> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`).
|
||||||
> архитектура `service-tui` (экраны, USB/M5-клиенты, оркестратор) приходит
|
> **Полностью заменяет предыдущую версию этой записи**: прошивка сторонних
|
||||||
> самим merge-коммитом; здесь только то, что было доработано отдельно после.
|
> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` +
|
||||||
|
> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор
|
||||||
|
> заменена прямыми вызовами `spsdk` Python API — ничего из старой записи
|
||||||
|
> больше не описывает текущий код.
|
||||||
|
|
||||||
### Кратко
|
### Кратко
|
||||||
|
|
||||||
- `service-tui` теперь умеет прошивать сторонние/легаси бинарники (платы
|
- Прошивка в `service-tui` переведена с subprocess-обёртки над
|
||||||
с W25Q256/512 вместо штатного W25Q128) через USB SDP, с явной записью
|
`nxpimage`/`sdphost`/`blhost` на прямые вызовы `spsdk` Python API
|
||||||
FCB вместо ненадёжного для таких чипов auto-config Flashloader.
|
(`McuBoot`/`SDP`/`HabImage`) — `app/flash_backend.py`, провалидировано
|
||||||
- Выбор оператора на `FlashScreen` (файл/память/DCD) запоминается на весь
|
byte-exact на живом железе (macOS + Windows). `tools/host/flash_usb.py`
|
||||||
запуск TUI — ускоряет прошивку партии одинаковых плат.
|
остаётся отдельным dev-CLI для `just host::flash*`, TUI его больше не
|
||||||
- Документация (`tools/production/README.md`, `tools/production/DEV_ARCH.md`,
|
вызывает ни субпроцессом, ни как библиотеку.
|
||||||
`tools/host/README.md`, `docs/HOW_TO_FLASH.md`, `docs/DEV_ARCH.md`)
|
- Обрыв 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`:
|
- `tools/production/app/flash_backend.py` — синхронное ядро прошивки на
|
||||||
буквальная запись 512-байтного FCB-блоба (`write-memory 0x60000000`)
|
spsdk: `detect_sdp`/`detect_cdc`, `load_flashloader`, `flash`,
|
||||||
вместо magic option word `0xF000000F`. Штатный `--firmware`-путь
|
`erase_chip`, `build_custom_hab` (`HabImage` вместо `nxpimage` CLI),
|
||||||
(`firmware_test`/`bootloader`/`app`) не тронут — работает как раньше.
|
`write_fcb_explicit`/`write_fcb_auto`. Zero Textual/asyncio импортов,
|
||||||
- `tools/production/app/models.py` — `FcbVariant` (`W25Q128` покрывает и
|
тестируется без event loop.
|
||||||
W25Q64, `W25Q512` — и W25Q256) и `FlashPreset` (липкий выбор оператора).
|
- `tools/production/app/usb_ports.py` — `resolve_serial_port()` по VID:PID
|
||||||
- `tools/production/app/flasher.py` — `_build_custom_hab()`: сборка
|
(имя порта не переносимо между перевтыкиваниями).
|
||||||
HAB-образа на лету через `nxpimage hab export` из «сырого» бинарника
|
- Иерархия `FlashBackendError`/`ConnectionLostError`/`DeviceNotFoundError`/
|
||||||
(без FCB/IVT/DCD) в `custom_binaries/`, с опциональным `DCDFilePath`;
|
`FlashLoaderTimeoutError`/`HabBuildError` с полем `connection_lost` —
|
||||||
стриминг вывода `nxpimage` в UI-лог, а не только в `logger.debug`.
|
различает физический обрыв USB от логической ошибки прошивки без
|
||||||
`list_custom_binaries()` + `SERVICE_CUSTOM_BINARIES_DIR` — резолв
|
парсинга текста сообщения.
|
||||||
директории кастомных бинарей (внешняя, не пакуется в PyInstaller).
|
- `tools/production/tests/test_flash_backend.py` — вырос до 45 unit-тестов
|
||||||
- `tools/production/app/screens/flash.py` — `Select` по `custom_binaries/`,
|
backend'а, включая обе ветки обрыва USB (`SPSDKTimeoutError`,
|
||||||
`Select` по `FcbVariant`, `Switch` DCD вместо свободного текстового
|
`False`-по-таймауту + вариант B через `detect_sdp()`) и golden-тест
|
||||||
`Input`; предзаполнение из `FlashPreset` при создании экрана.
|
byte-exact сборки HAB.
|
||||||
- `tools/production/app/app.py` — `ServiceApp._last_flash_preset`,
|
- Кнопка «✕ Выйти из приложения» на `WaitingScreen`.
|
||||||
прокидывается в новый `FlashScreen` при каждом `DeviceDetected(FLASHING)`.
|
- `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`
|
- `tools/production/app/flasher.py` — переведён с subprocess
|
||||||
`flash-erase-all` увеличен до `-t 200000` (W25Q512 стирается заметно
|
(`flash_usb.py` через `uv run`) на `asyncio.to_thread`-обёртку над
|
||||||
дольше W25Q128, дефолтного таймаута не хватало). `flash-erase-region`
|
`flash_backend.py`; сборка кастомного HAB — через `HabImage` в отдельном
|
||||||
(обычная прошивка) не тронут — там стирается пара секторов, масштаб иной.
|
потоке, а не subprocess `nxpimage`.
|
||||||
- `tools/production/app/app.tcss` — `#flash-target-group` ограничен по
|
- `tools/production/app/main.py` — логирование: root по умолчанию `INFO`
|
||||||
высоте (`max-height: 18`, свой скролл), `#flash-log` защищён
|
(было `DEBUG`), `spsdk`/`libusbsio` принудительно приглушены до
|
||||||
`min-height: 6` — разросшаяся custom-группа больше не сжимает лог
|
`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/
|
- `tools/production/README.md`/`tools/production/docs/DEV_ARCH.md` —
|
||||||
Switch), новый workflow «Прошивка стороннего бинарника», `SERVICE_CUSTOM_BINARIES_DIR`
|
полностью пересмотрены под факт: убраны все следы subprocess/`nxpimage`/
|
||||||
в примере `.env`.
|
`flash_usb.py` из описания архитектуры прошивки; добавлены §6.2
|
||||||
- `tools/production/DEV_ARCH.md` — новый §8 (конвейер кастомной прошивки,
|
(обработка обрыва USB), §14 (PyInstaller/frozen-резолв путей), §15
|
||||||
`FlashPreset`, явная запись FCB, обоснование отказа от auto-config для
|
(логирование); зафиксирован разрыв между закоммиченным
|
||||||
W25Q256/512 и от полноценного авто-батч-режима прошивки).
|
`service_tui.spec` (`datas` только `../shared`) и фактическим
|
||||||
- `docs/HOW_TO_FLASH.md` — §1.5, сноска в сравнительной таблице способов
|
содержимым уже собранных релизных бандлов в `dist/`.
|
||||||
прошивки (FCB «не нужен» верно только для W25Q128).
|
- `docs/testing/PROTOCOL.md` — версия `0.1.0`→`0.1.2`, добавлена команда
|
||||||
- `docs/DEV_ARCH.md` — `tools/production/` добавлен в дерево структуры
|
`get_version` и события `test_list`/`uid_response`/`version_response`,
|
||||||
репозитория (отсутствовал ранее).
|
матрица тестов исправлена (убраны никогда не существовавшие `uart_ttl`/
|
||||||
- `tools/host/README.md` — актуализирован статус `dcd/*.bin` (`w25q512_fdcb.bin`
|
`uart_iso`, добавлен реальный `mqs`), поток Display дополнен шагами
|
||||||
теперь используется), указатель на `service-tui` как способ прошивки
|
ротации (`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_<n>` без `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 не проверялся напрямую — решение
|
- `service_tui.spec` не включает `datas` для `spsdk`/`dcd/*.bin`/
|
||||||
писать FCB явно снимает вопрос архитектурно, но не подтверждает и не
|
`pyproject.toml`, хотя уже собранные alpha-бандлы их содержат — спек
|
||||||
опровергает надёжность auto-config как таковую.
|
нужно синхронизировать перед следующей сборкой релиза.
|
||||||
- Полноценный режим массового программирования (авто-прошивка по факту
|
- `pyusb` в `pyproject.toml` — мёртвая зависимость (Р7 перевёл детект на
|
||||||
детекта USB, без подтверждения оператора) рассмотрен и отклонён — в
|
`spsdk`/`serial.tools.list_ports`), кандидат на удаление.
|
||||||
SDP/Flashloader-режиме нет способа прочитать UID платы для идентификации.
|
- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили
|
||||||
- Standalone-упаковка (`PyInstaller`) для этого функционала ещё не
|
не полагаться на него вообще, FCB для кастомных бинарей всегда пишется
|
||||||
реализована — см. `tools/production/RELEASE_PLAN.md`.
|
явно.
|
||||||
|
- Массовое программирование (авто-прошивка по факту детекта SDP, без
|
||||||
|
подтверждения оператора) рассмотрено и отклонено — в SDP/Flashloader-режиме
|
||||||
|
нет способа прочитать UID платы для идентификации.
|
||||||
|
|
||||||
## [2026-06-29] — Этапы 6г–7: MQS, HIL pytest firmware_test, Provisioning
|
## [2026-06-29] — Этапы 6г–7: MQS, HIL pytest firmware_test, Provisioning
|
||||||
|
|
||||||
|
|
|
||||||
202
FIRST_RELEASE_PLAN.md
Normal file
202
FIRST_RELEASE_PLAN.md
Normal file
|
|
@ -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/<Type>/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 |
|
||||||
19
README.md
19
README.md
|
|
@ -11,9 +11,19 @@
|
||||||
|
|
||||||
| Проект | Путь | Описание |
|
| Проект | Путь | Описание |
|
||||||
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
|
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
|
||||||
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
| Тестовая прошивка (✅ реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
||||||
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
|
| Загрузчик (⏳ запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
|
||||||
| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
| 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 |
|
| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored |
|
||||||
| Unity, fff, SEGGER RTT | vendored |
|
| Unity, fff, SEGGER RTT | vendored |
|
||||||
| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` |
|
| 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` без интернета
|
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета
|
||||||
(кроме Python-зависимостей).
|
(кроме Python-зависимостей).
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ bsp_status_t bsp_can_set_filter(uint8_t idx, uint32_t id,
|
||||||
uint32_t mask, bool is_extended);
|
uint32_t mask, bool is_extended);
|
||||||
bsp_status_t bsp_can_accept_all(void);
|
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()`:**
|
**Коды возврата `bsp_can_send()`:**
|
||||||
|
|
@ -147,7 +147,7 @@ bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true);
|
||||||
add_host_test(
|
add_host_test(
|
||||||
NAME test_bsp_can
|
NAME test_bsp_can
|
||||||
SOURCES can/test_bsp_can.c
|
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
|
${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c
|
||||||
INCLUDES
|
INCLUDES
|
||||||
${PROJECT_SOURCE_DIR}/bsp/can/include
|
${PROJECT_SOURCE_DIR}/bsp/can/include
|
||||||
|
|
@ -160,7 +160,7 @@ add_host_test(
|
||||||
**Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`:
|
**Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "can_mock.h"
|
#include "can_mocks.h"
|
||||||
|
|
||||||
void setUp(void) { CAN_MOCK_RESET_ALL(); }
|
void setUp(void) { CAN_MOCK_RESET_ALL(); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,18 +161,15 @@ static uint32_t poll_rx_mailboxes(void)
|
||||||
|
|
||||||
uint8_t mb_idx = RX_MB_FIRST + i;
|
uint8_t mb_idx = RX_MB_FIRST + i;
|
||||||
|
|
||||||
/* Проверяем флаг готовности MB. */
|
|
||||||
uint64_t mb_flag = (uint64_t) 1U << mb_idx;
|
uint64_t mb_flag = (uint64_t) 1U << mb_idx;
|
||||||
if (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, mb_flag) == 0U)
|
if (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, mb_flag) == 0U)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Читаем фрейм из MB. */
|
|
||||||
flexcan_frame_t sdk_frame;
|
flexcan_frame_t sdk_frame;
|
||||||
status_t sdk_status = FLEXCAN_ReadRxMb(BSP_CAN_BASE, mb_idx, &sdk_frame);
|
status_t sdk_status = FLEXCAN_ReadRxMb(BSP_CAN_BASE, mb_idx, &sdk_frame);
|
||||||
|
|
||||||
/* Очищаем флаг. */
|
|
||||||
FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, mb_flag);
|
FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, mb_flag);
|
||||||
|
|
||||||
if ((sdk_status == kStatus_Success) || (sdk_status == kStatus_FLEXCAN_RxOverflow))
|
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;
|
bsp_can_frame_t bsp_frame;
|
||||||
frame_from_sdk(&sdk_frame, &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));
|
ring_buffer_write(&g_s_rx_ring, (const uint8_t *) &bsp_frame, sizeof(bsp_frame));
|
||||||
received++;
|
received++;
|
||||||
}
|
}
|
||||||
|
|
@ -222,16 +218,13 @@ bsp_status_t bsp_can_init(const bsp_can_config_t *p_config)
|
||||||
return BSP_ERR_PARAM;
|
return BSP_ERR_PARAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Если уже инициализирован — сначала деинициализируем. */
|
|
||||||
if (g_s_initialized)
|
if (g_s_initialized)
|
||||||
{
|
{
|
||||||
bsp_can_deinit();
|
bsp_can_deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Инициализация ring buffer. */
|
|
||||||
ring_buffer_init(&g_s_rx_ring, g_s_rx_ring_storage, RX_RING_SIZE);
|
ring_buffer_init(&g_s_rx_ring, g_s_rx_ring_storage, RX_RING_SIZE);
|
||||||
|
|
||||||
/* Конфигурация FlexCAN. */
|
|
||||||
flexcan_config_t flexcan_cfg;
|
flexcan_config_t flexcan_cfg;
|
||||||
FLEXCAN_GetDefaultConfig(&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;
|
uint8_t mb_idx = RX_MB_FIRST + index;
|
||||||
|
|
||||||
/* Конфигурация RX MB. */
|
|
||||||
flexcan_rx_mb_config_t rx_mb_cfg;
|
flexcan_rx_mb_config_t rx_mb_cfg;
|
||||||
rx_mb_cfg.type = kFLEXCAN_FrameTypeData;
|
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;
|
flexcan_frame_t sdk_frame;
|
||||||
frame_to_sdk(p_frame, &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);
|
status_t wr_status = FLEXCAN_WriteTxMb(BSP_CAN_BASE, TX_MB_IDX, &sdk_frame);
|
||||||
if (wr_status != kStatus_Success)
|
if (wr_status != kStatus_Success)
|
||||||
{
|
{
|
||||||
return BSP_ERR_BUSY;
|
return BSP_ERR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ждать завершения передачи с таймаутом. */
|
|
||||||
uint64_t tx_flag = (uint64_t) 1U << TX_MB_IDX;
|
uint64_t tx_flag = (uint64_t) 1U << TX_MB_IDX;
|
||||||
uint32_t start_ms = bsp_tick_get_ms();
|
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);
|
FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, tx_flag);
|
||||||
|
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
|
|
@ -471,16 +460,13 @@ bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms)
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
/* Опросить все активные MB, сложить в ring buffer. */
|
|
||||||
poll_rx_mailboxes();
|
poll_rx_mailboxes();
|
||||||
|
|
||||||
/* Попробовать извлечь фрейм. */
|
|
||||||
if (try_dequeue_frame(p_frame))
|
if (try_dequeue_frame(p_frame))
|
||||||
{
|
{
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Проверить таймаут. */
|
|
||||||
uint32_t elapsed = bsp_tick_get_ms() - start_ms;
|
uint32_t elapsed = bsp_tick_get_ms() - start_ms;
|
||||||
if (elapsed >= timeout_ms)
|
if (elapsed >= timeout_ms)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* @file bsp/mqs.h
|
* @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);
|
* Физически на плате выведен один канал (MQS_RIGHT, GPIO_AD_B0_04);
|
||||||
* SAI1 требует стерео-буфер — оба канала всегда идентичны.
|
* SAI3 требует стерео-буфер — оба канала всегда идентичны.
|
||||||
*
|
*
|
||||||
* Режимы использования:
|
* Режимы использования:
|
||||||
* - firmware_test: bsp_mqs_play_blocking() — синхронная подача
|
* - firmware_test: bsp_mqs_play_blocking() — синхронная подача
|
||||||
|
|
@ -38,14 +38,14 @@ extern "C"
|
||||||
* Параметры аудио-потока
|
* Параметры аудио-потока
|
||||||
* ----------------------------------------------------------------------- */
|
* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
/** Частота дискретизации, Гц. Небольшое отклонение (~0.5%) из-за
|
/** Частота дискретизации, Гц. Источник — SAI3_CLK_ROOT (Audio PLL / 8 / 8),
|
||||||
* источника SAI1_CLK_ROOT (System PLL PFD2, не Audio PLL). */
|
* делитель MCLK подобран точно (8), отклонения нет. */
|
||||||
#define BSP_MQS_SAMPLE_RATE_HZ (44100U)
|
#define BSP_MQS_SAMPLE_RATE_HZ (44100U)
|
||||||
|
|
||||||
/** Разрядность PCM. MQS поддерживает только 16 бит. */
|
/** Разрядность PCM. MQS поддерживает только 16 бит. */
|
||||||
#define BSP_MQS_BIT_WIDTH (16U)
|
#define BSP_MQS_BIT_WIDTH (16U)
|
||||||
|
|
||||||
/** Количество каналов в буфере. SAI1+MQS требует стерео; правый == левый. */
|
/** Количество каналов в буфере. SAI3+MQS требует стерео; правый == левый. */
|
||||||
#define BSP_MQS_CHANNELS (2U)
|
#define BSP_MQS_CHANNELS (2U)
|
||||||
|
|
||||||
/** Байт на один моно-сэмпл (16 бит → 2 байта). */
|
/** Байт на один моно-сэмпл (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
|
* TX Master, 16 бит, стерео, 44100 Гц, инициализирует eDMA канал 0
|
||||||
* (DMAMUX source kDmaRequestMuxSai1Tx) и MQS-модуль.
|
* (DMAMUX source kDmaRequestMuxSai3Tx) и MQS-модуль.
|
||||||
*
|
*
|
||||||
* Пин GPIO_AD_B0_04 (MQS_RIGHT) уже замультиплексирован в BOARD_InitPins().
|
* Пин GPIO_AD_B0_04 (MQS_RIGHT) уже замультиплексирован в BOARD_InitPins().
|
||||||
* MQS oversample (×32) уже выставлен в BOARD_BootClockRUN().
|
* MQS oversample (×32) уже выставлен в BOARD_BootClockRUN().
|
||||||
|
|
@ -88,7 +88,7 @@ extern "C"
|
||||||
bsp_status_t bsp_mqs_init(void);
|
bsp_status_t bsp_mqs_init(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Деинициализация: остановить DMA, сбросить SAI1 и MQS.
|
* @brief Деинициализация: остановить DMA, сбросить SAI3 и MQS.
|
||||||
*
|
*
|
||||||
* Безопасно вызывать даже если воспроизведение уже завершилось.
|
* Безопасно вызывать даже если воспроизведение уже завершилось.
|
||||||
* После вызова модуль требует повторного bsp_mqs_init().
|
* После вызова модуль требует повторного 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.
|
* Настраивает XBARA1 (fault disable), PWM4 submodule 0 channel A.
|
||||||
* Вызывать до bsp_mqs_play() — без ШИМ на VOLUME усиление равно нулю.
|
* Вызывать до bsp_mqs_play() — без ШИМ на VOLUME усиление равно нулю.
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
/**
|
/**
|
||||||
* @file bsp_mqs.c
|
* @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)
|
* Audio PLL = 24 МГц × (30 + 66/625) = 722.534 МГц
|
||||||
* ≈ 63 529 411 Гц (BOARD_BOOTCLOCKRUN_SAI1_CLK_ROOT)
|
* 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 Гц
|
* 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() через
|
* MQS oversample = 32, уже выставлен в BOARD_BootClockRUN() через
|
||||||
* IOMUXC_MQSConfig(IOMUXC_GPR, kIOMUXC_MqsPwmOverSampleRate32, 0).
|
* IOMUXC_MQSConfig(IOMUXC_GPR, kIOMUXC_MqsPwmOverSampleRate32, 0).
|
||||||
*
|
*
|
||||||
* Пин: GPIO_AD_B0_04 → MQS_RIGHT — замультиплексирован в BOARD_InitPins().
|
* Пин: GPIO_AD_B0_04 → MQS_RIGHT — замультиплексирован в BOARD_InitPins().
|
||||||
*
|
*
|
||||||
* eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai1Tx.
|
* eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai3Tx.
|
||||||
* Канал 0 зарезервирован за bsp_mqs. Прочие модули — каналы 1+.
|
* Канал 0 зарезервирован за bsp_mqs. Прочие модули — каналы 1+.
|
||||||
*
|
*
|
||||||
* SAI API (SDK 2.4.7 / fsl_sai.h, fsl_sai_edma.h 2.7.3):
|
* 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_CLOCK_GATE kCLOCK_Sai3
|
||||||
#define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT
|
#define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT
|
||||||
|
|
||||||
/** eDMA канал, выделенный под SAI1 TX. */
|
/** eDMA канал, выделенный под SAI3 TX. */
|
||||||
#define MQS_DMA_CHANNEL (0U)
|
#define MQS_DMA_CHANNEL (0U)
|
||||||
|
|
||||||
/** DMAMUX запрос для SAI1 TX. */
|
/** DMAMUX запрос для SAI3 TX. */
|
||||||
#define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx
|
#define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx
|
||||||
|
|
||||||
/** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */
|
/** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */
|
||||||
#define MQS_DMA_IRQ_PRIORITY (5U)
|
#define MQS_DMA_IRQ_PRIORITY (5U)
|
||||||
#define MQS_HMCLK_GATE kCLOCK_Mqs
|
#define MQS_HMCLK_GATE kCLOCK_Mqs
|
||||||
/**
|
/**
|
||||||
* FIFO watermark — половина глубины FIFO SAI1.
|
* FIFO watermark — половина глубины FIFO SAI3.
|
||||||
* FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает
|
* FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает
|
||||||
* глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную
|
* глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную
|
||||||
* латентность DMA: запрос формируется когда в FIFO остаётся место для
|
* латентность DMA: запрос формируется когда в FIFO остаётся место для
|
||||||
|
|
@ -108,11 +110,6 @@ static void mqs_edma_callback(I2S_Type *p_base, sai_edma_handle_t *p_handle, sta
|
||||||
/* --------------------------------------------------------------------------
|
/* --------------------------------------------------------------------------
|
||||||
* Публичный API
|
* Публичный API
|
||||||
* ----------------------------------------------------------------------- */
|
* ----------------------------------------------------------------------- */
|
||||||
/*
|
|
||||||
* AUDIO PLL setting: Frequency = Fref * (DIV_SELECT + NUM / DENOM)
|
|
||||||
* = 24 * (32 + 768/1000)
|
|
||||||
* = 786.432 MHz
|
|
||||||
*/
|
|
||||||
|
|
||||||
bsp_status_t bsp_mqs_init(void)
|
bsp_status_t bsp_mqs_init(void)
|
||||||
{
|
{
|
||||||
|
|
@ -121,7 +118,7 @@ bsp_status_t bsp_mqs_init(void)
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Тактирование SAI1 --- */
|
/* --- Тактирование SAI3 --- */
|
||||||
CLOCK_EnableClock(MQS_SAI_CLOCK_GATE);
|
CLOCK_EnableClock(MQS_SAI_CLOCK_GATE);
|
||||||
|
|
||||||
/* --- Тактирование MQS (CCGR0[CG2]) --- */
|
/* --- Тактирование MQS (CCGR0[CG2]) --- */
|
||||||
|
|
@ -132,10 +129,10 @@ bsp_status_t bsp_mqs_init(void)
|
||||||
IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false);
|
IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false);
|
||||||
IOMUXC_MQSEnable(IOMUXC_GPR, true);
|
IOMUXC_MQSEnable(IOMUXC_GPR, true);
|
||||||
|
|
||||||
/* --- SAI1: базовая инициализация (снимает reset, включает clock gate) --- */
|
/* --- SAI3: базовая инициализация (снимает reset, включает clock gate) --- */
|
||||||
SAI_Init(MQS_SAI_BASE);
|
SAI_Init(MQS_SAI_BASE);
|
||||||
|
|
||||||
/* --- SAI1 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
|
/* --- SAI3 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
|
||||||
sai_transceiver_t sai_cfg;
|
sai_transceiver_t sai_cfg;
|
||||||
|
|
||||||
SAI_GetLeftJustifiedConfig(&sai_cfg, kSAI_WordWidth16bits, kSAI_Stereo,
|
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_Init(DMA0, &dma_cfg);
|
||||||
EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL);
|
EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL);
|
||||||
|
|
||||||
/* --- DMAMUX: канал 0 → SAI1 TX --- */
|
/* --- DMAMUX: канал 0 → SAI3 TX --- */
|
||||||
DMAMUX_Init(DMAMUX);
|
DMAMUX_Init(DMAMUX);
|
||||||
DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE);
|
DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE);
|
||||||
DMAMUX_EnableChannel(DMAMUX, MQS_DMA_CHANNEL);
|
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_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_TxSoftwareReset() отсутствует в данной версии SDK. */
|
||||||
SAI_TxReset(MQS_SAI_BASE);
|
SAI_TxReset(MQS_SAI_BASE);
|
||||||
IOMUXC_MQSEnable(IOMUXC_GPR, false);
|
IOMUXC_MQSEnable(IOMUXC_GPR, false);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
* аудио-сигнала на входе (MQS_RIGHT через RC-фильтр → SOUND_OUT).
|
* аудио-сигнала на входе (MQS_RIGHT через RC-фильтр → SOUND_OUT).
|
||||||
*
|
*
|
||||||
* Управление громкостью:
|
* Управление громкостью:
|
||||||
* PWM4 SM0 PWM_A, частота 16 кГц, центрально-симметричный режим.
|
* PWM4 SM0 PWM_A, частота 12 кГц, центрально-симметричный режим.
|
||||||
* duty 0% → DC_VOL ≈ 0 В → усиление минимально (тишина).
|
* duty 0% → DC_VOL ≈ 0 В → усиление минимально (тишина).
|
||||||
* duty 50% → DC_VOL ≈ 2.5 В → номинальная громкость.
|
* duty 50% → DC_VOL ≈ 2.5 В → номинальная громкость.
|
||||||
* duty 100%→ DC_VOL ≈ 5 В → максимальное усиление.
|
* duty 100%→ DC_VOL ≈ 5 В → максимальное усиление.
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
* Тактирование:
|
* Тактирование:
|
||||||
* IPG clock = AHB/4 = 600/4 = 150 МГц.
|
* IPG clock = AHB/4 = 600/4 = 150 МГц.
|
||||||
* PWM prescaler = /16 → PWM clock = 9.375 МГц.
|
* PWM prescaler = /16 → PWM clock = 9.375 МГц.
|
||||||
* Fpwm = 16000 Гц (центрально-симметричный режим).
|
* Fpwm = 9 375 000 / 586 / 2 = 12000 Гц (центрально-симметричный режим).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "bsp/mqs.h"
|
#include "bsp/mqs.h"
|
||||||
|
|
@ -96,7 +96,7 @@ bsp_status_t bsp_mqs_amp_init(void)
|
||||||
/* --- ForceSignal: использовать нормальный PWM-сигнал --- */
|
/* --- ForceSignal: использовать нормальный PWM-сигнал --- */
|
||||||
PWM_SetupForceSignal(AMP_PWM_BASE, AMP_PWM_SUBMODULE, AMP_PWM_CHANNEL, kPWM_UsePwm);
|
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 = {
|
const pwm_signal_param_t PWM_SIGNAL = {
|
||||||
.pwmChannel = AMP_PWM_CHANNEL,
|
.pwmChannel = AMP_PWM_CHANNEL,
|
||||||
.dutyCyclePercent = AMP_DEFAULT_DUTY,
|
.dutyCyclePercent = AMP_DEFAULT_DUTY,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
* Пин LOW (тока нет) → BSP_OPTO_STATE_INACTIVE
|
* Пин LOW (тока нет) → BSP_OPTO_STATE_INACTIVE
|
||||||
*
|
*
|
||||||
* Режимы каналов (bsp_opto_ch_mode_t):
|
* Режимы каналов (bsp_opto_ch_mode_t):
|
||||||
* ы
|
|
||||||
* BSP_OPTO_MODE_LEVEL — IN1, IN2
|
* BSP_OPTO_MODE_LEVEL — IN1, IN2
|
||||||
* Детектирование уровня с программным дебаунсом.
|
* Детектирование уровня с программным дебаунсом.
|
||||||
* ISR переключает направление прерывания (RISING↔FALLING) после каждого фронта,
|
* ISR переключает направление прерывания (RISING↔FALLING) после каждого фронта,
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,8 @@
|
||||||
* @brief Прочитать уникальный идентификатор чипа из OCOTP.
|
* @brief Прочитать уникальный идентификатор чипа из OCOTP.
|
||||||
*
|
*
|
||||||
* Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]).
|
* Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]).
|
||||||
* Результат записывается в big-endian порядке: p_uid[0] — старший байт CFG1,
|
* Результат записывается в нативном порядке байт (little-endian на Cortex-M7):
|
||||||
* p_uid[7] — младший байт CFG0. Hex-строка совпадает с визуальным порядком слов
|
* p_uid[0..3] = CFG0 (UID[31:0]), p_uid[4..7] = CFG1 (UID[63:32]).
|
||||||
* в Reference Manual (MIMXRT1052RM Table 46-2).
|
|
||||||
*
|
*
|
||||||
* Функция выполняет OCOTP_Init() и включает clock gate перед чтением.
|
* Функция выполняет OCOTP_Init() и включает clock gate перед чтением.
|
||||||
* Clock gate остаётся открытым после вызова (паттерн проекта).
|
* Clock gate остаётся открытым после вызова (паттерн проекта).
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,16 @@ flowchart TD
|
||||||
|
|
||||||
```c
|
```c
|
||||||
bsp_status_t bsp_uart_host_init(uint32_t baud);
|
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(const uint8_t *p_data, size_t len);
|
||||||
bsp_status_t bsp_uart_host_write_str(const char *p_str);
|
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);
|
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);
|
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()` возвращает фактически прочитанное количество байт —
|
`bsp_uart_host_read()` возвращает фактически прочитанное количество байт —
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Константы */
|
/* Константы */
|
||||||
/* -------------------------------------------------s------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/** Передать в timeout_ms чтобы ждать данные бесконечно. */
|
/** Передать в timeout_ms чтобы ждать данные бесконечно. */
|
||||||
#define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX)
|
#define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX)
|
||||||
|
|
|
||||||
|
|
@ -84,17 +84,14 @@ bsp_status_t bsp_uart_host_init(uint32_t baud_rate)
|
||||||
return BSP_ERR_INIT;
|
return BSP_ERR_INIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Инициализация кольцевого буфера. */
|
|
||||||
if (!ring_buffer_init(&g_s_rx_ring, g_s_rx_buf, BSP_UART_HOST_RX_BUFFER_SIZE))
|
if (!ring_buffer_init(&g_s_rx_ring, g_s_rx_buf, BSP_UART_HOST_RX_BUFFER_SIZE))
|
||||||
{
|
{
|
||||||
/* Размер не степень двойки — ошибка конфигурации. */
|
/* Размер не степень двойки — ошибка конфигурации. */
|
||||||
return BSP_ERR_INIT;
|
return BSP_ERR_INIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Тактирование LPUART1. */
|
|
||||||
CLOCK_EnableClock(kCLOCK_Lpuart1);
|
CLOCK_EnableClock(kCLOCK_Lpuart1);
|
||||||
|
|
||||||
/* Настройка периферии. */
|
|
||||||
lpuart_config_t config;
|
lpuart_config_t config;
|
||||||
LPUART_GetDefaultConfig(&config);
|
LPUART_GetDefaultConfig(&config);
|
||||||
config.baudRate_Bps = baud_rate;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Буфер пуст — проверяем таймаут. */
|
|
||||||
if (timeout_ms == 0U)
|
if (timeout_ms == 0U)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Испол
|
||||||
Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s).
|
Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s).
|
||||||
PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`.
|
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`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ extern "C"
|
||||||
*
|
*
|
||||||
* @pre board_hw_init() вызван (MPU настроен, NonCacheable регион активен).
|
* @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);
|
bsp_status_t bsp_usb_cdc_init(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -566,8 +566,6 @@ bsp_status_t bsp_usb_cdc_init(void)
|
||||||
|
|
||||||
USB_DeviceIsrEnable();
|
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);
|
bsp_delay(USB_ATTACH_DELAY_US / 1000);
|
||||||
USB_DeviceRun(g_usbDeviceHandle);
|
USB_DeviceRun(g_usbDeviceHandle);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ flowchart LR
|
||||||
│ ├── 01_test_uart.py ← HIL тест bsp_uart_host (без M5)
|
│ ├── 01_test_uart.py ← HIL тест bsp_uart_host (без M5)
|
||||||
│ ├── 02_test_opto.py ← HIL тест bsp_opto (через M5StampPLC)
|
│ ├── 02_test_opto.py ← HIL тест bsp_opto (через M5StampPLC)
|
||||||
│ ├── 03_test_can.py ← HIL тест bsp_can
|
│ ├── 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)
|
│ ├── 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_opto.py ← HIL тест opto через firmware_test CDC
|
||||||
│ ├── 06_test_firmware_can.py ← HIL тест CAN через firmware_test CDC
|
│ ├── 06_test_firmware_can.py ← HIL тест CAN через firmware_test CDC
|
||||||
|
|
|
||||||
|
|
@ -81,16 +81,21 @@ auto-config не подтверждена — см. 1.5.
|
||||||
|
|
||||||
`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не
|
`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не
|
||||||
в этом репозитории (например, старые платы с W25Q512), тем же способом
|
в этом репозитории (например, старые платы с 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) собирается из **сырого** бинарника на
|
- HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на
|
||||||
лету через `nxpimage`, а не заранее через `just build::hab-*`
|
лету через `HabImage` (spsdk), а не заранее через `just build::hab-*`
|
||||||
- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`,
|
- FCB пишется **явно** (`mboot.write_memory()` с готовым блобом
|
||||||
буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config
|
`tools/host/dcd/w25qXXX_fdcb.bin`, буквальная запись вместо
|
||||||
для 4-байтной адресации не проверялся, решили на него не полагаться
|
`configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не
|
||||||
|
проверялся, решили на него не полагаться
|
||||||
|
|
||||||
Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь
|
Подробности конвейера — в [tools/production/docs/DEV_ARCH.md](../tools/production/docs/DEV_ARCH.md),
|
||||||
(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему
|
§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через
|
||||||
|
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
|
||||||
использует auto-config Flashloader, как описано в 1.4.
|
использует auto-config Flashloader, как описано в 1.4.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
>
|
>
|
||||||
> Документ описывает протокол обмена между диагностической прошивкой
|
> Документ описывает протокол обмена между диагностической прошивкой
|
||||||
> (`firmware_test`) и хостовым ПО сервисного инженера.
|
> (`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 Таргет
|
participant T as Таргет
|
||||||
|
|
||||||
Note over T: прошивка загружена через USB SDP
|
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"}
|
H->>T: {"type":"cmd","cmd":"ping"}
|
||||||
T-->>H: {"type":"pong"}
|
T-->>H: {"type":"pong"}
|
||||||
|
|
@ -169,6 +175,17 @@ sequenceDiagram
|
||||||
← {"ok":false,"error":"UID_READ_ERR"}
|
← {"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` — запуск подмножества тестов
|
### `run_selected` — запуск подмножества тестов
|
||||||
|
|
||||||
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,
|
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,
|
||||||
|
|
@ -199,7 +216,7 @@ sequenceDiagram
|
||||||
### `session_start`
|
### `session_start`
|
||||||
|
|
||||||
```json
|
```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`
|
### `test_begin`
|
||||||
|
|
@ -222,6 +239,11 @@ sequenceDiagram
|
||||||
|
|
||||||
`detail` — ASCII-строка до 95 символов. При `pass` — пустая.
|
`detail` — ASCII-строка до 95 символов. При `pass` — пустая.
|
||||||
|
|
||||||
|
### `test_list`
|
||||||
|
|
||||||
|
Ответ на `list_tests` — массив дескрипторов теста (`id`, `name`,
|
||||||
|
`critical`, `requires_hil`), см. пример в разделе `list_tests` выше.
|
||||||
|
|
||||||
### `progress`
|
### `progress`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -230,6 +252,11 @@ sequenceDiagram
|
||||||
|
|
||||||
Промежуточные шаги внутри теста. Используется в `usd`.
|
Промежуточные шаги внутри теста. Используется в `usd`.
|
||||||
|
|
||||||
|
### `uid_response` / `version_response`
|
||||||
|
|
||||||
|
Ответы на `get_uid`/`get_version` — см. описание соответствующих команд
|
||||||
|
выше.
|
||||||
|
|
||||||
### `confirm_request`
|
### `confirm_request`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -270,25 +297,30 @@ sequenceDiagram
|
||||||
| `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID |
|
| `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID |
|
||||||
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
|
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
|
||||||
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
|
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
|
||||||
|
| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Матрица тестов
|
## Матрица тестов
|
||||||
|
|
||||||
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|
Порядок — как в реестре `k_registry[]` (`test_runner.c`); полная версия с
|
||||||
| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ |
|
кодами `detail` и HIL-таблицей реле — в
|
||||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
[firmware/test/README.md §Матрица тестов](../../firmware/test/README.md#матрица-тестов).
|
||||||
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
|
|
||||||
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
|
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|
||||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) |
|
| --------- | ------------------- | ------------------ | -------- | -------- | --------------------- |
|
||||||
| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
|
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||||
| `can` | CAN | HIL | ❌ | ✅ | ❌ |
|
| `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
|
| `usd` | microSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
|
||||||
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ |
|
| `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ (6 шагов, см. ниже) |
|
||||||
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
|
| `buttons` | Test Buttons | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
|
||||||
|
| `opto` | Opto Inputs | HIL | ❌ | ✅ | ❌ (авто, 6 шагов) |
|
||||||
|
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ (авто, 2 шага) |
|
||||||
|
| `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ (слышимость тона) |
|
||||||
|
|
||||||
**Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive** —
|
**Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive** —
|
||||||
требует `confirm_request`; **HIL** — требует M5StampPLC.
|
требует `confirm_request`, отвечает оператор; **HIL** — требует M5StampPLC,
|
||||||
|
confirm автоматический (без оператора).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -320,6 +352,10 @@ sequenceDiagram
|
||||||
|
|
||||||
### Display (RGB888)
|
### Display (RGB888)
|
||||||
|
|
||||||
|
Шесть шагов: Red → Green → Blue → White, затем два ротационных (диагностика
|
||||||
|
непропаянных LR/UD пинов на TFT7/8/10). Тест прерывается на **первом**
|
||||||
|
неподтверждённом шаге.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant H as Хост
|
participant H as Хост
|
||||||
|
|
@ -333,10 +369,36 @@ sequenceDiagram
|
||||||
T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
|
T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
|
||||||
H->>T: {"type":"confirm","id":"display_blue","confirmed":true}
|
H->>T: {"type":"confirm","id":"display_blue","confirmed":true}
|
||||||
T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
|
T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
|
||||||
H->>T: {"type":"confirm","id":"display_white","confirmed":false}
|
H->>T: {"type":"confirm","id":"display_white","confirmed":true}
|
||||||
T-->>H: {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"}
|
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:"<id> 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
|
```mermaid
|
||||||
|
|
@ -393,32 +455,15 @@ firmware/test/src/
|
||||||
├── test_usd.c
|
├── test_usd.c
|
||||||
├── test_display.c
|
├── test_display.c
|
||||||
├── test_buttons.c
|
├── test_buttons.c
|
||||||
|
├── test_opto.c
|
||||||
├── test_can.c
|
├── test_can.c
|
||||||
├── test_uart_ttl.c
|
└── test_mqs.c
|
||||||
├── test_uart_iso.c
|
|
||||||
└── test_opto.c
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Добавление нового теста
|
### Добавление нового теста
|
||||||
|
|
||||||
1. Создать `firmware/test/src/tests/test_foo.c`.
|
Пошаговый гайд с шаблонами (self-тест, интерактивный, pre-confirm) —
|
||||||
2. Объявить дескриптор:
|
[firmware/test/README.md §Как добавить новый тест](../../firmware/test/README.md#как-добавить-новый-тест).
|
||||||
|
|
||||||
```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` таргета.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,18 @@ ls build/target-debug/tests/target/<name>/test_<name>.elf
|
||||||
|
|
||||||
## Шаг 5 — `conftest.py`: добавить фикстуры
|
## Шаг 5 — `conftest.py`: добавить фикстуры
|
||||||
|
|
||||||
### Базовый тест (без M5)
|
### Любой тест — фикстура загрузки всегда зависит от `m5`
|
||||||
|
|
||||||
|
M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а
|
||||||
|
не только сигнальными реле — поэтому `loaded_<n>` зависит от `m5` **во всех
|
||||||
|
случаях**, даже если сам тест не использует реле для сигналов (например,
|
||||||
|
`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается
|
||||||
|
подключиться к обесточенной плате.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# 1. Фикстура загрузки
|
# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def loaded_<n>(request: pytest.FixtureRequest) -> None:
|
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||||
_load_elf(
|
_load_elf(
|
||||||
request,
|
request,
|
||||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
||||||
|
|
@ -184,26 +190,10 @@ _UART_FIXTURE_MAP = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Тест с M5
|
Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_<n>`
|
||||||
|
(она всегда одна и та же), а в том, использует ли сам **тест-кейс**
|
||||||
```python
|
`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами
|
||||||
# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF
|
помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже).
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
|
||||||
_load_elf(
|
|
||||||
request,
|
|
||||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. UART-фикстура — та же одна строка
|
|
||||||
_UART_FIXTURE_MAP = {
|
|
||||||
...
|
|
||||||
"uart_<n>": "loaded_<n>",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Правило:** если тест управляет железом через M5 — `loaded_<n>` должен явно
|
|
||||||
зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph Host["Хост (macOS / Linux)"]
|
subgraph DC["Devcontainer (единственное место запуска)"]
|
||||||
DS["just host::debug-server\npyocd gdbserver :3333"]
|
C["tests/host/<dir>/test_<name>.c\nUnity [+ fff]"]
|
||||||
ML["MCU-Link (CMSIS-DAP)"]
|
CP["CMakePresets.json\nhost-debug / host-release"]
|
||||||
DS --> ML
|
JB["just/build.just\ntest-host"]
|
||||||
|
C --> CP --> JB
|
||||||
end
|
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`. Из контейнера
|
Host-тесты компилируются `clang-17` **на хосте** (не ARM GCC), исполняются
|
||||||
GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker,
|
как обычные нативные бинарники под `ctest`. Никакого железа не требуется —
|
||||||
резолвится в IP хост-машины.
|
в отличие от 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` |
|
||||||
|
|
||||||
| Компонент | Роль | Источник |
|
Полное объяснение разницы и структуры — в
|
||||||
| --------------------------------- | ------------------------------- | -------------------------- |
|
[tests/host/README.md §1](../../../tests/host/README.md#1-две-категории-тестируемых-модулей).
|
||||||
| `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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Поддерживаемые прошивки
|
## Шаг 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале)
|
mkdir -p tests/host/<name>/
|
||||||
just host::debug-server
|
touch tests/host/<name>/test_<name>.c
|
||||||
|
|
||||||
# 2. DevContainer — VSCode
|
|
||||||
# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5
|
|
||||||
```
|
```
|
||||||
|
|
||||||
GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе
|
### Шаблон — категория A (без моков)
|
||||||
в `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.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "SEGGER_RTT.h"
|
#include "unity.h"
|
||||||
SEGGER_RTT_printf(0, "value = %d\n", value);
|
#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/<module>.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_<name>
|
||||||
|
SOURCES <name>/test_<name>.c
|
||||||
|
${PROJECT_SOURCE_DIR}/<путь-к-модулю>/<module>.c
|
||||||
|
INCLUDES ${PROJECT_SOURCE_DIR}/<путь-к-инклюдам>
|
||||||
|
)
|
||||||
|
|
||||||
|
# категория B — BSP-модуль, нужны MOCKS
|
||||||
|
add_host_test(
|
||||||
|
NAME test_<name>
|
||||||
|
SOURCES <name>/test_<name>.c
|
||||||
|
${PROJECT_SOURCE_DIR}/bsp/<name>/src/<name>.c
|
||||||
|
INCLUDES ${PROJECT_SOURCE_DIR}/bsp/<name>/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_<name> PRIVATE UNIT_TEST)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FreeRTOS task view
|
## Шаг 3 — Собрать и прогнать
|
||||||
|
|
||||||
Конфигурация `🐛 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-символов.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Быстрый старт (первый запуск)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux):
|
# конфигурация (один раз или после изменения CMakeLists)
|
||||||
# "runArgs": ["--add-host=host.docker.internal:host-gateway"]
|
cmake --preset host-debug
|
||||||
|
|
||||||
# 2. Залить прошивку
|
# сборка + тесты одной командой
|
||||||
just host::flash-test-debug
|
just build::test-host
|
||||||
|
|
||||||
# 3. Хост — запустить GDB-сервер
|
# конкретный тест с полным выводом Unity
|
||||||
just host::debug-server
|
ctest --preset host-debug-test -R test_<name> -V
|
||||||
|
|
||||||
# 4. DevContainer — VSCode
|
# напрямую — без обёртки CTest
|
||||||
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
|
./build/host-debug/tests/host/test_<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
`just build::test-host` собирает под пресетом `host-debug` (`clang-17`,
|
||||||
|
без ARM-специфики) и прогоняет весь набор через CTest. `host-release`
|
||||||
|
собирает тот же набор с оптимизациями — используется в CI как
|
||||||
|
дополнительный гейт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чеклист
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[ ] tests/host/<name>/test_<name>.c — тест-файл (категория A или B)
|
||||||
|
[ ] tests/host/mocks/*.h — новый stub-хедер, если модуль
|
||||||
|
использует ранее не замоканный SDK-вызов
|
||||||
|
[ ] tests/host/CMakeLists.txt — add_host_test(...) для нового теста
|
||||||
|
[ ] just build::test-host — зелёная сборка + прогон
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Дерево файлов отладки
|
## Справочник
|
||||||
|
|
||||||
```bash
|
Полный API Unity (assertion-макросы), fff (создание фейков, `custom_fake`,
|
||||||
.
|
проверка вызовов), работа со stub-хедерами и типичные ловушки (dangling
|
||||||
├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH
|
pointer из `arg_history`, `static`-функции, `ScopeMismatch`-аналоги для
|
||||||
├── .vscode/
|
host-тестов) — в [tests/host/README.md](../../../tests/host/README.md).
|
||||||
│ ├── 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
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -1,488 +0,0 @@
|
||||||
# firmware_test — План разработки
|
|
||||||
|
|
||||||
> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контекст проекта
|
|
||||||
|
|
||||||
**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации).
|
|
||||||
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
|
|
||||||
|
|
||||||
**Стенд:**
|
|
||||||
|
|
||||||
- Хост подключается через USB CDC ACM — единственный канал firmware_test
|
|
||||||
- HIL-тесты управляются через M5StampPLC (опционально)
|
|
||||||
- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Текущий статус
|
|
||||||
|
|
||||||
| Компонент | Статус | Примечание |
|
|
||||||
| ------------------------------ | ------ | ------------------------------------------------ |
|
|
||||||
| `bsp_usb_cdc` | ✅ | HIL тест пройден |
|
|
||||||
| firmware_test скелет | ✅ | `main.c` + `cli.c` |
|
|
||||||
| Протокол v2 + test_runner | ✅ | JSON-lines event-driven |
|
|
||||||
| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention |
|
|
||||||
| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
|
|
||||||
| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага |
|
|
||||||
| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified |
|
|
||||||
| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified |
|
|
||||||
| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified |
|
|
||||||
| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified |
|
|
||||||
| `test_opto` | ✅ | Этап 6б, hardware-verified |
|
|
||||||
| `test_can` | ✅ | Этап 6в, hardware-verified |
|
|
||||||
| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура |
|
|
||||||
| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` |
|
|
||||||
| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` |
|
|
||||||
| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified |
|
|
||||||
| Provisioning | ⬜ | Этап 7 |
|
|
||||||
| TUI сервисного инженера | ⬜ | Этап 8 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Матрица тестов — итоговая
|
|
||||||
|
|
||||||
| ID | Название | Critical | HIL | Тип | BSP | Статус |
|
|
||||||
| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ |
|
|
||||||
| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ |
|
|
||||||
| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ |
|
|
||||||
| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ |
|
|
||||||
| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ |
|
|
||||||
| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ |
|
|
||||||
| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ |
|
|
||||||
| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ |
|
|
||||||
| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ |
|
|
||||||
|
|
||||||
**Убранные тесты (закрытые решения):**
|
|
||||||
|
|
||||||
- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется
|
|
||||||
- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Закрытые архитектурные решения
|
|
||||||
|
|
||||||
> Не пересматривать без явного запроса.
|
|
||||||
|
|
||||||
### Этапы 1–5 (ранее зафиксированные)
|
|
||||||
|
|
||||||
- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
|
|
||||||
- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`.
|
|
||||||
- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
|
|
||||||
- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`.
|
|
||||||
- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7).
|
|
||||||
- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`.
|
|
||||||
- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL.
|
|
||||||
- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP.
|
|
||||||
|
|
||||||
### Этап 6 (новые решения)
|
|
||||||
|
|
||||||
- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL).
|
|
||||||
TUI фильтрует HIL-тесты если M5StampPLC не подключён.
|
|
||||||
- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста.
|
|
||||||
TUI строит UI динамически, не хардкодит список тестов.
|
|
||||||
- **`run_selected`:** запуск произвольного подмножества тестов по списку ID.
|
|
||||||
Порядок выполнения — как в реестре таргета, не как в запросе.
|
|
||||||
Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI.
|
|
||||||
- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request`
|
|
||||||
от HIL-теста TUI командует M5, получает результат, отправляет confirm.
|
|
||||||
- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты
|
|
||||||
недоступны (серые в UI, не входят в `run_selected`).
|
|
||||||
- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`.
|
|
||||||
- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен
|
|
||||||
один канал. Буфер всегда стерео (L+R идентичны).
|
|
||||||
- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая),
|
|
||||||
`confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL.
|
|
||||||
`critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`.
|
|
||||||
- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`.
|
|
||||||
Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M.
|
|
||||||
Нарушение порядка приводит к щелчку при старте или отсутствию звука.
|
|
||||||
- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking),
|
|
||||||
параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с.
|
|
||||||
- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно
|
|
||||||
выставлять в `true`. При инициализации через designated initializers без явного
|
|
||||||
указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит
|
|
||||||
на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого
|
|
||||||
прогона). Воспроизводится только при cold reset.
|
|
||||||
- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на
|
|
||||||
`CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate
|
|
||||||
может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)`
|
|
||||||
перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым —
|
|
||||||
закрывать не нужно, LPUART1 тактируется с минимальным потреблением.
|
|
||||||
`bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`.
|
|
||||||
- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при
|
|
||||||
старте, может быть пропущено). Проверяет живость через `ping → pong`.
|
|
||||||
- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина
|
|
||||||
без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется
|
|
||||||
в `test_opto.c` после settle — обходит race condition когда чётное число ISR
|
|
||||||
при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`.
|
|
||||||
- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm,
|
|
||||||
но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`.
|
|
||||||
- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`.
|
|
||||||
Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`,
|
|
||||||
`bsp_opto_force_read()` читает финальное состояние пина напрямую.
|
|
||||||
|
|
||||||
### Этап 8 (TUI решения)
|
|
||||||
|
|
||||||
- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost).
|
|
||||||
Оператор сам переставляет перемычку BOOT — это ок, документируется.
|
|
||||||
- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130)
|
|
||||||
или CDC firmware_test (session_start) — и показывает соответствующий экран.
|
|
||||||
- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты,
|
|
||||||
работает в SSH-сессии, вписывается в uv-экосистему.
|
|
||||||
- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/`
|
|
||||||
и `tools/production/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН
|
|
||||||
|
|
||||||
### 6а — Расширение протокола ✅
|
|
||||||
|
|
||||||
**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md`
|
|
||||||
|
|
||||||
#### Новая команда `list_tests`
|
|
||||||
|
|
||||||
```json
|
|
||||||
→ {"type":"cmd","cmd":"list_tests"}
|
|
||||||
← {"type":"test_list","tests":[
|
|
||||||
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
|
|
||||||
{"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false},
|
|
||||||
{"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
|
|
||||||
{"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false},
|
|
||||||
{"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false},
|
|
||||||
{"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false},
|
|
||||||
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true},
|
|
||||||
{"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Новая команда `run_selected`
|
|
||||||
|
|
||||||
```json
|
|
||||||
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]}
|
|
||||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
|
||||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
|
||||||
← {"type":"test_begin","id":"qspi",...}
|
|
||||||
← {"type":"test_result","id":"qspi",...}
|
|
||||||
← {"type":"test_begin","id":"display",...}
|
|
||||||
← {"type":"test_result","id":"display",...}
|
|
||||||
← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"}
|
|
||||||
```
|
|
||||||
|
|
||||||
Если хотя бы один ID не найден в реестре:
|
|
||||||
|
|
||||||
```json
|
|
||||||
← {"ok":false,"error":"UNKNOWN_TEST"}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Реализация в `test_runner.c`:**
|
|
||||||
|
|
||||||
- Новый режим `RUNNER_MODE_SELECTED`
|
|
||||||
- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc
|
|
||||||
- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция
|
|
||||||
|
|
||||||
### 6б — test_opto.c ✅
|
|
||||||
|
|
||||||
**Файл:** `firmware/test/src/tests/test_opto.c`
|
|
||||||
|
|
||||||
6 шагов, попарно ACTIVE/INACTIVE для трёх каналов:
|
|
||||||
|
|
||||||
| Шаг | confirm_request id | M5 действие | Проверка |
|
|
||||||
| --- | ------------------- | ----------- | -------------------------------- |
|
|
||||||
| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` |
|
|
||||||
| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` |
|
|
||||||
| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` |
|
|
||||||
| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` |
|
|
||||||
| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` |
|
|
||||||
| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` |
|
|
||||||
|
|
||||||
- Init: `bsp_opto_init()` единым вызовом для всех каналов
|
|
||||||
- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`)
|
|
||||||
- FAIL при несоответствии: `detail = "<id> state mismatch: expected ACTIVE got INACTIVE"`
|
|
||||||
- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг
|
|
||||||
|
|
||||||
### 6в — test_can.c ✅
|
|
||||||
|
|
||||||
**Файл:** `firmware/test/src/tests/test_can.c`
|
|
||||||
|
|
||||||
2 шага, оба направления независимо:
|
|
||||||
|
|
||||||
**Шаг 1 — RX (M5 → таргет):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
confirm_request("can_rx_ready")
|
|
||||||
→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF])
|
|
||||||
→ TUI: confirm(true)
|
|
||||||
→ таргет: bsp_can_receive(&frame, 500 мс)
|
|
||||||
→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF]
|
|
||||||
→ FAIL если timeout или несовпадение
|
|
||||||
```
|
|
||||||
|
|
||||||
**Шаг 2 — TX (таргет → M5):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс)
|
|
||||||
confirm_request("can_tx_verify")
|
|
||||||
→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data
|
|
||||||
→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет
|
|
||||||
→ FAIL если confirmed=false или timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует
|
|
||||||
- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()`
|
|
||||||
|
|
||||||
### 6г — bsp_mqs + test_mqs.c ✅
|
|
||||||
|
|
||||||
**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c`
|
|
||||||
|
|
||||||
**bsp_mqs:**
|
|
||||||
|
|
||||||
- SAI3 + eDMA (DMA0 канал 0) + MQS периферия
|
|
||||||
- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT`
|
|
||||||
- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358
|
|
||||||
- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`,
|
|
||||||
`bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume`
|
|
||||||
|
|
||||||
**test_mqs:**
|
|
||||||
|
|
||||||
- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида
|
|
||||||
- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле
|
|
||||||
- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL
|
|
||||||
- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг)
|
|
||||||
- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`
|
|
||||||
|
|
||||||
### 6д — HIL pytest для firmware_test ✅
|
|
||||||
|
|
||||||
**Файлы:**
|
|
||||||
|
|
||||||
```
|
|
||||||
tools/hil/conftest.py ← фикстура firmware_cdc
|
|
||||||
tools/hil/06_test_firmware_opto.py
|
|
||||||
tools/hil/06_test_firmware_can.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Фикстура `firmware_cdc`:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def firmware_cdc(m5):
|
|
||||||
"""
|
|
||||||
Открывает USB CDC порт firmware_test.
|
|
||||||
firmware_test уже прошит в Flash (не загружается pyOCD).
|
|
||||||
Проверяет живость через ping → pong.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
**`FirmwareCdcClient`** — тонкий клиент:
|
|
||||||
|
|
||||||
- `send_cmd(cmd_dict)` — отправить JSON команду
|
|
||||||
- `wait_event(type, timeout_s)` — ждать события нужного типа
|
|
||||||
- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}`
|
|
||||||
- `run_test(id)` — запустить тест, вернуть test_result dict
|
|
||||||
|
|
||||||
**Justfile:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hil-firmware-opto → pytest 06_test_firmware_opto.py -v
|
|
||||||
hil-firmware-can → pytest 06_test_firmware_can.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 7 — Provisioning
|
|
||||||
|
|
||||||
### Что нужно
|
|
||||||
|
|
||||||
1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp`
|
|
||||||
2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary`
|
|
||||||
3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста
|
|
||||||
4. Записывать статус в Flash (первый сектор после прошивки, вне XIP)
|
|
||||||
|
|
||||||
### BSP (предварительно)
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* bsp/provisioning/include/bsp/provisioning.h */
|
|
||||||
bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */
|
|
||||||
```
|
|
||||||
|
|
||||||
### Открытые вопросы — Этап 7
|
|
||||||
|
|
||||||
- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID?
|
|
||||||
- [ ] Нужна ли защита от повторного provisioning (write-once)?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Этап 8 — TUI сервисного инженера
|
|
||||||
|
|
||||||
### Стек технологий
|
|
||||||
|
|
||||||
| Компонент | Выбор | Обоснование |
|
|
||||||
| ------------- | ------------- | ----------------------------------------------------- |
|
|
||||||
| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема |
|
|
||||||
| Serial | pyserial | Уже в стеке (tools/hil) |
|
|
||||||
| Прошивка | spsdk | sdphost + blhost, уже в tools/host |
|
|
||||||
| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом |
|
|
||||||
|
|
||||||
### Структура приложения
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tools/production/
|
|
||||||
├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv
|
|
||||||
├── uv.lock
|
|
||||||
├── main.py ← точка входа
|
|
||||||
├── app/
|
|
||||||
│ ├── tui.py ← Textual App, экраны, layout
|
|
||||||
│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test
|
|
||||||
│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py)
|
|
||||||
│ ├── flasher.py ← USB SDP обёртка над spsdk
|
|
||||||
│ ├── orchestrator.py ← confirm_request → M5 action → confirm response
|
|
||||||
│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses)
|
|
||||||
└── README.md
|
|
||||||
|
|
||||||
tools/shared/
|
|
||||||
└── m5_agent.py ← общая M5-логика для hil/ и production/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Два режима работы
|
|
||||||
|
|
||||||
**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─ Прошивка платы ─────────────────────────────────┐
|
|
||||||
│ Обнаружен BootROM (SDP режим) │
|
|
||||||
│ │
|
|
||||||
│ Что прошить? │
|
|
||||||
│ ◉ firmware_test (диагностика) │
|
|
||||||
│ ○ Production (bootloader + tft_app) │
|
|
||||||
│ │
|
|
||||||
│ Файл: [/path/to/firmware_test_hab.bin ···] │
|
|
||||||
│ │
|
|
||||||
│ [ Прошить ] │
|
|
||||||
│ │
|
|
||||||
│ ████████████░░░░░░ 64% Запись во Flash... │
|
|
||||||
└────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Режим B — Диагностика** (триггер: session_start получен по CDC)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─ Диагностика платы fw:0.1.0 ─────────────────────┐
|
|
||||||
│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │
|
|
||||||
├────────────────────────────────────────────────────┤
|
|
||||||
│ Выбор тестов: │ Результаты: │
|
|
||||||
│ ☑ SDRAM 32 MB │ sdram ✓ PASS │
|
|
||||||
│ ☑ QSPI Flash │ qspi ✓ PASS │
|
|
||||||
│ ☑ microSD │ usd ✗ FAIL │
|
|
||||||
│ ☑ TFT Display │ mount failed: 5 │
|
|
||||||
│ ☑ Кнопки │ display ✓ PASS │
|
|
||||||
│ ☑ MQS Audio │ buttons ✓ PASS │
|
|
||||||
│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │
|
|
||||||
│ ☑ Оптовходы [HIL] │ ... │
|
|
||||||
├────────────────────────────────────────────────────┤
|
|
||||||
│ [ Запустить выбранные ] [ Все тесты ] │
|
|
||||||
│ ████████████████░░░░ 80% Тест: display │
|
|
||||||
├────────────────────────────────────────────────────┤
|
|
||||||
│ ⚠ Экран залит красным цветом? │
|
|
||||||
│ [ ✓ Да ] [ ✗ Нет ] │
|
|
||||||
└────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Поведение confirm_request в TUI
|
|
||||||
|
|
||||||
| Тип теста | Источник confirm | Действие TUI |
|
|
||||||
| -------------------- | ------------------ | --------------------------------------------- |
|
|
||||||
| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown |
|
|
||||||
| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown |
|
|
||||||
| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result |
|
|
||||||
| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) |
|
|
||||||
|
|
||||||
HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt.
|
|
||||||
|
|
||||||
### Конфигурация (.env)
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# Существующие переменные (tools/hil/.env):
|
|
||||||
HIL_VCOM_PORT=/dev/ttyACM0
|
|
||||||
HIL_M5_PORT=/dev/ttyACM1
|
|
||||||
|
|
||||||
# Новые переменные для production TUI:
|
|
||||||
SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start
|
|
||||||
SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5
|
|
||||||
FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin
|
|
||||||
PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin
|
|
||||||
PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Запуск
|
|
||||||
|
|
||||||
```bash
|
|
||||||
just host::service-tui # запустить TUI сервисного инженера
|
|
||||||
just host::service-flash <bin> # прошить без TUI (для автоматизации)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Процесс работы сервисника
|
|
||||||
|
|
||||||
**Диагностика (firmware_test уже в Flash):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
1. Плата в нормальном режиме (BOOT_MOD_1 → GND)
|
|
||||||
2. Подключить USB к сервисному ПК
|
|
||||||
3. just host::service-tui → TUI обнаружил session_start → Режим B
|
|
||||||
4. Выбрать тесты → Запустить → Смотреть результаты
|
|
||||||
```
|
|
||||||
|
|
||||||
**Перепрошивка (нужна новая версия firmware_test или production):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
1. Перемычка BOOT_MOD_1 → 3V3
|
|
||||||
2. Reset, подключить USB
|
|
||||||
3. TUI обнаружил 1FC9:0130 → Режим A
|
|
||||||
4. Выбрать бинарь → Прошить
|
|
||||||
5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Порядок реализации
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ Этап 1 протокол v2 + runner
|
|
||||||
✅ Этап 2 bsp_sdram + test_sdram
|
|
||||||
✅ Этап 3 bsp_qspi_flash + test_qspi
|
|
||||||
✅ Этап 4 bsp_sd + test_usd
|
|
||||||
✅ Этап 5 display + buttons
|
|
||||||
✅ Этап 6а протокол: list_tests + run_selected
|
|
||||||
✅ Этап 6б test_opto.c + hardware верификация
|
|
||||||
✅ Этап 6в test_can.c + hardware верификация
|
|
||||||
✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc)
|
|
||||||
✅ Этап 6е HIL pytest: 06_test_firmware_opto.py
|
|
||||||
✅ Этап 6ж HIL pytest: 06_test_firmware_can.py
|
|
||||||
✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация
|
|
||||||
|
|
||||||
⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ
|
|
||||||
|
|
||||||
⬜ Этап 8а tools/production/ скелет + models + clients
|
|
||||||
⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты)
|
|
||||||
⬜ Этап 8в Экран прошивки (flasher + SDP автодетект)
|
|
||||||
⬜ Этап 8г Provisioning в TUI
|
|
||||||
⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики)
|
|
||||||
|
|
||||||
⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Зависимости между этапами
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs)
|
|
||||||
↓
|
|
||||||
✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest)
|
|
||||||
↓
|
|
||||||
⬜ 7 (provisioning)
|
|
||||||
↓
|
|
||||||
⬜ 8 (TUI)
|
|
||||||
```
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
# firmware_test
|
# firmware_test
|
||||||
|
|
||||||
> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по
|
> Диагностическая прошивка для плат TFT индикаторов, вернувшихся по
|
||||||
> рекламации. Запускается сервисным инженером через USB CDC ACM без
|
> рекламации. Загружается на таргет сервисным инженером через USB (через BootROM IMXRT1052).
|
||||||
> предварительной прошивки загрузчика.
|
|
||||||
>
|
>
|
||||||
> Версия прошивки: `0.1.0` | Протокол: v2
|
>Версия прошивки: `0.1.2` | Протокол: v2
|
||||||
|
>
|
||||||
|
>Версия — из `project(firmware_test VERSION X.Y.Z)` в `CMakeLists.txt`
|
||||||
|
> (см. [Версионирование](#версионирование))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -27,6 +29,7 @@
|
||||||
- [Как добавить новый тест](#как-добавить-новый-тест)
|
- [Как добавить новый тест](#как-добавить-новый-тест)
|
||||||
- [Host unit-тесты](#host-unit-тесты)
|
- [Host unit-тесты](#host-unit-тесты)
|
||||||
- [Версионирование](#версионирование)
|
- [Версионирование](#версионирование)
|
||||||
|
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -45,7 +48,7 @@ just build::hab-firmware-test-debug
|
||||||
# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
|
# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
|
||||||
just host::flash-test-debug
|
just host::flash-test-debug
|
||||||
|
|
||||||
# Или через SWD (power cycle после)
|
# Или через SWD
|
||||||
just host::flash-swd-test-debug
|
just host::flash-swd-test-debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -66,7 +69,7 @@ screen /dev/ttyACM0
|
||||||
После подключения таргет сразу присылает:
|
После подключения таргет сразу присылает:
|
||||||
|
|
||||||
```json
|
```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
|
```json
|
||||||
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
||||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
← {"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 сообщение
|
│ JSON-lines, 1 строка = 1 сообщение
|
||||||
▼
|
▼
|
||||||
[Плата MIMXRT1052 с firmware_test]
|
[Плата 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 тестов]
|
[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`).
|
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
|
||||||
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
|
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
|
||||||
интерактивными шагами через `confirm`.
|
интерактивными шагами через `confirm`.
|
||||||
- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок
|
- Тесты **атомарны**: инженер запускает один тест, подмножество или все сразу —
|
||||||
не фиксирован.
|
порядок не фиксирован.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -136,6 +139,8 @@ firmware/test/
|
||||||
└── src/
|
└── src/
|
||||||
├── main.c — инициализация BSP, главный цикл
|
├── main.c — инициализация BSP, главный цикл
|
||||||
│
|
│
|
||||||
|
├── version.h.in — шаблон версии (CMake → generated/version.h)
|
||||||
|
│
|
||||||
├── cli.h / cli.c — IO-слой
|
├── cli.h / cli.c — IO-слой
|
||||||
│ буферизация строк, парсинг "type",
|
│ буферизация строк, парсинг "type",
|
||||||
│ диспатч на test_runner / protocol
|
│ диспатч на test_runner / protocol
|
||||||
|
|
@ -149,20 +154,23 @@ firmware/test/
|
||||||
│
|
│
|
||||||
├── test_runner.h / .c — реестр + state machine
|
├── test_runner.h / .c — реестр + state machine
|
||||||
│ IDLE → PRE_CONFIRM → RUNNING → IDLE
|
│ IDLE → PRE_CONFIRM → RUNNING → IDLE
|
||||||
│ test_runner_wait_confirm() для display
|
│ test_runner_wait_confirm() для in-run confirm
|
||||||
│
|
│
|
||||||
└── tests/
|
└── tests/
|
||||||
├── test_sdram.c — SDRAM 32 MB (self)
|
├── test_sdram.c — SDRAM 32 MB (self)
|
||||||
├── test_qspi.c — QSPI Flash 8 MB (self)
|
├── test_qspi.c — QSPI Flash W25Qxx (self)
|
||||||
├── test_usd.c — uSD SDIO (interactive)
|
├── test_usd.c — microSD SDIO (interactive, pre-confirm)
|
||||||
├── test_display.c — Display RGB888 (interactive)
|
├── test_display.c — Display RGB888 (interactive, in-run confirm)
|
||||||
├── test_buttons.c — Test_But_1/2 (interactive)
|
├── 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_can.c — CAN loopback (HIL)
|
||||||
├── test_uart_ttl.c — UART TTL (HIL)
|
└── test_mqs.c — MQS Audio Out (interactive, in-run confirm)
|
||||||
├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL)
|
|
||||||
└── test_opto.c — Opto-in EXT_IN1/IN2 (HIL)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Порядок файлов в `tests/` — как в `CMakeLists.txt`. Порядок **выполнения**
|
||||||
|
> тестов определяется реестром `k_registry[]` в `test_runner.c`
|
||||||
|
> (см. [Матрица тестов](#матрица-тестов)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Граф зависимостей
|
### Граф зависимостей
|
||||||
|
|
@ -175,8 +183,9 @@ main.c
|
||||||
├── bsp_usb_cdc (USB CDC ACM, единственный транспорт)
|
├── bsp_usb_cdc (USB CDC ACM, единственный транспорт)
|
||||||
├── cli.c
|
├── cli.c
|
||||||
│ └── bsp_usb_cdc (read / write)
|
│ └── bsp_usb_cdc (read / write)
|
||||||
│ └── protocol.c (send_error, send_pong)
|
│ └── bsp_provisioning (bsp_prov_read_uid — для get_uid)
|
||||||
│ └── test_runner.c (run_single, run_all, on_confirm)
|
│ └── 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
|
├── protocol.c
|
||||||
│ └── cli.c (cli_send)
|
│ └── cli.c (cli_send)
|
||||||
│ └── bsp_tick (bsp_tick_get_ms — для uptime)
|
│ └── bsp_tick (bsp_tick_get_ms — для uptime)
|
||||||
|
|
@ -188,26 +197,28 @@ main.c
|
||||||
└── tests/*.c (тест-модули через реестр)
|
└── tests/*.c (тест-модули через реестр)
|
||||||
```
|
```
|
||||||
|
|
||||||
**BSP-зависимости тест-модулей:**
|
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
||||||
|
|
||||||
| Тест | BSP модуль |
|
| Тест | BSP модуль |
|
||||||
| --------------- | ------------------------------ |
|
| -------------- | ------------------------------ |
|
||||||
| `test_sdram` | `bsp_sdram` |
|
| `test_sdram` | `bsp_sdram` |
|
||||||
| `test_qspi` | `bsp_qspi` |
|
| `test_qspi` | `bsp_qspi_flash` |
|
||||||
| `test_usd` | `bsp_usd` |
|
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
||||||
| `test_display` | существующий display BSP |
|
| `test_display` | `bsp_display` |
|
||||||
| `test_buttons` | `bsp_button` ✅ |
|
| `test_buttons` | `bsp_button` |
|
||||||
| `test_can` | `bsp_can` ✅ |
|
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
|
||||||
| `test_uart_ttl` | `bsp_uart_host` ✅ |
|
| `test_can` | `bsp_can` |
|
||||||
| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ |
|
| `test_mqs` | `bsp_mqs` |
|
||||||
| `test_opto` | `bsp_opto` ✅ |
|
|
||||||
|
> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного
|
||||||
|
> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### State machine test_runner
|
### State machine test_runner
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmd: run / run_all
|
cmd: run / run_all / run_selected
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
|
|
@ -232,9 +243,9 @@ main.c
|
||||||
confirmed=false → SKIP │ │
|
confirmed=false → SKIP │ │
|
||||||
timeout → SKIP │ │
|
timeout → SKIP │ │
|
||||||
│ │
|
│ │
|
||||||
run: IDLE ──┘ │
|
run: IDLE ──┘ │
|
||||||
run_all: следующий тест в реестре ───┘
|
run_all / run_selected: следующий тест ───┘
|
||||||
run_all done: protocol_send_summary()
|
done: protocol_send_summary()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ключевые свойства state machine:**
|
**Ключевые свойства state machine:**
|
||||||
|
|
@ -242,10 +253,12 @@ main.c
|
||||||
- `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`.
|
- `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`.
|
||||||
Пока тест выполняется, новые команды получают `BUSY`.
|
Пока тест выполняется, новые команды получают `BUSY`.
|
||||||
- `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов
|
- `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 приходит без возврата в главный цикл.
|
USB-стек остаётся живым, confirm приходит без возврата в главный цикл.
|
||||||
- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают
|
- `run_selected` работает по той же машине над маской выбранных тестов
|
||||||
`SKIP` немедленно, `summary.overall = "fail"`.
|
(`g_s_selected[]`); порядок — по реестру, не по порядку в запросе.
|
||||||
|
- `critical=true` + `FAIL` в `run_all`/`run_selected` → все оставшиеся тесты
|
||||||
|
получают `SKIP` немедленно, `summary.overall = "fail"`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -258,7 +271,7 @@ main.c
|
||||||
| Интерфейс | USB CDC ACM, разъём J2 |
|
| Интерфейс | USB CDC ACM, разъём J2 |
|
||||||
| Кодировка | UTF-8 |
|
| Кодировка | UTF-8 |
|
||||||
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
|
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
|
||||||
| Максимальная длина строки | 128 байт включая `\n` |
|
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
|
||||||
| CR+LF | Принимается (таргет отбрасывает `\r`) |
|
| CR+LF | Принимается (таргет отбрасывает `\r`) |
|
||||||
|
|
||||||
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
|
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
|
||||||
|
|
@ -273,7 +286,7 @@ main.c
|
||||||
│ │
|
│ │
|
||||||
│ [USB SDP: прошивка загружена] │
|
│ [USB SDP: прошивка загружена] │
|
||||||
│ [CDC ACM: порт открыт] │
|
│ [CDC ACM: порт открыт] │
|
||||||
│◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически
|
│◄─── {"type":"session_start","fw":"0.1.2",...} │ автоматически
|
||||||
│ │
|
│ │
|
||||||
│──── {"type":"cmd","cmd":"ping"} ─────────────►│
|
│──── {"type":"cmd","cmd":"ping"} ─────────────►│
|
||||||
│◄─── {"type":"pong"} │
|
│◄─── {"type":"pong"} │
|
||||||
|
|
@ -290,7 +303,8 @@ main.c
|
||||||
```
|
```
|
||||||
|
|
||||||
`session_start` отправляется **автоматически** при каждом старте, до получения
|
`session_start` отправляется **автоматически** при каждом старте, до получения
|
||||||
первой команды. Хост должен быть готов принять его сразу после открытия порта.
|
первой команды. Хост должен быть готов принять его сразу после открытия порта
|
||||||
|
(либо не полагаться на него — фикстуры HIL проверяют живость через `ping`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -310,7 +324,7 @@ main.c
|
||||||
```json
|
```json
|
||||||
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
||||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
← {"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` не найден:
|
Если `id` не найден:
|
||||||
|
|
@ -324,13 +338,58 @@ main.c
|
||||||
```json
|
```json
|
||||||
→ {"type":"cmd","cmd":"run_all"}
|
→ {"type":"cmd","cmd":"run_all"}
|
||||||
← {"type":"test_begin","id":"sdram",...}
|
← {"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_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"}
|
← {"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` — ответ оператора на интерактивный шаг
|
#### `confirm` — ответ оператора на интерактивный шаг
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -347,35 +406,19 @@ main.c
|
||||||
#### `session_start`
|
#### `session_start`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type":"session_start", "fw":"0.1.2", "target":"IMXRT1052", "uptime_ms":0 }
|
||||||
"type": "session_start",
|
|
||||||
"fw": "0.1.0",
|
|
||||||
"target": "IMXRT1052",
|
|
||||||
"uptime_ms": 0
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `test_begin`
|
#### `test_begin`
|
||||||
|
|
||||||
```json
|
```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`
|
#### `test_result`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type":"test_result", "id":"sdram", "status":"pass", "ms":15304, "detail":"" }
|
||||||
"type": "test_result",
|
|
||||||
"id": "sdram",
|
|
||||||
"status": "pass",
|
|
||||||
"ms": 312,
|
|
||||||
"detail": ""
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| `status` | Смысл |
|
| `status` | Смысл |
|
||||||
|
|
@ -384,37 +427,39 @@ main.c
|
||||||
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
|
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
|
||||||
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
|
| `"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`
|
#### `confirm_request`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type":"confirm_request", "id":"display_red", "prompt":"Screen is solid red?", "timeout_ms":15000 }
|
||||||
"type": "confirm_request",
|
|
||||||
"id": "display_red",
|
|
||||||
"prompt": "Экран залит красным цветом?",
|
|
||||||
"timeout_ms": 15000
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете.
|
Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете.
|
||||||
Хост может дублировать countdown для UX.
|
Хост может дублировать countdown для UX.
|
||||||
|
|
||||||
|
#### `uid_response` / `version_response`
|
||||||
|
|
||||||
|
Ответы на `get_uid` / `get_version` (см. соответствующие команды выше).
|
||||||
|
|
||||||
#### `summary`
|
#### `summary`
|
||||||
|
|
||||||
```json
|
```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":"fail"` — если хотя бы один `critical` тест провален.
|
||||||
`"overall":"pass"` — все `critical` тесты прошли (non-critical могут fail).
|
`"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":"UNKNOWN_TEST"} — "id" не найден в реестре
|
||||||
← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт
|
← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт
|
||||||
← {"ok":false,"error":"BUSY"} — таргет выполняет тест
|
← {"ok":false,"error":"BUSY"} — таргет выполняет тест
|
||||||
|
← {"ok":false,"error":"UID_READ_ERR"} — bsp_prov_read_uid() вернул ошибку
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Интерактивные тесты
|
### Интерактивные тесты
|
||||||
|
|
||||||
#### uSD — вставить карту
|
#### microSD — вставить карту (pre-confirm)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000}
|
← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000}
|
||||||
→ {"type":"confirm","id":"usd_insert","confirmed":true}
|
→ {"type":"confirm","id":"usd","confirmed":true}
|
||||||
← {"type":"test_begin","id":"usd",...}
|
← {"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
|
#### Display RGB888 — подтвердить цвета и ротацию (in-run confirm)
|
||||||
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Display RGB888 — подтвердить цвета
|
Шесть шагов: Red → Green → Blue → White, затем два ротационных
|
||||||
|
(диагностика непропаянных LR/UD пинов). Тест прерывается на **первом**
|
||||||
Четыре шага R/G/B/W. Итог — AND всех подтверждений.
|
неподтверждённом шаге (короткое замыкание, не сбор всех ответов).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
← {"type":"test_begin","id":"display",...}
|
← {"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","id":"display_red","confirmed":true}
|
||||||
← {"type":"confirm_request","id":"display_green",...}
|
← {"type":"confirm_request","id":"display_green",...}
|
||||||
→ {"type":"confirm","id":"display_green","confirmed":true}
|
→ {"type":"confirm","id":"display_green","confirmed":true}
|
||||||
← {"type":"confirm_request","id":"display_blue",...}
|
← {"type":"confirm_request","id":"display_blue",...}
|
||||||
→ {"type":"confirm","id":"display_blue","confirmed":true}
|
→ {"type":"confirm","id":"display_blue","confirmed":true}
|
||||||
← {"type":"confirm_request","id":"display_white",...}
|
← {"type":"confirm_request","id":"display_white",...}
|
||||||
→ {"type":"confirm","id":"display_white","confirmed":false}
|
→ {"type":"confirm","id":"display_white","confirmed":true}
|
||||||
← {"type":"test_result","id":"display","status":"fail","ms":22103,
|
← {"type":"confirm_request","id":"display_rot0","prompt":"Screen: left RED, right BLUE?","timeout_ms":15000}
|
||||||
"detail":"display_white not confirmed"}
|
→ {"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:"<id> 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
|
```bash
|
||||||
← {"type":"test_begin","id":"buttons",...}
|
← {"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 от хоста]
|
[таргет ждёт bsp_button — без JSON confirm от хоста]
|
||||||
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000}
|
← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000}
|
||||||
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
|
← {"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 |
|
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
||||||
| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- |
|
|
||||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||||
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
|
| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- |
|
||||||
| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm |
|
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() |
|
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||||
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only |
|
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||||
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ |
|
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
|
||||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
|
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
|
||||||
| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ |
|
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
|
||||||
| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
|
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
|
||||||
|
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
|
||||||
|
|
||||||
**Типы confirm:**
|
**Типы confirm:**
|
||||||
|
|
||||||
- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`,
|
- **pre_confirm** — `test_runner` отправляет `confirm_request` до вызова `run()`,
|
||||||
ждёт JSON-ответ через state machine (асинхронно).
|
ждёт JSON-ответ через state machine (асинхронно). id = id теста.
|
||||||
- **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`,
|
- **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`,
|
||||||
блокируется до ответа (синхронно).
|
блокируется до ответа (синхронно).
|
||||||
- **prompt only** — `protocol_send_confirm_request()` отправляется как UI-подсказка,
|
- **prompt only** — `protocol_send_confirm_request()` как UI-подсказка,
|
||||||
хост не отвечает JSON, таргет ждёт физического события.
|
хост не отвечает JSON, таргет ждёт физического события.
|
||||||
|
- **авто (HIL)** — confirm генерирует не оператор, а хост-оркестратор, командуя
|
||||||
|
M5StampPLC (см. `README_TESTING.md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -540,13 +610,13 @@ static test_result_t test_foo_run(void)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const test_module_t k_test_foo = {
|
const test_module_t K_TEST_FOO = {
|
||||||
.id = "foo", /* короткий ASCII-ключ */
|
.id = "foo", /* короткий ASCII-ключ */
|
||||||
.name = "Foo Peripheral",
|
.name = "Foo Peripheral",
|
||||||
.critical = false, /* true → run_all стопится при fail */
|
.critical = false, /* true → run_all стопится при fail */
|
||||||
.requires_hil = false, /* true → нужен M5StampPLC */
|
.requires_hil = false, /* true → нужен M5StampPLC */
|
||||||
.pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */
|
.pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */
|
||||||
.init = NULL, /* bsp_foo_init если нужен */
|
.init = NULL, /* bsp_foo_init если нужен */
|
||||||
.run = test_foo_run,
|
.run = test_foo_run,
|
||||||
.deinit = NULL,
|
.deinit = NULL,
|
||||||
};
|
};
|
||||||
|
|
@ -558,15 +628,19 @@ const test_module_t k_test_foo = {
|
||||||
|
|
||||||
```c
|
```c
|
||||||
/* Forward declarations */
|
/* Forward declarations */
|
||||||
extern const test_module_t k_test_sdram;
|
extern const test_module_t K_TEST_SDRAM;
|
||||||
extern const test_module_t k_test_foo; /* ← добавить */
|
extern const test_module_t K_TEST_FOO; /* ← добавить */
|
||||||
|
|
||||||
static const test_module_t *const k_registry[] = {
|
static const test_module_t *const k_registry[] = {
|
||||||
&k_test_sdram,
|
&K_TEST_SDRAM,
|
||||||
&k_test_foo, /* ← добавить */
|
...
|
||||||
|
&K_TEST_FOO, /* ← добавить */
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> При росте реестра выше `TEST_REGISTRY_MAX_SIZE` (`test_module.h`) сборка
|
||||||
|
> упадёт на `_Static_assert` в `test_runner.c` — увеличить константу.
|
||||||
|
|
||||||
### Шаг 3 — Добавить в CMakeLists.txt
|
### Шаг 3 — Добавить в CMakeLists.txt
|
||||||
|
|
||||||
**Файл:** `firmware/test/CMakeLists.txt`
|
**Файл:** `firmware/test/CMakeLists.txt`
|
||||||
|
|
@ -578,7 +652,6 @@ add_executable(
|
||||||
src/cli.c
|
src/cli.c
|
||||||
src/protocol.c
|
src/protocol.c
|
||||||
src/test_runner.c
|
src/test_runner.c
|
||||||
src/tests/test_sdram.c
|
|
||||||
src/tests/test_foo.c # ← добавить
|
src/tests/test_foo.c # ← добавить
|
||||||
...
|
...
|
||||||
)
|
)
|
||||||
|
|
@ -592,24 +665,18 @@ target_link_libraries(
|
||||||
|
|
||||||
### Шаг 4 — Обновить матрицу тестов
|
### Шаг 4 — Обновить матрицу тестов
|
||||||
|
|
||||||
Добавить строку в таблицу в этом README.
|
Добавить строку в таблицу в этом README (и, если есть протокольный поток —
|
||||||
|
в `README_TESTING.md`).
|
||||||
|
|
||||||
### Шаблоны для разных типов тестов
|
### Шаблоны для разных типов тестов
|
||||||
|
|
||||||
#### Self-тест с инициализацией
|
#### Self-тест с инициализацией
|
||||||
|
|
||||||
```c
|
```c
|
||||||
static void test_foo_init(void)
|
static void test_foo_init(void) { bsp_foo_init(); }
|
||||||
{
|
static void test_foo_deinit(void) { bsp_foo_deinit(); }
|
||||||
bsp_foo_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_foo_deinit(void)
|
const test_module_t K_TEST_FOO = {
|
||||||
{
|
|
||||||
bsp_foo_deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const test_module_t k_test_foo = {
|
|
||||||
.id = "foo",
|
.id = "foo",
|
||||||
.init = test_foo_init,
|
.init = test_foo_init,
|
||||||
.run = test_foo_run,
|
.run = test_foo_run,
|
||||||
|
|
@ -622,7 +689,6 @@ const test_module_t k_test_foo = {
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "test_runner.h" /* test_runner_wait_confirm() */
|
#include "test_runner.h" /* test_runner_wait_confirm() */
|
||||||
#include "protocol.h" /* protocol_send_confirm_request() */
|
|
||||||
|
|
||||||
static test_result_t test_foo_run(void)
|
static test_result_t test_foo_run(void)
|
||||||
{
|
{
|
||||||
|
|
@ -650,13 +716,13 @@ static test_result_t test_foo_run(void)
|
||||||
#### Тест с pre_confirm (вставить карту, подключить кабель)
|
#### Тест с pre_confirm (вставить карту, подключить кабель)
|
||||||
|
|
||||||
```c
|
```c
|
||||||
const test_module_t k_test_foo = {
|
const test_module_t K_TEST_FOO = {
|
||||||
.id = "foo",
|
.id = "foo",
|
||||||
.pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK",
|
.pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK",
|
||||||
.run = test_foo_run,
|
.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)` |
|
| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` |
|
||||||
| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению |
|
| `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` — единственная точка правды о версии.
|
Текущая версия ПО задается с помощью `project(firmware_test VERSION X.Y.Z)`
|
||||||
Поле `"fw"` в `session_start` несёт эту строку.
|
в `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 рекламационной диагностики |
|
|
||||||
| Тесты атомарны | Инженер сам решает что проверять |
|
|
||||||
| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики |
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ static size_t parse_string_array(const char *p_array_start, char (*p_out_bufs)[T
|
||||||
{
|
{
|
||||||
return 0U;
|
return 0U;
|
||||||
}
|
}
|
||||||
p++; /* пропустить '[' */
|
p++;
|
||||||
|
|
||||||
size_t count = 0U;
|
size_t count = 0U;
|
||||||
while (count < max_items)
|
while (count < max_items)
|
||||||
|
|
@ -285,8 +285,6 @@ static void handle_cmd_run_selected(const char *p_line)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Обработать сообщение {"type":"cmd",...}.
|
* @brief Обработать сообщение {"type":"cmd",...}.
|
||||||
*
|
|
||||||
* Команды: ping → pong, run_all → test_runner, run → test_runner.
|
|
||||||
*/
|
*/
|
||||||
static void handle_cmd(const char *p_line)
|
static void handle_cmd(const char *p_line)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
void protocol_send_test_list(const test_module_t *const *p_pp_registry, size_t count)
|
||||||
{
|
{
|
||||||
/* Заголовок массива */
|
|
||||||
cli_send("{\"type\":\"test_list\",\"tests\":[");
|
cli_send("{\"type\":\"test_list\",\"tests\":[");
|
||||||
|
|
||||||
for (size_t i = 0U; i < count; i++)
|
for (size_t i = 0U; i < count; i++)
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ static volatile bool g_s_confirm_value;
|
||||||
static char g_s_pending_confirm_id[RUNNER_CONFIRM_ID_SIZE];
|
static char g_s_pending_confirm_id[RUNNER_CONFIRM_ID_SIZE];
|
||||||
static uint32_t g_s_confirm_deadline_ms;
|
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_passed;
|
||||||
static uint8_t g_s_failed;
|
static uint8_t g_s_failed;
|
||||||
static uint8_t g_s_skipped;
|
static uint8_t g_s_skipped;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
# firmware_test — Руководство по тестированию
|
# firmware_test — Руководство по тестированию
|
||||||
|
|
||||||
Тестовая прошивка входного контроля платы MIMXRT1052CVJ5B.
|
Тестовая прошивка входного контроля платы TFT индикатора.
|
||||||
Транспорт: USB CDC ACM (J2). Протокол: JSON-lines v2, один JSON-объект на строку.
|
Транспорт: USB CDC ACM (J2). Протокол: JSON-lines v2, один JSON-объект на строку.
|
||||||
Загружается в RAM через BootROM USB SDP — без предварительной прошивки загрузчика.
|
Загружается в 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":"ping"}` | Проверка канала |
|
||||||
| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID |
|
| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID |
|
||||||
| `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра |
|
| `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":"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}` | Ответ оператора на запрос |
|
| `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос |
|
||||||
|
|
||||||
### Исходящие события (target → host)
|
### Исходящие события (target → host)
|
||||||
|
|
@ -30,8 +37,10 @@
|
||||||
| `test_begin` | `id`, `name`, `critical` | Тест запущен |
|
| `test_begin` | `id`, `name`, `critical` | Тест запущен |
|
||||||
| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста |
|
| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста |
|
||||||
| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору |
|
| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору |
|
||||||
| `progress` | `test`, `step`, `status` | Прогресс внутри теста |
|
| `progress` | `test`, `step`, `status` | Прогресс внутри теста (usd) |
|
||||||
| `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` / `run_selected` |
|
| `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` |
|
| `pong` | — | Ответ на `ping` |
|
||||||
| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола |
|
| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола |
|
||||||
|
|
||||||
|
|
@ -39,12 +48,14 @@
|
||||||
|
|
||||||
**Коды ошибок в `error`:**
|
**Коды ошибок в `error`:**
|
||||||
|
|
||||||
| Код | Причина |
|
| Код | Причина |
|
||||||
| -------------- | --------------------------------------------------------------- |
|
| --------------- | --------------------------------------------------------------- |
|
||||||
| `BUSY` | Предыдущий тест ещё выполняется |
|
| `BUSY` | Предыдущий тест ещё выполняется |
|
||||||
| `UNKNOWN_TEST` | ID теста не найден в реестре |
|
| `UNKNOWN_TEST` | ID теста не найден в реестре |
|
||||||
| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) |
|
| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) |
|
||||||
| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда |
|
| `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`:
|
После подключения немедленно отправляет `session_start`:
|
||||||
|
|
||||||
```bash
|
```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)
|
### Проверка канала (ping)
|
||||||
|
|
@ -64,6 +75,19 @@
|
||||||
← {"type":"pong"}
|
← {"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 — контроль оперативной памяти
|
## SDRAM — контроль оперативной памяти
|
||||||
|
|
@ -179,7 +203,7 @@ sequenceDiagram
|
||||||
| Время выполнения | < 1 с после вставки карты |
|
| Время выполнения | < 1 с после вставки карты |
|
||||||
|
|
||||||
Оператор вставляет карту по запросу. Тест запускается только после подтверждения.
|
Оператор вставляет карту по запросу. Тест запускается только после подтверждения.
|
||||||
Отказ или таймаут 30 с → `SKIP`.
|
Отказ или таймаут 30 с → `SKIP`. confirm id для pre-confirm равен id теста (`usd`).
|
||||||
|
|
||||||
Пять шагов с `progress`-событиями: card detect → mount → write 4 KB → read/compare → unmount.
|
Пять шагов с `progress`-событиями: card detect → mount → write 4 KB → read/compare → unmount.
|
||||||
|
|
||||||
|
|
@ -246,7 +270,8 @@ sequenceDiagram
|
||||||
|
|
||||||
**pre-confirm отсутствует** — `test_begin` отправляется сразу после `run`.
|
**pre-confirm отсутствует** — `test_begin` отправляется сразу после `run`.
|
||||||
|
|
||||||
Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL):
|
Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL);
|
||||||
|
тест прерывается на первом неподтверждённом шаге:
|
||||||
|
|
||||||
- **Этап 1 (все дисплеи):** Red → Green → Blue → White
|
- **Этап 1 (все дисплеи):** Red → Green → Blue → White
|
||||||
- **Этап 2 (TFT7/8/10):** паттерн Red/Blue + горизонтальный флип — диагностика непропаянных LR/UD пинов
|
- **Этап 2 (TFT7/8/10):** паттерн Red/Blue + горизонтальный флип — диагностика непропаянных LR/UD пинов
|
||||||
|
|
@ -379,8 +404,9 @@ sequenceDiagram
|
||||||
| Тип | Interactive (in-run confirm) |
|
| Тип | Interactive (in-run confirm) |
|
||||||
| Время выполнения | ~4 с воспроизведение + до 15 с на confirm |
|
| Время выполнения | ~4 с воспроизведение + до 15 с на confirm |
|
||||||
|
|
||||||
Тест воспроизводит мелодию (~4 с: нота A4 затем E5) через MQS-выход
|
Тест воспроизводит мелодию (~4 с: нота A4 затем E5, стерео PCM16 44100 Гц)
|
||||||
(`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M.
|
через MQS-выход (`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M.
|
||||||
|
Буфер — статический в некэшируемой секции (OCRAM NonCacheable), L == R.
|
||||||
Оператор подтверждает слышимость тона.
|
Оператор подтверждает слышимость тона.
|
||||||
|
|
||||||
```mermaid
|
```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: <N>` | `f_mount()` вернул FRESULT N |
|
|
||||||
| `usd` | `write failed: <N>` | `f_write()` вернул FRESULT N |
|
|
||||||
| `usd` | `compare failed at offset <N>` | Данные после чтения не совпадают |
|
|
||||||
| `display` | `display init failed` | `bsp_display_init()` вернул ошибку |
|
|
||||||
| `display` | `<id> 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` | `<id> 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 <N>` | 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)
|
## Оптоизолированные входы (HIL)
|
||||||
|
|
||||||
| Параметр | Значение |
|
| Параметр | Значение |
|
||||||
|
|
@ -583,7 +468,8 @@ HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC н
|
||||||
| 6 | `opto_rs_inactive` | RLY2 OFF | `BSP_OPTO_CH_RS == INACTIVE` |
|
| 6 | `opto_rs_inactive` | RLY2 OFF | `BSP_OPTO_CH_RS == INACTIVE` |
|
||||||
|
|
||||||
HIL confirm полностью автоматический — TUI командует M5 и отправляет confirm
|
HIL confirm полностью автоматический — TUI командует M5 и отправляет confirm
|
||||||
без участия оператора.
|
без участия оператора. После confirm тест выжидает settle (~30 мс, перекрывает
|
||||||
|
debounce 10 мс) и читает состояние через `bsp_opto_force_read()`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
→ {"type":"cmd","cmd":"run","id":"opto"}
|
→ {"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: <N>` | `f_mount()` вернул FRESULT N |
|
||||||
|
| `usd` | `write failed: <N>` | `f_write()` вернул FRESULT N |
|
||||||
|
| `usd` | `compare failed at offset <N>` | Данные после чтения не совпадают |
|
||||||
|
| `display` | `display init failed` | `bsp_display_init()` вернул ошибку |
|
||||||
|
| `display` | `<id> 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` | `<id> 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 <N>` | 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
|
## HIL pytest — автоматическая верификация через firmware_test CDC
|
||||||
|
|
||||||
Два файла тестируют `test_opto` и `test_can` через реальный CDC-протокол v2.
|
Два файла тестируют `test_opto` и `test_can` через реальный CDC-протокол v2.
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,6 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# ── Проверить FCB ─────────────────────────────────────────────────────────
|
|
||||||
if not args.fcb.exists():
|
if not args.fcb.exists():
|
||||||
print(f" ❌ FCB not found: {args.fcb}", file=sys.stderr)
|
print(f" ❌ FCB not found: {args.fcb}", file=sys.stderr)
|
||||||
print(
|
print(
|
||||||
|
|
@ -174,7 +173,6 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# ── Найти HAB-образ ───────────────────────────────────────────────────────
|
|
||||||
hab_name = HAB_NAMES[args.firmware]
|
hab_name = HAB_NAMES[args.firmware]
|
||||||
hab_path = BUILD_DIR / args.build_type / hab_name
|
hab_path = BUILD_DIR / args.build_type / hab_name
|
||||||
|
|
||||||
|
|
@ -195,7 +193,6 @@ def main() -> int:
|
||||||
print(f" Target : {args.target}")
|
print(f" Target : {args.target}")
|
||||||
print(f" Frequency : {args.frequency} Hz\n")
|
print(f" Frequency : {args.frequency} Hz\n")
|
||||||
|
|
||||||
# ── Собрать объединённый образ ────────────────────────────────────────────
|
|
||||||
image = build_full_image(args.fcb, hab_path)
|
image = build_full_image(args.fcb, hab_path)
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|
@ -205,7 +202,6 @@ def main() -> int:
|
||||||
print(f"\n Dry run — image saved to {out}")
|
print(f"\n Dry run — image saved to {out}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# ── Записать во Flash через pyOCD ─────────────────────────────────────────
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
suffix=f"_{args.firmware}_full.bin", delete=False
|
suffix=f"_{args.firmware}_full.bin", delete=False
|
||||||
) as tmp:
|
) as tmp:
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,6 @@ def main() -> None:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Группа: что прошивать (взаимоисключающие варианты)
|
|
||||||
target_group = parser.add_mutually_exclusive_group()
|
target_group = parser.add_mutually_exclusive_group()
|
||||||
target_group.add_argument(
|
target_group.add_argument(
|
||||||
"--firmware",
|
"--firmware",
|
||||||
|
|
@ -420,9 +419,6 @@ def main() -> None:
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Валидация: --firmware требует --build-type (уже есть default, но запомним)
|
|
||||||
# --bin-path: build-type игнорируется
|
|
||||||
# --erase-chip: несовместим с --ram-only
|
|
||||||
if args.erase_chip and args.ram_only:
|
if args.erase_chip and args.ram_only:
|
||||||
parser.error("--erase-chip несовместим с --ram-only")
|
parser.error("--erase-chip несовместим с --ram-only")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# service-tui — TUI сервисного инженера
|
# service-tui — TUI сервисного инженера
|
||||||
|
|
||||||
TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе.
|
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
|
```bash
|
||||||
┌────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────┐
|
||||||
│ service_tool vX.Y.Z │
|
│ service_tool v0.2.0 │
|
||||||
│ │
|
│ │
|
||||||
│ [LOGO_ART] │
|
│ [LOGO_ART] │
|
||||||
│ │
|
│ │
|
||||||
│ Подключите плату индикатора к USB... ⠋ │
|
│ Подключите плату индикатора к USB... ⠋ │
|
||||||
│ │
|
│ │
|
||||||
│ [ ✕ Выйти из приложения ] │
|
│ [ ✕ Выйти из приложения ] │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Версия читается из `pyproject.toml` — при бампе версии мокап выше не нужно
|
||||||
|
обновлять руками, TUI подставит актуальную сама.
|
||||||
|
|
||||||
При потере соединения на любом другом экране сессия разрывается полностью —
|
При потере соединения на любом другом экране сессия разрывается полностью —
|
||||||
TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над
|
TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над
|
||||||
подсказкой на 4 секунды появляется строка `⚠ <причина>` (например,
|
подсказкой на 4 секунды появляется строка `⚠ <причина>` (например,
|
||||||
|
|
@ -56,37 +59,30 @@ TUI не пытается восстановить прежнее состоян
|
||||||
│ │
|
│ │
|
||||||
│ ████████████░░░░░░ ← без числового % │
|
│ ████████████░░░░░░ ← без числового % │
|
||||||
│ ┌────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
│ │ ▶ Прошивка: firmware_test │ │
|
│ │ ▶ Сборка HAB-образа (HabImage)... │ │
|
||||||
|
│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │
|
||||||
│ │ ... │ │
|
│ │ ... │ │
|
||||||
│ └────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────┘ │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Если после «Загрузить» появилась ошибка, а плата всё ещё видна на этом же
|
Лог виден постоянно (не только во время прошивки), прогресс-бар — только
|
||||||
экране** — это ожидаемо: логическая ошибка (не найден файл, не подошёл
|
во время активной операции (скрыт в простое), без числового `%` — только
|
||||||
формат) не выкидывает на экран ожидания, потому что плата физически
|
полоса и построчный лог в реальном времени. Панель выбора прошивки
|
||||||
подключена. Прочитайте сообщение в логе, поправьте выбор и нажмите
|
ограничена по высоте и скроллится сама, если разрастается (варианты
|
||||||
«Загрузить» ещё раз. На экран ожидания TUI переключает только при реальном
|
"Другое") — лог снизу гарантированно не сжимается меньше 6 строк.
|
||||||
физическом обрыве USB.
|
|
||||||
|
|
||||||
**"Другое" — для бинарников, собранных не в этом репозитории.** В
|
**"Другое" — для бинарников, собранных не в этом репозитории.** В
|
||||||
`custom_binaries/` кладётся бинарник — сырой (код + таблица векторов, без
|
`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без
|
||||||
FCB/IVT/DCD) либо уже готовый HAB-образ, в зависимости от источника. TUI
|
FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до сборки HAB).
|
||||||
сама достраивает недостающее на лету:
|
TUI сама собирает из него загружаемый образ на лету, in-process через
|
||||||
|
Python API `spsdk` (без вызова внешних CLI-утилит):
|
||||||
|
|
||||||
1. Собирает загружаемый HAB-образ (добавляет IVT, +DCD — если включён
|
1. `HabImage` (spsdk) — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM")
|
||||||
тумблер "Использует SDRAM")
|
2. в Flash пишется явный FCB под выбранную память платы (не тот же
|
||||||
2. В Flash пишется явный FCB под выбранную память платы (не тот же
|
|
||||||
auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для
|
auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для
|
||||||
W25Q256/512 он ненадёжен, см. `DEV_ARCH.md §8.4`)
|
W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`)
|
||||||
3. Образ прошивается стандартным адресом
|
3. образ прошивается с `0x60001000`, как обычно
|
||||||
|
|
||||||
**Нужен ли тумблер DCD — зависит от конкретного бинарника, не от того, в
|
|
||||||
каком виде он получен.** Одна и та же связка `bootloader + tft_app` не
|
|
||||||
требует DCD, а часть кастомных/легаси образов (например, старый загрузчик,
|
|
||||||
используемый на производстве) требует его независимо от формата файла. Если
|
|
||||||
не уверены, нужен ли конкретному образу DCD — уточните у того, кто его
|
|
||||||
предоставил, прежде чем прошивать.
|
|
||||||
|
|
||||||
**Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не
|
**Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не
|
||||||
нужно выставлять заново на каждой следующей плате: прошили одну, вынули
|
нужно выставлять заново на каждой следующей плате: прошили одну, вынули
|
||||||
|
|
@ -105,8 +101,8 @@ Production/Custom этот шаг не нужен).
|
||||||
│ Переведите плату в нормальный режим: │
|
│ Переведите плату в нормальный режим: │
|
||||||
│ BOOT_MOD_1 → GND → Reset │
|
│ BOOT_MOD_1 → GND → Reset │
|
||||||
│ │
|
│ │
|
||||||
│ Автопереход через: 40с │
|
|
||||||
│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │
|
│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │
|
||||||
|
│ Автопереход через: 40с │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -137,7 +133,6 @@ Production/Custom этот шаг не нужен).
|
||||||
```
|
```
|
||||||
|
|
||||||
Что важно знать:
|
Что важно знать:
|
||||||
|
|
||||||
- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать
|
- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать
|
||||||
все"/"Снять все".
|
все"/"Снять все".
|
||||||
- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена
|
- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена
|
||||||
|
|
@ -157,7 +152,7 @@ Production/Custom этот шаг не нужен).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Рабочие процессы сервисного инженера
|
## Рабочие процессы сервисника
|
||||||
|
|
||||||
### Диагностика (firmware_test уже прошит)
|
### Диагностика (firmware_test уже прошит)
|
||||||
|
|
||||||
|
|
@ -184,12 +179,11 @@ Production/Custom этот шаг не нужен).
|
||||||
Для плат старых ревизий и любых образов, собранных не в этом репозитории.
|
Для плат старых ревизий и любых образов, собранных не в этом репозитории.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
1. Положить бинарник (сырой или уже HAB, см. раздел выше) в custom_binaries/
|
1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/
|
||||||
(или в директорию из SERVICE_CUSTOM_BINARIES_DIR)
|
(или в директорию из SERVICE_CUSTOM_BINARIES_DIR)
|
||||||
2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
||||||
3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD, если
|
3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости
|
||||||
конкретно этот образ его требует (уточнить у источника файла)
|
4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB
|
||||||
4. Загрузить — TUI сама соберёт HAB-образ (если нужно) и запишет правильный FCB
|
|
||||||
5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже
|
5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже
|
||||||
подставлен, останется нажать «Загрузить»
|
подставлен, останется нажать «Загрузить»
|
||||||
```
|
```
|
||||||
|
|
@ -204,9 +198,30 @@ Production/Custom этот шаг не нужен).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Известные ограничения
|
||||||
|
|
||||||
|
- **Одна плата на столе одновременно.** В SDP/Flashloader-режиме плату
|
||||||
|
нельзя идентифицировать по UID — авто-прошивка по факту детекта без
|
||||||
|
подтверждения оператора убрала бы последний шанс заметить, что в руках
|
||||||
|
не та плата. Массового программирования (несколько плат параллельно)
|
||||||
|
нет и не планируется в этом виде — см. `docs/DEV_ARCH.md`, §8.
|
||||||
|
- **Циклический прогон тестов** (повторный автозапуск набора без ручного
|
||||||
|
нажатия на каждую итерацию) — отложен на пост-релиз, не входит в текущую
|
||||||
|
версию.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Конфигурация (`.env`)
|
## Конфигурация (`.env`)
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
# USB VID:PID — BootROM SDP (константы NXP, не менять)
|
||||||
|
BOOTROM_VID=1fc9
|
||||||
|
BOOTROM_PID=0130
|
||||||
|
|
||||||
|
# USB VID:PID — Flashloader (константы NXP, не менять)
|
||||||
|
FLASHLOADER_VID=15a2
|
||||||
|
FLASHLOADER_PID=0073
|
||||||
|
|
||||||
# USB VID:PID — firmware_test CDC (наше устройство)
|
# USB VID:PID — firmware_test CDC (наше устройство)
|
||||||
SERVICE_CDC_VID=1996
|
SERVICE_CDC_VID=1996
|
||||||
SERVICE_CDC_PID=00ad
|
SERVICE_CDC_PID=00ad
|
||||||
|
|
@ -215,7 +230,7 @@ SERVICE_CDC_PID=00ad
|
||||||
SERVICE_M5_VID=303a
|
SERVICE_M5_VID=303a
|
||||||
SERVICE_M5_PID=4001
|
SERVICE_M5_PID=4001
|
||||||
|
|
||||||
# Директория с кастомными бинарниками для FlashScreen → "Другое".
|
# Директория с сырыми кастомными бинарниками для FlashScreen → "Другое".
|
||||||
# По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом
|
# По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом
|
||||||
# с main.py в dev-режиме); создаётся автоматически при старте.
|
# с main.py в dev-режиме); создаётся автоматически при старте.
|
||||||
# SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries
|
# SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries
|
||||||
|
|
@ -224,19 +239,14 @@ SERVICE_M5_PID=4001
|
||||||
# Release временно нестабилен — по умолчанию Debug.
|
# Release временно нестабилен — по умолчанию Debug.
|
||||||
FIRMWARE_BUILD_TYPE=Debug
|
FIRMWARE_BUILD_TYPE=Debug
|
||||||
|
|
||||||
# Уровень логирования. По умолчанию INFO (плюс WARNING принудительно для
|
|
||||||
# шумных модулей spsdk/libusbsio). DEBUG — полный лог, включая построчные
|
|
||||||
# HID-дампы каждой команды spsdk (для диагностики проблем прошивки).
|
|
||||||
# SERVICE_LOG_LEVEL=DEBUG
|
|
||||||
|
|
||||||
# Опционально: путь к директории лога TUI
|
# Опционально: путь к директории лога TUI
|
||||||
# SERVICE_LOG_DIR=/tmp
|
# SERVICE_LOG_DIR=/tmp
|
||||||
```
|
|
||||||
|
|
||||||
> `.env` не загружается в упакованном (frozen) приложении — standalone-бинарь
|
# Уровень логирования. По умолчанию — INFO, при этом spsdk/libusbsio
|
||||||
> работает на встроенных значениях по умолчанию. Переменные окружения (не
|
# принудительно приглушены до WARNING. DEBUG включает полный дамп,
|
||||||
> `.env`-файл) по-прежнему действуют и во frozen-режиме, если их выставить
|
# включая сырые HID-пакеты spsdk (много строк на одну прошивку).
|
||||||
> перед запуском.
|
# SERVICE_LOG_LEVEL=DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -245,56 +255,55 @@ FIRMWARE_BUILD_TYPE=Debug
|
||||||
### Из монорепозитория (разработчик)
|
### Из монорепозитория (разработчик)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just host::service-setup # установить/обновить зависимости tools/production/
|
just host::service-setup # установить зависимости tools/production/
|
||||||
just host::service-tui # запустить TUI
|
just host::service-tui # запустить TUI
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standalone-бинарь (сервисник)
|
### Standalone-бинарь (сервисник)
|
||||||
|
|
||||||
Распаковать `service-tui-vX.Y.Z-<os>.zip` в любую директорию и запустить
|
|
||||||
`service_tui` (`service_tui.exe` на Windows). Файл самодостаточен — не
|
|
||||||
требует установленного Python, `uv`, драйверов (Zadig/WinUSB) или сетевого
|
|
||||||
доступа.
|
|
||||||
|
|
||||||
### Сборка релизного бандла (разработчик)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just build::hab-all-release # или hab-all-debug — собрать HAB-образы заранее
|
just host::package-tui
|
||||||
just host::package-tui # → tools/production/dist/service-tui-vX.Y.Z-<os>/
|
# → tools/production/dist/service-tui-vX.Y.Z-<os>/
|
||||||
```
|
```
|
||||||
|
|
||||||
Устройство бандла (`_internal/`, `firmware/`, `custom_binaries/`) и детали
|
Бандл (PyInstaller, onedir) самодостаточен — прошивка идёт напрямую через
|
||||||
сборки (`service_tui.spec`) — в [DEV_ARCH.md §14](DEV_ARCH.md#14-упаковка-pyinstaller-фаза-5).
|
spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` ни
|
||||||
|
субпроцессом, ни как-либо иначе. Не нужны ни Zadig, ни установленный
|
||||||
> Если на Windows `package-tui` падает с `Permission denied` на шаге
|
Python/uv на машине сервисника. Структура бандла и резолв путей во frozen —
|
||||||
> переименования — закройте запущенный `service_tui.exe` от предыдущей
|
см. [DEV_ARCH.md](docs/DEV_ARCH.md), §14.
|
||||||
> сборки и повторите (см. `DEV_ARCH.md §14.4`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
|
|
||||||
| Пакет | Версия | Назначение |
|
| Пакет | Версия | Назначение |
|
||||||
| --------------- | ------ | -------------------------------------------------------------------------------------- |
|
| --------------- | ------- | -------------------------------------------------------- |
|
||||||
| `textual` | ≥ 0.80 | TUI фреймворк |
|
| `textual` | ≥ 0.80 | TUI фреймворк |
|
||||||
| `pyserial` | ≥ 3.5 | USB CDC ACM (firmware_test) + Serial (M5StampPLC) |
|
| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial |
|
||||||
| `spsdk` | 3.7.0 | прошивка in-process: SDP, McuBoot, HabImage |
|
| `pyusb` | ≥ 1.0 | не используется в коде (детект SDP/CDC идёт через `spsdk`) — исторический остаток, кандидат на удаление из `pyproject.toml` |
|
||||||
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
| `spsdk` | 3.7.0 | прошивка: SDP/McuBoot (Flash, chip erase) + HabImage (сборка кастомных HAB), нативный HID (libusbsio) — без Zadig |
|
||||||
| `pyinstaller` | ≥ 6.0 | сборка standalone-бандла |
|
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
||||||
| `pyusb` | ≥ 1.0 | не используется текущей детект-логикой (см. `DEV_ARCH.md §2`), оставлен в зависимостях |
|
| `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
|
```bash
|
||||||
tools/production/service_tui.log ← по умолчанию (рядом с main.py в dev,
|
tools/production/service_tui.log ← по умолчанию (dev) / рядом с exe (frozen)
|
||||||
рядом с exe во frozen)
|
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env
|
||||||
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env / окружении
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Уровень по умолчанию: `INFO` для модулей приложения (`WARNING` для `textual`
|
Уровень по умолчанию — `INFO`; `textual`, `spsdk` и `libusbsio` понижены до
|
||||||
и принудительно для шумных модулей `spsdk`/`libusbsio`, которые на `DEBUG`
|
`WARNING` независимо от root (иначе прошивка даёт ~135 строк сырых
|
||||||
печатают построчные HID-дампы каждой команды). Полный `DEBUG` — через
|
HID-пакетов на одну операцию). `SERVICE_LOG_LEVEL=DEBUG` включает полный
|
||||||
`SERVICE_LOG_LEVEL=DEBUG`. TUI не пишет в stdout — Textual захватывает
|
DEBUG везде, включая эти модули — используется при разборе проблем на
|
||||||
терминал.
|
железе. TUI не пишет в stdout — Textual захватывает терминал.
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ class ServiceApp(App):
|
||||||
self.push_screen(WaitingScreen())
|
self.push_screen(WaitingScreen())
|
||||||
|
|
||||||
# ── Переходы между экранами ───────────────────────────────────────────────
|
# ── Переходы между экранами ───────────────────────────────────────────────
|
||||||
|
|
||||||
@on(WaitingScreen.DeviceDetected)
|
|
||||||
@on(WaitingScreen.DeviceDetected)
|
@on(WaitingScreen.DeviceDetected)
|
||||||
def _on_device_detected(self, event: WaitingScreen.DeviceDetected) -> None:
|
def _on_device_detected(self, event: WaitingScreen.DeviceDetected) -> None:
|
||||||
if event.mode == AppMode.FLASHING:
|
if event.mode == AppMode.FLASHING:
|
||||||
|
|
@ -97,7 +95,6 @@ class ServiceApp(App):
|
||||||
|
|
||||||
@on(DiagScreen.DiagDone)
|
@on(DiagScreen.DiagDone)
|
||||||
def _on_diag_done(self, event: DiagScreen.DiagDone) -> None:
|
def _on_diag_done(self, event: DiagScreen.DiagDone) -> None:
|
||||||
"""После диагностики — отключиться, вернуться в Waiting."""
|
|
||||||
self._disconnect()
|
self._disconnect()
|
||||||
self.switch_screen(WaitingScreen(disconnect_reason=event.reason))
|
self.switch_screen(WaitingScreen(disconnect_reason=event.reason))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,9 @@ from .models import TestInfo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Таймаут чтения одной строки (сек)
|
|
||||||
_READLINE_TIMEOUT_S = 0.1
|
_READLINE_TIMEOUT_S = 0.1
|
||||||
# Таймаут ping→pong при подключении (сек)
|
|
||||||
_PING_TIMEOUT_S = 5.0
|
_PING_TIMEOUT_S = 5.0
|
||||||
# Таймаут ожидания событий теста (сек) — длиннее самого долгого теста (SDRAM ~15 с)
|
# Длиннее самого долгого теста (SDRAM ~15 с)
|
||||||
_TEST_EVENT_TIMEOUT_S = 120.0
|
_TEST_EVENT_TIMEOUT_S = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import serial.tools.list_ports
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# VID/PID M5StampPLC
|
|
||||||
_M5_VID = int(os.environ.get("SERVICE_M5_VID", "0x303A"), 16)
|
_M5_VID = int(os.environ.get("SERVICE_M5_VID", "0x303A"), 16)
|
||||||
_M5_PID = int(os.environ.get("SERVICE_M5_PID", "0x4001"), 16)
|
_M5_PID = int(os.environ.get("SERVICE_M5_PID", "0x4001"), 16)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,10 @@ from .models import ConfirmRequest, TestResult, TestStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Задержки для HIL
|
|
||||||
_RELAY_ON_S = 0.15
|
_RELAY_ON_S = 0.15
|
||||||
_RELAY_OFF_S = 0.50
|
_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_RELAY_MAP: dict[str, tuple[int, bool]] = {
|
||||||
"opto_in1_active": (3, True),
|
"opto_in1_active": (3, True),
|
||||||
"opto_in1_inactive": (3, False),
|
"opto_in1_inactive": (3, False),
|
||||||
|
|
@ -66,7 +64,6 @@ _OPTO_RELAY_MAP: dict[str, tuple[int, bool]] = {
|
||||||
"opto_rs_inactive": (2, False),
|
"opto_rs_inactive": (2, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# CAN параметры
|
|
||||||
_CAN_RX_ID = 0x100
|
_CAN_RX_ID = 0x100
|
||||||
_CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
|
_CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||||
_CAN_TX_ID = 0x200
|
_CAN_TX_ID = 0x200
|
||||||
|
|
@ -80,13 +77,13 @@ _TIMEOUT_EVENT_TYPE = "_timeout"
|
||||||
|
|
||||||
|
|
||||||
class OrchestratorEventType(Enum):
|
class OrchestratorEventType(Enum):
|
||||||
TEST_BEGIN = auto() # тест начался
|
TEST_BEGIN = auto()
|
||||||
TEST_RESULT = auto() # тест завершился
|
TEST_RESULT = auto()
|
||||||
TEST_PROGRESS = auto() # внутришаговый прогресс долгого теста (usd и т.п.)
|
TEST_PROGRESS = auto() # внутришаговый прогресс долгого теста (usd и т.п.)
|
||||||
CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone)
|
CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone)
|
||||||
CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически
|
CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически
|
||||||
BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm)
|
BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm)
|
||||||
SUMMARY = auto() # итог всей сессии
|
SUMMARY = auto()
|
||||||
ERROR = auto() # ошибка протокола, M5, или обрыв по таймауту
|
ERROR = auto() # ошибка протокола, M5, или обрыв по таймауту
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ class TestListPanel(Widget):
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
# test_id → Checkbox для быстрого доступа
|
|
||||||
self._checkboxes: dict[str, Checkbox] = {}
|
self._checkboxes: dict[str, Checkbox] = {}
|
||||||
# test_id → True если HIL-тест недоступен без M5 (постоянное состояние,
|
# test_id → True если HIL-тест недоступен без M5 (постоянное состояние,
|
||||||
# не зависящее от прогона). Отдельно от Checkbox.disabled, который
|
# не зависящее от прогона). Отдельно от Checkbox.disabled, который
|
||||||
|
|
|
||||||
|
|
@ -173,9 +173,8 @@ class FlashScreen(Screen, ConnectionWatcherMixin):
|
||||||
self._stop_connection_watch()
|
self._stop_connection_watch()
|
||||||
|
|
||||||
def _check_sdp_present(self) -> bool:
|
def _check_sdp_present(self) -> bool:
|
||||||
# Не считаем потерей соединения, если идёт активная операция —
|
# Не считаем потерей соединения, если идёт активная операция — обрыв
|
||||||
# flash_usb.py сам обработает реальный обрыв через subprocess.
|
# в этом случае обнаружит и обработает сам flash_backend
|
||||||
# обрыв в этом случае обнаружит и обработает сам flash_backend
|
|
||||||
# (ConnectionLostError, см. Фазу 4), не watcher.
|
# (ConnectionLostError, см. Фазу 4), не watcher.
|
||||||
if self._flashing:
|
if self._flashing:
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -6,42 +6,41 @@
|
||||||
> взаимодействия с firmware/M5, экранную архитектуру Textual, известные
|
> взаимодействия с firmware/M5, экранную архитектуру Textual, известные
|
||||||
> особенности фреймворка.
|
> особенности фреймворка.
|
||||||
> Пользовательская документация (экраны, запуск, конфигурация,
|
> Пользовательская документация (экраны, запуск, конфигурация,
|
||||||
> рабочие процессы сервисника) — в [README.md](../README.md).
|
> рабочие процессы сервисника) — в [README.md](README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Структура проекта
|
## 1. Структура проекта
|
||||||
|
|
||||||
## 1. Структура проекта
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/production/
|
tools/production/
|
||||||
├── main.py ← точка входа
|
├── main.py ← точка входа: логирование (Р12) + ServiceApp().run()
|
||||||
├── pyproject.toml ← зависимости uv
|
├── pyproject.toml ← зависимости uv (включая spsdk==3.7.0)
|
||||||
├── dist/ ← дистрибутивы программы (PyInstaller)
|
|
||||||
├── uv.lock
|
├── uv.lock
|
||||||
├── service_tui.spec ← PyInstaller spec
|
├── service_tui.spec ← PyInstaller spec (Фаза 5, onedir)
|
||||||
├── custom_binaries/ ← runtime, создаётся автоматически;
|
├── custom_binaries/ ← runtime, gitignored, создаётся автоматически
|
||||||
│ сырые/готовые бинарники для FlashScreen → «Другое»
|
│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое»
|
||||||
└── app/ ← implicit namespace package
|
├── tests/
|
||||||
│
|
│ └── test_flash_backend.py ← unit-тесты flash_backend.py (45 тестов, без event loop)
|
||||||
│
|
├── spike/ ← Фаза 0, де-риск spsdk API (в релиз не идёт)
|
||||||
|
└── app/
|
||||||
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
||||||
├── app.tcss ← единый файл стилей для всех экранов
|
├── app.tcss ← единый файл стилей для всех экранов
|
||||||
├── models.py ← все типы данных (dataclass/Enum)
|
├── models.py ← все типы данных (dataclass/Enum)
|
||||||
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
|
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
|
||||||
├── firmware_client.py ← async USB CDC клиент firmware_test
|
├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8)
|
||||||
├── m5_client.py ← async M5StampPLC клиент
|
├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8)
|
||||||
├── flash_backend.py ← spsdk 3.7.0 in-process: SDP, McuBoot, HabImage
|
├── usb_ports.py ← resolve_serial_port() — резолв COM/tty по VID:PID (Р8)
|
||||||
├── flasher.py ← async-обёртка над flash_backend для Textual workers
|
├── flash_backend.py ← синхронное ядро прошивки: прямой spsdk API (McuBoot/SDP/HabImage),
|
||||||
├── usb_ports.py ← резолвер serial-портов по VID:PID
|
│ zero Textual/asyncio импортов, тестируется без event loop
|
||||||
|
├── flasher.py ← async-обёртка над flash_backend.py (asyncio.to_thread)
|
||||||
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
|
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
|
||||||
├── widgets/
|
├── widgets/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
||||||
└── screens/
|
└── screens/
|
||||||
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
||||||
├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер, кнопка «Выйти»
|
├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер причины возврата
|
||||||
├── flash.py ← FlashScreen — прошивка / chip erase
|
├── flash.py ← FlashScreen — прошивка / chip erase
|
||||||
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
||||||
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
||||||
|
|
@ -52,6 +51,15 @@ tools/production/
|
||||||
└── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown
|
└── 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. Концепция
|
## 2. Концепция
|
||||||
|
|
@ -63,31 +71,48 @@ graph LR
|
||||||
subgraph app["app/"]
|
subgraph app["app/"]
|
||||||
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
||||||
M5["m5_client.py\nSerial JSON-lines, UTF-8"]
|
M5["m5_client.py\nSerial JSON-lines, UTF-8"]
|
||||||
FL["flasher.py + 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"]
|
OR["orchestrator.py\nconfirm/progress/timeout router"]
|
||||||
end
|
end
|
||||||
TUI --> FC & M5 & FL & OR
|
TUI --> FC & M5 & FL & OR
|
||||||
|
FL --> FB
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Board["Плата TFT (MIMXRT1052)"]
|
subgraph Board["Плата TFT (MIMXRT1052)"]
|
||||||
FW["firmware_test\n(USB CDC)"]
|
FW["firmware_test\n(USB CDC)"]
|
||||||
ROM["BootROM SDP\n(1FC9:0130)"]
|
ROM["BootROM SDP\n(1FC9:0130)"]
|
||||||
|
FLD["Flashloader\n(15A2:0073, RAM-резидент)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph HIL["HIL стенд (опционально)"]
|
subgraph HIL["HIL стенд (опционально)"]
|
||||||
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
||||||
end
|
end
|
||||||
|
|
||||||
FC |"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
|
FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
|
||||||
FL |"spsdk (libusbsio HID)\nVID:PID 1FC9:0130 / 15A2:0073"| ROM
|
FB -->|"SDP.write_file + jump_and_run\n(spsdk.sdp)"| ROM
|
||||||
M5 |"JSON-lines\nSerial"| M5HW
|
ROM -.->|"загружает ivt_flashloader.bin"| FLD
|
||||||
|
FB -->|"McuBoot: erase/write_memory/reset\n(spsdk.mboot)"| FLD
|
||||||
|
M5 <-->|"JSON-lines\nSerial"| M5HW
|
||||||
M5HW -->|"RLY1–4"| Board
|
M5HW -->|"RLY1–4"| Board
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Детект USB:** `flash_backend.detect_sdp()`/`detect_cdc()` используют spsdk
|
> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` делегируют в
|
||||||
> напрямую (`SdpUSBInterface.scan()` / `MbootUSBInterface.scan()`, HID-транспорт
|
> `flash_backend.detect_sdp()`/`detect_cdc()` (Р7 — `spsdk`-сканеры
|
||||||
> через `libusbsio`). CDC firmware_test и M5StampPLC резолвятся через
|
> `SdpUSBInterface.scan()`/`serial.tools.list_ports`, БЕЗ `pyusb`: BootROM
|
||||||
> `pyserial` (`usb_ports.py::resolve_serial_port()`, `m5_client.py`).
|
> 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
|
WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC
|
||||||
|
|
||||||
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
||||||
FLASHING --> WAITING : Production/Custom прошит,\nили потеря USB (в простое ИЛИ во время операции)
|
FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое
|
||||||
|
|
||||||
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
||||||
|
|
||||||
|
|
@ -117,13 +142,6 @@ stateDiagram-v2
|
||||||
`ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на
|
`ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на
|
||||||
сообщения `DeviceDetected`/`FlashDone`/`DiagDone`.
|
сообщения `DeviceDetected`/`FlashDone`/`DiagDone`.
|
||||||
|
|
||||||
> **Важная деталь, не показанная на диаграмме**:
|
|
||||||
> `FLASHING --> WAITING` по стрелке «ошибка» срабатывает **только** при
|
|
||||||
> физическом обрыве USB (`FlashResult.connection_lost=True`). Логическая
|
|
||||||
> ошибка (файл не найден, битый custom-бинарь) — плата на месте, экран
|
|
||||||
> остаётся на `FLASHING` (нет перехода состояния вообще, поэтому на
|
|
||||||
> диаграмме это не отдельная стрелка). См. §6.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Обработка confirm_request
|
## 4. Обработка confirm_request
|
||||||
|
|
@ -163,7 +181,9 @@ flowchart TD
|
||||||
|
|
||||||
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно
|
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно
|
||||||
одним событием `SUMMARY` — настоящим от firmware или синтетическим
|
одним событием `SUMMARY` — настоящим от firmware или синтетическим
|
||||||
(`aborted: true`), если чтение порта оборвалось по таймауту.
|
(`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии
|
||||||
|
зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда
|
||||||
|
(исторический баг, см. `CHANGELOG.md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -191,49 +211,85 @@ flowchart TD
|
||||||
PID — при детекте ориентироваться на `just host::m5-scan`, а не на
|
PID — при детекте ориентироваться на `just host::m5-scan`, а не на
|
||||||
документацию, если она когда-либо разойдётся с кодом.
|
документацию, если она когда-либо разойдётся с кодом.
|
||||||
|
|
||||||
|
**Важно на будущее:** документация (`HIL_BENCH.md`/`HIL_HOW_TO.md`) местами не
|
||||||
|
успевает за изменениями `agent.py`. При любых будущих изменениях протокола
|
||||||
|
агента (новые команды, смена формата ответа) — сверяться напрямую через
|
||||||
|
`grep` по `tools/hil/m5/agent.py`, а не полагаться только на документацию.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Мониторинг соединения и разрыв сессии
|
## 6. Мониторинг соединения и разрыв сессии
|
||||||
|
|
||||||
|
### 6.1 Простой (`ConnectionWatcherMixin`)
|
||||||
|
|
||||||
`ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к
|
`ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к
|
||||||
`FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
`FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
||||||
|
|
||||||
- **На `FlashScreen`** — проверка приостановлена во время активной
|
- **На `FlashScreen`** — проверка приостановлена во время активной
|
||||||
прошивки/erase (`self._flashing == True`).
|
прошивки/erase (обрыв в этом случае обнаруживает сам `flash_backend.py`,
|
||||||
|
см. §6.2, — не watcher).
|
||||||
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов
|
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов
|
||||||
(обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не
|
(обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не
|
||||||
просто исчезновение устройства из списка).
|
просто исчезновение устройства из списка).
|
||||||
|
- При срабатывании — `ConnectionLost` message → экран постит
|
||||||
|
`FlashDone(success=False, target=None)` / `DiagDone(reason=...)` →
|
||||||
|
`ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает
|
||||||
|
на `WaitingScreen(disconnect_reason=...)`. `FlashDone` в этой ветке не несёт
|
||||||
|
`preset` — «липкий» выбор (см. §8) сохраняется отдельно, в момент нажатия
|
||||||
|
«Загрузить», а не при завершении прошивки.
|
||||||
|
- `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем
|
||||||
|
продолжает обычный автодетект.
|
||||||
|
|
||||||
**Три независимых механизма детекта обрыва**:
|
Архитектурное решение: **сессия никогда не восстанавливается** — после
|
||||||
|
|
||||||
1. **`ConnectionWatcherMixin` в простое** — периодический опрос шины.
|
|
||||||
2. **`flash_backend.py` во время активной операции** — spsdk бросает
|
|
||||||
`SPSDKConnectionError`/`SPSDKTimeoutError` (оба ловятся явным кортежем
|
|
||||||
`_CONNECTION_LOST_EXCEPTIONS` — `SPSDKTimeoutError` НЕ наследует
|
|
||||||
`SPSDKConnectionError`, оба - потомки `SPSDKError`
|
|
||||||
3. **Вариант B** — некоторые команды spsdk (`flash_erase_all`,
|
|
||||||
`write_memory` и т.п.) при таймауте не бросают исключение, а тихо
|
|
||||||
возвращают `False`. `_fail_command()` в этом случае сам проверяет
|
|
||||||
`_sdp_still_present()`: плата пропала с шины → `ConnectionLostError`;
|
|
||||||
плата на месте → обычная `FlashBackendError`.
|
|
||||||
|
|
||||||
Оба механизма 2 и 3 транслируются в `Flasher.flash()`/`erase_chip()` как
|
|
||||||
`FlashResult(ok: bool, connection_lost: bool)` — **не голый `bool`**. Это
|
|
||||||
принципиально для `FlashScreen`:
|
|
||||||
|
|
||||||
- `connection_lost=True` → `FlashDone(target=None, error_message=...)` →
|
|
||||||
`ServiceApp` переключает на `WaitingScreen(disconnect_reason=...)`.
|
|
||||||
- `connection_lost=False` → **экран не покидает себя**.
|
|
||||||
Плата физически на месте, сообщение об ошибке уже в `#flash-log`, кнопки
|
|
||||||
разблокированы (`_set_busy(False)`) — оператор может поправить выбор
|
|
||||||
(другой файл, другой вариант памяти) и повторить, не выдёргивая USB.
|
|
||||||
|
|
||||||
`WaitingScreen` показывает причину возврата баннером на 4 секунды, затем
|
|
||||||
продолжает обычный автодетект.
|
|
||||||
|
|
||||||
**Cессия никогда не восстанавливается** — после
|
|
||||||
разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует
|
разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует
|
||||||
заново с нуля.
|
диагностику с нуля.
|
||||||
|
|
||||||
|
### 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 Проблема
|
### 8.1 Проблема
|
||||||
|
|
||||||
Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются заранее
|
Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются
|
||||||
(`just build::hab-*`) и всегда идут на плату с W25Q128 — для них auto-config
|
`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 —
|
||||||
Flashloader достаточен. Для сторонних/легаси бинарников (старые платы,
|
для них auto-config Flashloader (`configure-memory 0xC0000007` →
|
||||||
W25Q256/512) это не так: auto-config Flashloader не документирован как
|
`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси
|
||||||
надёжный для 4-байтной адресации, а сами бинарники приходят «сырыми» (код +
|
бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader
|
||||||
таблица векторов, без FCB/IVT/DCD) либо уже готовым HAB-образом — зависит от
|
не документирован как надёжный для 4-байтной адресации, а сами бинарники
|
||||||
источника. Решение — собирать HAB на лету (если нужно) и писать FCB явно, а
|
приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат,
|
||||||
не полагаться на auto-config.
|
что `inputImageFile` в `hab_*.yaml` до сборки). Решение — собирать HAB
|
||||||
|
на лету и писать FCB явно, а не полагаться на auto-config.
|
||||||
|
|
||||||
### 8.2 Модели (`models.py`)
|
### 8.2 Модели (`models.py`)
|
||||||
|
|
||||||
|
|
@ -306,44 +363,76 @@ class FlashPreset:
|
||||||
одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/
|
одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/
|
||||||
памяти/DCD, нажал «Загрузить», вынул, вставил следующую.
|
памяти/DCD, нажал «Загрузить», вынул, вставил следующую.
|
||||||
|
|
||||||
**Нужен ли DCD — implementation-defined, зависит от конкретного бинарника,
|
Рассматривался отдельный режим «массовое программирование» (авто-прошивка
|
||||||
не от его формата (сырой/готовый HAB).** Правило «сырой → включить DCD,
|
по факту детекта SDP, без нажатия кнопки на каждую плату) — отклонён:
|
||||||
готовый HAB → выключить» **неверно как общее правило**: например, в связке
|
в SDP/Flashloader-режиме нет способа прочитать UID платы, авто-старт без
|
||||||
`bootloader + tft_app` сам `bootloader` не требует DCD, а часть кастомных
|
подтверждения оператора убирает последний шанс заметить, что в руках не та
|
||||||
бинарников (в т.ч. старый загрузчик, используемый на производстве) требует
|
плата. Оставлена только «липкая» память выбора (этот раздел).
|
||||||
DCD независимо от того, в каком виде получен файл. Оператор должен знать
|
|
||||||
по конкретному образу, инициализирует ли он SDRAM самостоятельно — TUI не
|
|
||||||
может определить это автоматически по содержимому файла.
|
|
||||||
|
|
||||||
### 8.3 Конвейер сборки (`flasher.py`)
|
### 8.3 Конвейер сборки — прямые вызовы spsdk (`flash_backend.py`)
|
||||||
|
|
||||||
```bash
|
Сборка HAB-образа и прошивка выполняются **in-process** через Python API
|
||||||
Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb)
|
`spsdk` — никакого subprocess/CLI (`nxpimage`/`sdphost`/`blhost`), в отличие
|
||||||
└── _flash_custom()
|
от dev-CLI `tools/host/flash_usb.py`, который остаётся отдельной,
|
||||||
├── _build_custom_hab(raw_bin, use_dcd, progress_cb)
|
независимой реализацией на тех же CLI-утилитах (см. §1, врезка про
|
||||||
│ └── flash_backend.build_custom_hab() — in-process spsdk API:
|
разделение dev-CLI/production-TUI).
|
||||||
│ Config (family=mimxrt1050, startAddress=0x60000000,
|
|
||||||
│ ivtOffset=0x1000, initialLoadSize=0x2000,
|
```
|
||||||
│ + DCDFilePath, если use_dcd) → HabImage.export()
|
Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) [flasher.py]
|
||||||
└── _run_flash_op(flash_backend.flash, hab_bin, fcb_path=...)
|
└── _flash_custom() [flasher.py]
|
||||||
временный HAB-образ удаляется после прошивки
|
├── asyncio.to_thread(flash_backend.build_custom_hab, raw_bin, use_dcd)
|
||||||
(finally: shutil.rmtree(hab_bin.parent))
|
│ ├── _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`) — один и тот же файл независимо от
|
`dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не
|
||||||
проекта (SEMC/SDRAM-init не зависит от того, что именно исполняется), простой
|
зависит от того, что именно исполняется), поэтому просто константный путь,
|
||||||
константный путь, без вариантов. Резолвится через `flash_backend._host_dcd_dir()`
|
без вариантов. Пути к data-блобам (`dcd.bin`, `*_fdcb.bin`,
|
||||||
— двухрежимный (dev/frozen), см. §14.
|
`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)`,
|
```python
|
||||||
буквальная запись 512-байтного FCB-блоба (tag `FCFB`), а не magic option word
|
def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
||||||
`0xF000000F`. Обязателен для кастомных бинарей — auto-config Flashloader
|
"""Пишет буквальный FCB-блоб (512 байт) в Flash[FLASH_BASE] (custom-бинари).
|
||||||
проверен только для W25Q128 (см. §Известные открытые вопросы).
|
|
||||||
|
|
||||||
Штатный путь (`firmware_test`/`bootloader`/`app` из `build/<Type>/`) не
|
См. flash_usb.py::write_fcb_explicit — nxpimage не кладёт FCB в HAB-образ,
|
||||||
затрагивается — использует auto-config, как и раньше.
|
поэтому для произвольных чипов нужен явный блоб под конкретный 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`)
|
### 8.5 UI (`flash.py`)
|
||||||
|
|
||||||
|
|
@ -359,13 +448,6 @@ Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb)
|
||||||
дополнительно защищён `min-height: 6` — лог гарантированно виден даже в
|
дополнительно защищён `min-height: 6` — лог гарантированно виден даже в
|
||||||
худшем случае.
|
худшем случае.
|
||||||
|
|
||||||
**Троттлинг лога:** прогресс-бар обновляется на каждом
|
|
||||||
событии `FlashProgress`, но `#flash-log` для фазы `write` пишет только при
|
|
||||||
пересечении 10%-границы — без этого запись HAB-образа даёт ~135 строк в лог
|
|
||||||
на одну прошивку. Первая строка фазы (`"Запись <имя> (<размер> байт)"`) всегда
|
|
||||||
проходит; остальные фазы (`configure`/`erase`/`fcb`/`reset`/`error`) логируются
|
|
||||||
без троттлинга — их и так немного.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Архитектура экранов
|
## 9. Архитектура экранов
|
||||||
|
|
@ -390,7 +472,7 @@ graph TB
|
||||||
subgraph Clients["Клиенты"]
|
subgraph Clients["Клиенты"]
|
||||||
FC["FirmwareClient"]
|
FC["FirmwareClient"]
|
||||||
M5["M5Client"]
|
M5["M5Client"]
|
||||||
FL["Flasher\n(async) + flash_backend\n(spsdk in-process)"]
|
FL["Flasher"]
|
||||||
end
|
end
|
||||||
|
|
||||||
WS -->|"DeviceDetected(FLASHING)"| FS
|
WS -->|"DeviceDetected(FLASHING)"| FS
|
||||||
|
|
@ -429,7 +511,7 @@ sequenceDiagram
|
||||||
|
|
||||||
OP->>TUI: запустить service_tui
|
OP->>TUI: запустить service_tui
|
||||||
TUI->>WS: push_screen()
|
TUI->>WS: push_screen()
|
||||||
WS->>WS: USB poll каждые 1.5 с
|
WS->>WS: spsdk/list_ports poll каждые 1.5 с
|
||||||
|
|
||||||
OP->>FW: подключить плату USB
|
OP->>FW: подключить плату USB
|
||||||
WS->>TUI: DeviceDetected(DIAGNOSING)
|
WS->>TUI: DeviceDetected(DIAGNOSING)
|
||||||
|
|
@ -489,7 +571,7 @@ sequenceDiagram
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Версионирование firmware и TUI
|
## 11. Версионирование firmware
|
||||||
|
|
||||||
`firmware_test` версионируется через CMake
|
`firmware_test` версионируется через CMake
|
||||||
(`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через
|
(`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через
|
||||||
|
|
@ -502,10 +584,7 @@ sequenceDiagram
|
||||||
отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из
|
отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из
|
||||||
`pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata`
|
`pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata`
|
||||||
сознательно не используется — проект не ставится как пакет
|
сознательно не используется — проект не ставится как пакет
|
||||||
(`tool.uv.package = false`), метаданных может не быть. Резолв
|
(`tool.uv.package = false`), метаданных может не быть.
|
||||||
`Path(__file__).resolve().parents[2] / "pyproject.toml"` одинаково корректен
|
|
||||||
в dev и frozen (относительный от модуля, а не абсолютный) — при условии, что
|
|
||||||
`service_tui.spec` кладёт `pyproject.toml` в корень бандла (см. §14).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -544,9 +623,7 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл
|
||||||
`mount_all()`.
|
`mount_all()`.
|
||||||
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня
|
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня
|
||||||
проекта — постоянно расходится при рефакторинге структуры. Решение: один
|
проекта — постоянно расходится при рефакторинге структуры. Решение: один
|
||||||
`CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`. Во frozen
|
`CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`.
|
||||||
дополнительно требует, чтобы `app.tcss` физически лежал в бандле по тому же
|
|
||||||
относительному пути (см. §14).
|
|
||||||
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает
|
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает
|
||||||
ширину по умолчанию равную длине заголовка — длинный контент обрезается
|
ширину по умолчанию равную длине заголовка — длинный контент обрезается
|
||||||
независимо от `height` строки. Нужно использовать `add_column(label,
|
независимо от `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/<Type>/*_hab.bin`
|
||||||
|
из `BUILD_DIR`, переименовывает результат в `dist/service-tui-vX.Y.Z-<os>/`
|
||||||
|
(версия — из `pyproject.toml`).
|
||||||
|
|
||||||
```bash
|
Целевая структура бандла:
|
||||||
|
|
||||||
|
```
|
||||||
service-tui-vX.Y.Z-<os>/
|
service-tui-vX.Y.Z-<os>/
|
||||||
├── service_tui[.exe]
|
├── service_tui[.exe]
|
||||||
├── _internal/
|
├── _internal/
|
||||||
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||||
│ └── ... ← рантайм PyInstaller, libusbsio (из Analysis)
|
│ └── ... ← рантайм PyInstaller, libusbsio
|
||||||
├── firmware/
|
├── firmware/
|
||||||
│ └── <Type>/firmware_test_hab.bin ← копируется post-build
|
│ └── <Type>/firmware_test_hab.bin
|
||||||
└── custom_binaries/ ← пустая, для оператора
|
└── custom_binaries/ ← пустая, создаётся оператором/автоматически
|
||||||
```
|
```
|
||||||
|
|
||||||
Два разных механизма наполнения — не взаимозаменяемы:
|
Каждый модуль, которому нужен путь к данным, сам решает dev vs frozen через
|
||||||
|
`getattr(sys, "frozen", False)` — единообразный паттерн по всему `app/`:
|
||||||
|
|
||||||
- **`_internal/data/`** — через `datas` в `service_tui.spec`
|
| Функция | Dev | Frozen |
|
||||||
(`collect_data_files("spsdk")` + `tools/host/dcd/*.bin`). Резолвится в
|
| --- | --- | --- |
|
||||||
рантайме через `sys._MEIPASS` (для onedir `_MEIPASS` == `_internal/`).
|
| `flash_backend._host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS/data` |
|
||||||
- **`firmware/`** — PyInstaller `datas` физически не может положить файл
|
| `flash_backend.firmware_hab_path()` | `$BUILD_DIR/<Type>/*_hab.bin` (репо `build/`) | `<exe_dir>/firmware/<Type>/*_hab.bin` |
|
||||||
вне `_internal/`, поэтому это отдельный **post-build copy-шаг** в
|
| `flasher._resolve_custom_binaries_dir()` | `tools/production/custom_binaries/` | `<exe_dir>/custom_binaries/` (override — `SERVICE_CUSTOM_BINARIES_DIR`) |
|
||||||
`just host::package-tui` (не часть `.spec`), копирующий `build/<Type>/*_hab.bin`
|
| `waiting._read_app_version()` | `tools/production/pyproject.toml` | тот же путь — `pyproject.toml` кладётся в `datas` спека (нужен для парсинга версии во frozen) |
|
||||||
в бандл. Резолвится в рантайме через `Path(sys.executable).resolve().parent`
|
| `main._setup_logging()` | рядом с `main.py` | рядом с исполняемым файлом (`sys.executable.parent`) |
|
||||||
(сиблинг exe, не `_MEIPASS`) — сознательный выбор: HAB-образы должны быть
|
|
||||||
легко заменяемы без пересборки бандла.
|
|
||||||
- **`custom_binaries/`** — создаётся дважды, независимо: приложением само
|
|
||||||
при первом запуске (`flasher.py::_resolve_custom_binaries_dir()`,
|
|
||||||
`mkdir(exist_ok=True)`) и заодно явно в `package-tui` (`mkdir -p` перед
|
|
||||||
финальным переименованием) — избыточно, но безвредно, бандл выглядит
|
|
||||||
«полным» ещё до первого запуска.
|
|
||||||
|
|
||||||
### 14.2 Двухрежимный резолв путей (`flash_backend.py`)
|
`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 |
|
> **Расхождение spec/факт:** закоммиченный `service_tui.spec` объявляет в
|
||||||
| -------------------------------------------------------------------- | -------------------------------- | ------------------------------------------- |
|
> `datas` только `('../shared', 'shared')` — без `dcd/*.bin`,
|
||||||
| `firmware_hab_path()` | `BUILD_DIR`/`build/<Type>/` | `sys.executable.parent / "firmware"` |
|
> `pyproject.toml` или `spsdk`-данных. Тем не менее уже собранные релизные
|
||||||
| `_host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS / "data"` |
|
> бандлы в `tools/production/dist/service-tui-v0.2.0-{macos,windows}/`
|
||||||
| `flashloader_bin_path()` / `real_dcd_bin_path()` / `fcb_blob_path()` | производные от `_host_dcd_dir()` | |
|
> фактически содержат `_internal/data/{dcd.bin,*_fdcb.bin,ivt_flashloader.bin}`,
|
||||||
| `_resolve_custom_binaries_dir()` (`flasher.py`) | рядом с `main.py` | `sys.executable.parent / "custom_binaries"` |
|
> `_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 ощутимо медленнее стартует (распаковка во
|
`main.py::_setup_logging()`:
|
||||||
временную директорию при каждом запуске).
|
|
||||||
- **`collect_data_files("spsdk")`** — обязателен, не перестраховка: ~380
|
|
||||||
файлов (`data/devices/*/database.yaml` и т.п.), которые реально резолвит
|
|
||||||
`HabImage`/`Config` для `family=mimxrt1050`.
|
|
||||||
- **`collect_dynamic_libs("libusbsio")`** — заберёт бинарники **всех**
|
|
||||||
поддерживаемых платформ (`bin/osx_arm64/`, `bin/x64/`, `bin/linux_*` и
|
|
||||||
т.д. — `rglob` без фильтра по текущей ОС). Не баг: сама `libusbsio.py`
|
|
||||||
резолвит нужный файл в рантайме по `platform.system()`/`platform.machine()`,
|
|
||||||
лишние платформы просто раздувают бандл. При необходимости можно
|
|
||||||
отфильтровать под текущую ОС отдельно.
|
|
||||||
- **`hiddenimports=["app", "app.app", "app.screens", "app.widgets"]`** —
|
|
||||||
явная подстраховка из-за отсутствия `__init__.py` в `app/` (см. §1).
|
|
||||||
Современный PyInstaller обычно справляется и без этого через анализ
|
|
||||||
импортов из `main.py`, но цена перестраховки нулевая.
|
|
||||||
- **`upx=False`** — сознательно, не дефолт PyInstaller: UPX-паковка вместе
|
|
||||||
с нативными HID-либами (libusbsio) — известный источник проблем с
|
|
||||||
загрузкой.
|
|
||||||
|
|
||||||
### 14.4 Известные грабли упаковки
|
- 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 шаге может упасть с
|
`FlashScreen._on_progress()` (Р11) троттлит **только** запись в
|
||||||
`Permission denied`**, если целевая директория из предыдущей сборки ещё
|
`#flash-log` для фазы `write`: событие логируется раз на каждые 10%
|
||||||
содержит заблокированный файл (например, `service_tui.exe` от прошлого
|
(`progress.percent // 10`), а не на каждый пакет `spsdk` (~135 →
|
||||||
запуска, не закрытый перед повторной упаковкой, либо антивирус временно
|
~10 строк). Прогресс-бар при этом обновляется на **каждом** событии —
|
||||||
удерживает хендл на свежесозданном `.exe`). Симптом: сообщение об ошибке
|
плавность не теряется, троттлинг влияет только на текстовый лог.
|
||||||
показывает путь **вложенным** (`dist/service-tui-vX.Y.Z-windows/service_tui`)
|
|
||||||
— это Unix-семантика `mv` в существующую директорию, сигнал, что `rm -rf`
|
|
||||||
не до конца очистил цель. Лечится закрытием запущенного exe перед повторной
|
|
||||||
упаковкой.
|
|
||||||
- **`just` + bash-shebang рецепты на Windows** — на некоторых машинах поиск
|
|
||||||
`bash` через PATH может резолвиться в `C:\Windows\System32\bash.exe`
|
|
||||||
(WSL-заглушка) вместо Git Bash, если WSL сконфигурирован некорректно —
|
|
||||||
проявляется как `WSL (...) ERROR: execve(/bin/bash) failed`. Специфично
|
|
||||||
для конкретной машины/PATH, не для рецепта — решается на уровне окружения
|
|
||||||
(порядок PATH, состояние WSL), не в `Justfile`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Известные открытые вопросы
|
## Известные открытые вопросы
|
||||||
|
|
||||||
- **Release-сборка firmware нестабильна** : работает только с оптимизацией уровня 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
|
- **`tools/shared/m5_agent.py`** — сознательно не делался: pytest
|
||||||
HIL-окружение и TUI используют независимые M5-клиенты, признано правильным
|
HIL-окружение и TUI используют независимые M5-клиенты, признано правильным
|
||||||
архитектурным решением, а не техдолгом. (Устаревшая `just host::service-build`
|
архитектурным решением, а не техдолгом.
|
||||||
ссылается на несуществующий `tools/shared/` через `--add-data` — рецепт,
|
|
||||||
скорее всего, нерабочий, кандидат на удаление в пользу `package-tui`.)
|
|
||||||
- Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и
|
- Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и
|
||||||
«копирование UID с экрана» — отложены, не начаты.
|
«копирование UID с экрана» — отложены, не начаты.
|
||||||
|
- **POST-1 (циклический прогон неинтерактивных тестов на DiagScreen)** —
|
||||||
|
сознательно отложен на пост-релиз, вне `MONOLITH_APP_PLAN.md` (см.
|
||||||
|
`RELEASE_ROADMAP.md`).
|
||||||
- **Массовое программирование** — решено НЕ делать авто-прошивку по факту
|
- **Массовое программирование** — решено НЕ делать авто-прошивку по факту
|
||||||
детекта SDP (см. §8.2); ограничились «липким» `FlashPreset`. Если в будущем
|
детекта SDP (см. §8.2); ограничились «липким» `FlashPreset`. Если в будущем
|
||||||
понадобится полный батч-режим — потребуется отдельный предохранитель
|
понадобится полный батч-режим — потребуется отдельный предохранитель
|
||||||
(задержка с отменой перед стартом), т.к. в SDP-режиме плату нельзя
|
(задержка с отменой перед стартом), т.к. в SDP-режиме плату нельзя
|
||||||
идентифицировать по UID.
|
идентифицировать по UID. Связанное ограничение v1 (О3) — предполагается
|
||||||
|
ровно одна плата на столе одновременно (см. README).
|
||||||
- **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили
|
- **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили
|
||||||
не полагаться на него вообще, для кастомных бинарей FCB всегда пишется
|
не полагаться на него вообще, для кастомных бинарей FCB всегда пишется
|
||||||
явно (§8.4). Остаётся не до конца понятым, работает ли
|
явно (параметр `fcb_path` в `flash_backend.flash()`, см. §8.4). Остаётся не
|
||||||
`configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос
|
до конца понятым, работает ли `configure-memory 0xF000000F` для этих
|
||||||
снят с повестки архитектурным решением, а не исследован до конца.
|
чипов корректно в принципе — вопрос снят с повестки архитектурным
|
||||||
|
решением, а не исследован до конца.
|
||||||
|
|
||||||
467
tools/production/docs/RELEASE_ROADMAP.md
Normal file
467
tools/production/docs/RELEASE_ROADMAP.md
Normal file
|
|
@ -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/<Type>/firmware_test_hab.bin` (Type из `.env`, О2);
|
||||||
|
- `datas`: `pyproject.toml` (для `_read_app_version` во frozen);
|
||||||
|
- onedir (не onefile — onefile замедляет старт распаковкой);
|
||||||
|
- резолвер путей backend'а уже готов: frozen → `sys.executable.parent`
|
||||||
|
(`firmware_hab_path`, `_resolve_custom_binaries_dir`).
|
||||||
|
|
||||||
|
Целевая структура бандла:
|
||||||
|
|
||||||
|
```
|
||||||
|
service-tui-vX.Y.Z-<os>/
|
||||||
|
├── service_tui[.exe]
|
||||||
|
├── _internal/
|
||||||
|
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||||
|
│ └── ... ← рантайм PyInstaller, libusbsio
|
||||||
|
├── firmware/
|
||||||
|
│ └── <Type>/firmware_test_hab.bin
|
||||||
|
└── custom_binaries/ ← пустая
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI-полировка (Предложение 2)
|
||||||
|
|
||||||
|
Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично
|
||||||
|
`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`).
|
||||||
|
|
||||||
|
### Гейт 5 (Windows + macOS)
|
||||||
|
|
||||||
|
- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный
|
||||||
|
полевой цикл — детект SDP → firmware_test → диагностика →
|
||||||
|
custom (W25Q128 и W25Q512) → chip erase.
|
||||||
|
- [ ] То же на macOS.
|
||||||
|
- [ ] Версия на `WaitingScreen` корректна во frozen.
|
||||||
|
- [ ] Порты резолвятся при перетыкании в другой физический USB-порт
|
||||||
|
(проверка Р8 на обеих ОС).
|
||||||
|
- [ ] Кнопка «Выйти» на `WaitingScreen` работает.
|
||||||
|
- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны —
|
||||||
|
подтверждено О1) виден во frozen-бандле.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз
|
||||||
|
|
||||||
|
**Цель:** синхронизировать документацию с реальностью монолита,
|
||||||
|
провести отложенную зачистку комментариев/grep, собрать релизный
|
||||||
|
артефакт из тега.
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
|
||||||
|
| Файл | Тип правки |
|
||||||
|
| --- | --- |
|
||||||
|
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) |
|
||||||
|
| `CHANGELOG.md` | правки |
|
||||||
|
| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap |
|
||||||
|
| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) |
|
||||||
|
| `HOW_TO_FLASH.md` | правки |
|
||||||
|
| `tools/production/README.md` | правки |
|
||||||
|
| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||||
|
|
||||||
|
### Содержание
|
||||||
|
|
||||||
|
1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход
|
||||||
|
по всему коду — актуализировать docstring-провенансы («прямой порт
|
||||||
|
flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита.
|
||||||
|
Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто
|
||||||
|
в `app/`) — либо достигается, либо остаётся осознанно как
|
||||||
|
документация происхождения (решение по каждому вхождению).
|
||||||
|
2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess),
|
||||||
|
нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8),
|
||||||
|
нативная обработка отвала USB, упаковка одним exe.
|
||||||
|
3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план
|
||||||
|
RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1).
|
||||||
|
4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты),
|
||||||
|
`flash_backend.py` — production-TUI; независимые реализации (Р2).
|
||||||
|
5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk.
|
||||||
|
6. **Ограничение «одна плата на столе»** (О3) — в README.
|
||||||
|
7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как
|
||||||
|
запланированную пост-релизную фичу.
|
||||||
|
8. Тег релиза = версия из `pyproject.toml`.
|
||||||
|
|
||||||
|
### Гейт 6
|
||||||
|
|
||||||
|
- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5).
|
||||||
|
- [ ] `just host::flash*`, `incoming`, `production` работают как раньше —
|
||||||
|
регрессия dev-пути.
|
||||||
|
- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на
|
||||||
|
релизном бинаре.
|
||||||
|
- [ ] POST-1 зафиксирован в бэклоге, не потерян.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сводная последовательность и зависимости
|
||||||
|
|
||||||
|
```
|
||||||
|
4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ └── доки, зачистка, тег, регрессия dev-пути
|
||||||
|
│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting
|
||||||
|
│ └── уровни логов + троттлинг #flash-log
|
||||||
|
└── типизация обрыва (SPSDKTimeoutError + erase вариант B)
|
||||||
|
|
||||||
|
Блокеры перед фазами:
|
||||||
|
4a: нет — старт сразу
|
||||||
|
4b: нет — после 4a
|
||||||
|
5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG
|
||||||
|
6: все гейты 4a/4b/5 зелёные на железе
|
||||||
|
```
|
||||||
|
|
||||||
|
## Открытые мелочи (согласовать по ходу, не блокируют старт 4a)
|
||||||
|
|
||||||
|
| Вопрос | Когда нужен | Предложение |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) |
|
||||||
|
| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю |
|
||||||
|
| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-<os>` (из плана) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Риски этого этапа
|
||||||
|
|
||||||
|
| Риск | Фаза | Митигация |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает |
|
||||||
|
| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель |
|
||||||
|
| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле |
|
||||||
|
| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` |
|
||||||
|
| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Приложение: работа в новом треде
|
||||||
|
|
||||||
|
Этот roadmap рассчитан на продолжение в **новом чате без контекста**
|
||||||
|
предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы
|
||||||
|
новый тред стартовал без потерь.
|
||||||
|
|
||||||
|
## A. Какой набор правил к чему применяется
|
||||||
|
|
||||||
|
Проектные правила «Role & Hardware Context» (senior embedded C, i.MX
|
||||||
|
RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`,
|
||||||
|
CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`).
|
||||||
|
|
||||||
|
**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в
|
||||||
|
`tools/production`. Поэтому:
|
||||||
|
|
||||||
|
| Правило | Применимо к Python-работе roadmap? |
|
||||||
|
| --- | --- |
|
||||||
|
| Unified diffs, не полные переписывания | ✅ Да |
|
||||||
|
| «Какой файл / какая функция затронуты» — первым | ✅ Да |
|
||||||
|
| ASK при неоднозначности/противоречии | ✅ Да |
|
||||||
|
| Не изобретать just-таски / пути / структуру | ✅ Да |
|
||||||
|
| Проверять существующие файлы перед правкой | ✅ Да |
|
||||||
|
| No malloc/free в драйверах и ISR | ❌ C-специфично |
|
||||||
|
| NXP SDK HAL вместо raw-регистров | ❌ C-специфично |
|
||||||
|
| Doxygen на public API | ❌ (Python — docstrings, уже используются) |
|
||||||
|
| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) |
|
||||||
|
| CMake target_compile_options | ❌ Неприменимо |
|
||||||
|
|
||||||
|
Когда/если roadmap коснётся C-части — C-правила снова в силе.
|
||||||
|
|
||||||
|
## B. Первый вопрос на старте нового треда (не потерять)
|
||||||
|
|
||||||
|
**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip`
|
||||||
|
предлагается обернуть в `try/except`, и **любую ошибку самой проверки**
|
||||||
|
(не только «устройство отсутствует») трактовать как обрыв — потому что
|
||||||
|
проверка и так выполняется только после уже случившегося сбоя, шина
|
||||||
|
нестабильна, и «не смог проверить» практически всегда означает «платы
|
||||||
|
нет». Требуется явное подтверждение этой трактовки перед написанием
|
||||||
|
кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный
|
||||||
|
`FlashBackendError`.)
|
||||||
|
|
||||||
|
## C. Файлы, которые нужно предоставить — по фазам
|
||||||
|
|
||||||
|
Пути относительно `tools/production/`, если не указано иное. Пометка
|
||||||
|
**[есть в этом треде]** — файл уже фигурировал и его актуальная версия
|
||||||
|
известна; в новом треде его всё равно нужно приложить заново.
|
||||||
|
|
||||||
|
### Фаза 4a — типизация обрыва
|
||||||
|
|
||||||
|
| Файл | Зачем |
|
||||||
|
| --- | --- |
|
||||||
|
| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B |
|
||||||
|
| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию |
|
||||||
|
| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) |
|
||||||
|
| `app/models.py` | контекст: `FlashProgress` |
|
||||||
|
|
||||||
|
### Фаза 4b — логи
|
||||||
|
|
||||||
|
| Файл | Зачем |
|
||||||
|
| --- | --- |
|
||||||
|
| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) |
|
||||||
|
| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) |
|
||||||
|
| `.env` / `.env.example` (`tools/production/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными |
|
||||||
|
|
||||||
|
### Фаза 5 — упаковка PyInstaller + UI
|
||||||
|
|
||||||
|
| Файл | Зачем |
|
||||||
|
| --- | --- |
|
||||||
|
| `pyproject.toml` (`tools/production/`) | зависимости, версия, `requires-python` — база для spec |
|
||||||
|
| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта |
|
||||||
|
| `app/main.py` | entry point для PyInstaller |
|
||||||
|
| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen |
|
||||||
|
| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit |
|
||||||
|
| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) |
|
||||||
|
| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла |
|
||||||
|
| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) |
|
||||||
|
| `project_tree.txt` или `ls -R tools/production` | реальная структура пакета `app/` для spec |
|
||||||
|
| существующий `.spec`, если уже есть | не изобретать заново |
|
||||||
|
|
||||||
|
### Фаза 6 — документация и релиз
|
||||||
|
|
||||||
|
| Файл | Зачем |
|
||||||
|
| --- | --- |
|
||||||
|
| `CHANGELOG.md` | дописать секцию монолита |
|
||||||
|
| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap |
|
||||||
|
| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) |
|
||||||
|
| `HOW_TO_FLASH.md` | актуализировать под TUI-backend |
|
||||||
|
| `tools/production/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI |
|
||||||
|
| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) |
|
||||||
|
| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) |
|
||||||
|
| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) |
|
||||||
|
|
||||||
|
## D. Полный список актуальных файлов монолита (снимок на входе)
|
||||||
|
|
||||||
|
Чтобы в новом треде можно было приложить всё разом, если удобнее не
|
||||||
|
дробить по фазам. Актуальные (пост-Фаза-4) версии:
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/production/
|
||||||
|
├── pyproject.toml
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── app.py
|
||||||
|
│ ├── app.tcss
|
||||||
|
│ ├── main.py (точка входа — фактически в tools/production/main.py, см. pyproject scripts)
|
||||||
|
│ ├── models.py
|
||||||
|
│ ├── flasher.py ← Фаза 2/4, актуальная версия
|
||||||
|
│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест)
|
||||||
|
│ ├── usb_ports.py ← Фаза 1
|
||||||
|
│ ├── firmware_client.py
|
||||||
|
│ ├── m5_client.py
|
||||||
|
│ ├── orchestrator.py
|
||||||
|
│ ├── boot_art.py
|
||||||
|
│ ├── widgets.py (или widgets/)
|
||||||
|
│ └── screens/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── waiting.py
|
||||||
|
│ ├── flash.py ← Фаза 4 (правлены комментарии)
|
||||||
|
│ ├── post_flash.py
|
||||||
|
│ ├── connection_watcher.py
|
||||||
|
│ └── diag/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── confirm_panel.py
|
||||||
|
│ ├── results.py
|
||||||
|
│ └── test_list.py
|
||||||
|
├── tests/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── test_flash_backend.py ← 41 тест
|
||||||
|
├── spike/ (Фаза 0, в релиз не идёт)
|
||||||
|
│ ├── spike_hab.py
|
||||||
|
│ ├── spike_flash.py
|
||||||
|
│ └── spike_readback.py (диагностика Гейта 3, на будущее)
|
||||||
|
└── custom_binaries/ (пустая, для оператора)
|
||||||
|
|
||||||
|
tools/host/ (dev-CLI, Р2 — НЕ трогается)
|
||||||
|
├── flash_usb.py
|
||||||
|
└── dcd/
|
||||||
|
├── ivt_flashloader.bin
|
||||||
|
├── dcd.bin
|
||||||
|
├── w25q128_fdcb.bin
|
||||||
|
└── w25q512_fdcb.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
> Примечание: `main.py` в `pyproject.toml` прописан как
|
||||||
|
> `service-tui = "main:main"` — точка входа лежит в
|
||||||
|
> `tools/production/main.py` (не в `app/`), а `app/app.py` содержит
|
||||||
|
> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5.
|
||||||
|
|
||||||
|
## E. Что уже решено и не пересматривается (сводка для нового треда)
|
||||||
|
|
||||||
|
- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже).
|
||||||
|
- **Р10** — erase-таймаут → вариант B (detect_sdp после False).
|
||||||
|
- **Р11** — троттлинг лога 10%.
|
||||||
|
- **Р12** — уровни логов + env DEBUG.
|
||||||
|
- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны.
|
||||||
|
- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`.
|
||||||
|
- **О3** — одна плата на столе, ограничение v1.
|
||||||
|
- **POST-1** — циклический прогон тестов, после релиза.
|
||||||
|
- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py`
|
||||||
|
меняются только там, где явно указано в roadmap.
|
||||||
|
- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе.
|
||||||
|
- Порядок ревью: один файл за раз, полные файлы для новых/целиком
|
||||||
|
переписываемых, unified diff для точечных правок.
|
||||||
|
|
@ -329,7 +329,6 @@ def test_flash_write_memory_fails(monkeypatch, events, tmp_path):
|
||||||
with pytest.raises(fb.FlashBackendError, match="write_memory"):
|
with pytest.raises(fb.FlashBackendError, match="write_memory"):
|
||||||
fb.flash(hab_bin, progress_cb=_collector(events))
|
fb.flash(hab_bin, progress_cb=_collector(events))
|
||||||
|
|
||||||
# reset/done не должны наступить после ошибки записи
|
|
||||||
assert "reset" not in _phases(events)
|
assert "reset" not in _phases(events)
|
||||||
assert "done" not in _phases(events)
|
assert "done" not in _phases(events)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue