# Refactoring: all the documents

This commit is contained in:
Dmitry Akimov 2026-07-07 11:25:06 +03:00
parent 22c40779ef
commit 0f54c35be6
16 changed files with 758 additions and 436 deletions

View file

@ -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

View file

@ -11,9 +11,21 @@
| Проект | Путь | Описание | | Проект | Путь | Описание |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- | | ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Тестовая прошивка | `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/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
`bootloader`/`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 +77,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-зависимостей).

View file

@ -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(); }

View file

@ -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 усиление равно нулю.

View file

@ -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 остаётся место для
@ -121,7 +123,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 +134,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 +161,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 +190,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);

View file

@ -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,

View file

@ -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 остаётся открытым после вызова (паттерн проекта).

View file

@ -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()` возвращает фактически прочитанное количество байт —

View file

@ -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`).
--- ---

View file

@ -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

View file

@ -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.
--- ---

View file

@ -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`) |
--- ---
## Матрица тестов ## Матрица тестов
Порядок — как в реестре `k_registry[]` (`test_runner.c`); полная версия с
кодами `detail` и HIL-таблицей реле — в
[firmware/test/README.md §Матрица тестов](../../firmware/test/README.md#матрица-тестов).
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | | ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ | | --------- | ------------------- | ------------------ | -------- | -------- | --------------------- |
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | | `usd` | microSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) | | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ (6 шагов, см. ниже) |
| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) | | `buttons` | Test Buttons | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
| `can` | CAN | HIL | ❌ | ✅ | ❌ | | `opto` | Opto Inputs | HIL | ❌ | ✅ | ❌ (авто, 6 шагов) |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | | `can` | CAN loopback | HIL | ❌ | ✅ | ❌ (авто, 2 шага) |
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ | | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ (слышимость тона) |
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы:** **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` таргета.
--- ---

View file

@ -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 попытается подключиться до включения питания.
--- ---

View file

@ -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/&lt;dir&gt;/test_&lt;name&gt;.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 + код
```
Весь диапазон `0x600000000x6000FFFF` — один 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
```

View file

@ -18,14 +18,19 @@ TUI-приложение для диагностики и прошивки пл
```bash ```bash
┌────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────┐
│ service_tool v0.3.0 │ │ service_tool v0.2.0 │
│ │ │ │
│ [LOGO_ART] │ │ [LOGO_ART] │
│ │ │ │
│ Подключите плату индикатора к USB... ⠋ │ │ Подключите плату индикатора к USB... ⠋ │
│ │
│ [ ✕ Выйти из приложения ] │
└────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘
``` ```
Версия читается из `pyproject.toml` — при бампе версии мокап выше не нужно
обновлять руками, TUI подставит актуальную сама.
При потере соединения на любом другом экране сессия разрывается полностью — При потере соединения на любом другом экране сессия разрывается полностью —
TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над
подсказкой на 4 секунды появляется строка `⚠ <причина>` (например, подсказкой на 4 секунды появляется строка `⚠ <причина>` (например,
@ -55,7 +60,7 @@ TUI не пытается восстановить прежнее состоян
│ │ │ │
│ ████████████░░░░░░ ← без числового % │ │ ████████████░░░░░░ ← без числового % │
│ ┌────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────┐ │
│ │ ▶ Сборка HAB-образа (nxpimage)... │ │ │ │ ▶ Сборка HAB-образа (HabImage)... │ │
│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ │ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │
│ │ ... │ │ │ │ ... │ │
│ └────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────┘ │
@ -70,10 +75,11 @@ TUI не пытается восстановить прежнее состоян
**"Другое" — для бинарников, собранных не в этом репозитории.** В **"Другое" — для бинарников, собранных не в этом репозитории.** В
`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без `custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без
FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до `nxpimage`). FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до сборки HAB).
TUI сама собирает из него загружаемый образ на лету: TUI сама собирает из него загружаемый образ на лету, in-process через
Python API `spsdk` (без вызова внешних CLI-утилит):
1. `nxpimage hab export` — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") 1. `HabImage` (spsdk) — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM")
2. в Flash пишется явный FCB под выбранную память платы (не тот же 2. в Flash пишется явный FCB под выбранную память платы (не тот же
auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для
W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`) W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`)
@ -193,6 +199,19 @@ Production/Custom этот шаг не нужен).
--- ---
## Известные ограничения
- **Одна плата на столе одновременно.** В SDP/Flashloader-режиме плату
нельзя идентифицировать по UID — авто-прошивка по факту детекта без
подтверждения оператора убрала бы последний шанс заметить, что в руках
не та плата. Массового программирования (несколько плат параллельно)
нет и не планируется в этом виде — см. `docs/DEV_ARCH.md`, §8.
- **Циклический прогон тестов** (повторный автозапуск набора без ручного
нажатия на каждую итерацию) — отложен на пост-релиз, не входит в текущую
версию.
---
## Конфигурация (`.env`) ## Конфигурация (`.env`)
```ini ```ini
@ -223,6 +242,11 @@ FIRMWARE_BUILD_TYPE=Debug
# Опционально: путь к директории лога TUI # Опционально: путь к директории лога TUI
# SERVICE_LOG_DIR=/tmp # SERVICE_LOG_DIR=/tmp
# Уровень логирования. По умолчанию — INFO, при этом spsdk/libusbsio
# принудительно приглушены до WARNING. DEBUG включает полный дамп,
# включая сырые HID-пакеты spsdk (много строк на одну прошивку).
# SERVICE_LOG_LEVEL=DEBUG
``` ```
--- ---
@ -239,38 +263,48 @@ just host::service-tui # запустить TUI
### Standalone-бинарь (сервисник) ### Standalone-бинарь (сервисник)
```bash ```bash
just host::service-build just host::package-tui
# → tools/production/dist/service_tui # → tools/production/dist/service-tui-vX.Y.Z-<os>/
``` ```
> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен Бандл (PyInstaller, onedir) самодостаточен — прошивка идёт напрямую через
> инициализированный `tools/host/` (`just host::setup-tools`), либо spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` ни
> абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`). субпроцессом, ни как-либо иначе. Не нужны ни Zadig, ни установленный
Python/uv на машине сервисника. Структура бандла и резолв путей во frozen —
см. [DEV_ARCH.md](docs/DEV_ARCH.md), §14.
--- ---
## Зависимости ## Зависимости
| Пакет | Версия | Назначение | | Пакет | Версия | Назначение |
| --------------- | ------ | ----------------------------------------------------- | | --------------- | ------- | -------------------------------------------------------- |
| `textual` | ≥ 0.80 | TUI фреймворк | | `textual` | ≥ 0.80 | TUI фреймворк |
| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | | `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial |
| `pyusb` | ≥ 1.0 | детект BootROM SDP (не виден через pyserial на macOS) | | `pyusb` | ≥ 1.0 | не используется в коде (детект SDP/CDC идёт через `spsdk`) — исторический остаток, кандидат на удаление из `pyproject.toml` |
| `spsdk` | 3.7.0 | прошивка: SDP/McuBoot (Flash, chip erase) + HabImage (сборка кастомных HAB), нативный HID (libusbsio) — без Zadig |
| `python-dotenv` | ≥ 1.0 | загрузка `.env` | | `python-dotenv` | ≥ 1.0 | загрузка `.env` |
| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | | `pyinstaller` | ≥ 6.0 | сборка standalone-бандла (`just host::package-tui`) |
**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает **Никакой рантайм-зависимости на `tools/host/` в смысле subprocess-вызовов
`tools/host/flash_usb.py` через `uv run``tools/host/` должен быть нет.** Прошивка выполняется in-process через `spsdk` (`app/flash_backend.py`).
инициализирован (`just host::setup-tools`). Из `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 ← по умолчанию tools/production/service_tui.log ← по умолчанию (dev) / рядом с exe (frozen)
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env $SERVICE_LOG_DIR/service_tui.log ← если задан в .env
``` ```
Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не Уровень по умолчанию — `INFO`; `textual`, `spsdk` и `libusbsio` понижены до
пишет в stdout — Textual захватывает терминал. `WARNING` независимо от root (иначе прошивка даёт ~135 строк сырых
HID-пакетов на одну операцию). `SERVICE_LOG_LEVEL=DEBUG` включает полный
DEBUG везде, включая эти модули — используется при разборе проблем на
железе. TUI не пишет в stdout — Textual захватывает терминал.

View file

@ -14,11 +14,15 @@
```bash ```bash
tools/production/ tools/production/
├── main.py ← точка входа (10 строк) ├── main.py ← точка входа: логирование (Р12) + ServiceApp().run()
├── pyproject.toml ← зависимости uv ├── pyproject.toml ← зависимости uv (включая spsdk==3.7.0)
├── uv.lock ├── uv.lock
├── service_tui.spec ← PyInstaller spec (Фаза 5, onedir)
├── custom_binaries/ ← runtime, gitignored, создаётся автоматически ├── custom_binaries/ ← runtime, gitignored, создаётся автоматически
│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое» │ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое»
├── tests/
│ └── test_flash_backend.py ← unit-тесты flash_backend.py (45 тестов, без event loop)
├── spike/ ← Фаза 0, де-риск spsdk API (в релиз не идёт)
└── app/ └── app/
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
├── app.tcss ← единый файл стилей для всех экранов ├── app.tcss ← единый файл стилей для всех экранов
@ -26,7 +30,10 @@ tools/production/
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen ├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8) ├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8)
├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8) ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8)
├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py ├── usb_ports.py ← resolve_serial_port() — резолв COM/tty по VID:PID (Р8)
├── flash_backend.py ← синхронное ядро прошивки: прямой spsdk API (McuBoot/SDP/HabImage),
│ zero Textual/asyncio импортов, тестируется без event loop
├── flasher.py ← async-обёртка над flash_backend.py (asyncio.to_thread)
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты ├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
├── widgets/ ├── widgets/
│ ├── __init__.py │ ├── __init__.py
@ -44,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. Концепция
@ -55,37 +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\nsubprocess + pyusb detect"] 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\nRLY14 + CAN"] M5HW["M5StampPLC\nRLY14 + CAN"]
end end
subgraph Host["tools/host/"]
FU["flash_usb.py\nsdphost + blhost"]
end
FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
FL -->|"subprocess uv run"| FU FB -->|"SDP.write_file + jump_and_run\n(spsdk.sdp)"| ROM
FU -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM ROM -.->|"загружает ivt_flashloader.bin"| FLD
FB -->|"McuBoot: erase/write_memory/reset\n(spsdk.mboot)"| FLD
M5 <-->|"JSON-lines\nSerial"| M5HW M5 <-->|"JSON-lines\nSerial"| M5HW
M5HW -->|"RLY14"| Board M5HW -->|"RLY14"| Board
``` ```
> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как > **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` делегируют в
> основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через > `flash_backend.detect_sdp()`/`detect_cdc()` (Р7 — `spsdk`-сканеры
> `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC. > `SdpUSBInterface.scan()`/`serial.tools.list_ports`, БЕЗ `pyusb`: BootROM
> M5StampPLC детектируется отдельно в `m5_client.py` тем же способом > SDP не создаёт serial-порт на macOS, но `spsdk` видит его нативно через
> (`pyusb`, VID/PID из `.env` — см. раздел 5). > 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 (обработка ошибок прошивки).
--- ---
@ -193,11 +220,14 @@ flowchart TD
## 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 (обрыв обнаружит сам `flash_usb.py` subprocess). прошивки/erase (обрыв в этом случае обнаруживает сам `flash_backend.py`,
см. §6.2, — не watcher).
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов - **На `DiagScreen`** — проверка приостановлена во время прогона тестов
(обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не (обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не
просто исчезновение устройства из списка). просто исчезновение устройства из списка).
@ -214,6 +244,53 @@ flowchart TD
разрыва 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__` упал».
--- ---
## 7. AppFrame — общий каркас экранов ## 7. AppFrame — общий каркас экранов
@ -292,47 +369,70 @@ class FlashPreset:
подтверждения оператора убирает последний шанс заметить, что в руках не та подтверждения оператора убирает последний шанс заметить, что в руках не та
плата. Оставлена только «липкая» память выбора (этот раздел). плата. Оставлена только «липкая» память выбора (этот раздел).
### 8.3 Конвейер сборки (`flasher.py`) ### 8.3 Конвейер сборки — прямые вызовы spsdk (`flash_backend.py`)
Сборка HAB-образа и прошивка выполняются **in-process** через Python API
`spsdk` — никакого subprocess/CLI (`nxpimage`/`sdphost`/`blhost`), в отличие
от dev-CLI `tools/host/flash_usb.py`, который остаётся отдельной,
независимой реализацией на тех же CLI-утилитах (см. §1, врезка про
разделение dev-CLI/production-TUI).
``` ```
Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) [flasher.py]
└── _run_flash_custom() └── _flash_custom() [flasher.py]
├── _build_custom_hab(raw_bin, use_dcd, progress_cb) ├── asyncio.to_thread(flash_backend.build_custom_hab, raw_bin, use_dcd)
│ ├── генерирует temp .yaml в tools/host/hab/ (по образцу hab_bootloader_*.yaml: │ ├── _make_hab_config() — генерирует YAML с АБСОЛЮТНЫМИ путями
│ │ startAddress=0x60000000, ivtOffset=0x1000, initialLoadSize=0x2000, │ │ во временном work_dir (tempfile.mkdtemp), + DCDFilePath
│ │ family=mimxrt1050, + DCDFilePath: ../dcd/dcd.bin если use_dcd) │ │ на real_dcd_bin_path() если use_dcd
│ ├── uv run nxpimage hab export --force -c <yaml> -o <out>, │ ├── Config.create_from_file() + HabImage.get_validation_schemas_from_cfg()
│ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath │ │ + cfg.check() — валидация конфига (spsdk.image.hab.hab_image)
│ │ резолвится от этой директории, как в build.just) │ ├── HabImage.load_from_config(cfg).export() — сборка байт HAB-образа
│ └── стриминг stdout nxpimage в progress_cb (не только logger.debug — │ │ в памяти (та же логика, что nxpimage CLI, см. Фазу 0 — golden-тест
│ иначе во время сборки лог FlashScreen выглядит «зависшим») │ │ byte-exact, test_build_custom_hab_bytes_match_golden)
└── flash_usb.py --bin-path <hab_bin> --fcb-path tools/host/dcd/{fcb_variant}_fdcb.bin │ └── эмитит progress_cb(phase="hab_build", 0% → 100%)
(временный .yaml и собранный HAB-образ удаляются после прошивки) └── 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` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не `dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не
зависит от того, что именно исполняется), поэтому просто константный путь, зависит от того, что именно исполняется), поэтому просто константный путь,
без вариантов. без вариантов. Пути к data-блобам (`dcd.bin`, `*_fdcb.bin`,
`ivt_flashloader.bin`) резолвятся двухрежимно (`_host_dcd_dir()`,
`flash_backend.py`): в dev — `tools/host/dcd/` (те же файлы, что использует
`flash_usb.py`, отслеживаются в git), в frozen-бандле —
`sys._MEIPASS/data` (см. §14).
### 8.4 `flash_usb.py` — явная запись FCB вместо auto-config ### 8.4 Явная запись FCB вместо auto-config (`write_fcb_explicit`)
```python ```python
def write_fcb_explicit(fcb_path: Path) -> None: def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
"""write-memory 0x60000000 <fcb_path> — буквальная запись 512-байтного """Пишет буквальный FCB-блоб (512 байт) в Flash[FLASH_BASE] (custom-бинари).
FCB-блоба (tag 'FCFB'), а не magic option word 0xF000000F.
Обязателен для кастомных бинарей — auto-config Flashloader проверен См. flash_usb.py::write_fcb_explicit — nxpimage не кладёт FCB в HAB-образ,
только для W25Q128.""" поэтому для произвольных чипов нужен явный блоб под конкретный memory chip.
"""
``` ```
Активируется флагом `--fcb-path` (только вместе с `--bin-path`). Штатный `mboot.write_memory(FLASH_BASE, data, mem_id=0)` — буквальная запись
`--firmware`-путь (три сборки из `BUILD_DIR`) не тронут: без `--fcb-path` 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`, как и раньше.
Заодно увеличен таймаут `blhost` для `flash-erase-all` (chip erase) — Таймаут для `flash_erase_all` (chip erase) увеличен до `ERASE_ALL_TIMEOUT_MS
`-t 200000` вместо дефолтного: W25Q512 стирается заметно дольше W25Q128, = 200_000` мс (эквивалент `blhost -t 200000`): W25Q512 стирается заметно
дефолтного таймаута `blhost` не хватало. `flash-erase-region` (стирание дольше W25Q128, дефолтного таймаута McuBoot не хватало.
пары секторов под FCB+HAB при обычной прошивке) не трогали — там масштаб `flash_erase_region` (стирание пары секторов под FCB+HAB при обычной
на порядки меньше, дефолта достаточно независимо от чипа. прошивке) не трогали — там масштаб на порядки меньше, дефолта достаточно
независимо от чипа.
### 8.5 UI (`flash.py`) ### 8.5 UI (`flash.py`)
@ -411,7 +511,7 @@ sequenceDiagram
OP->>TUI: запустить service_tui OP->>TUI: запустить service_tui
TUI->>WS: push_screen() TUI->>WS: push_screen()
WS->>WS: pyusb poll каждые 1.5 с WS->>WS: spsdk/list_ports poll каждые 1.5 с
OP->>FW: подключить плату USB OP->>FW: подключить плату USB
WS->>TUI: DeviceDetected(DIAGNOSING) WS->>TUI: DeviceDetected(DIAGNOSING)
@ -547,8 +647,102 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл
--- ---
## 14. Упаковка PyInstaller и frozen-резолв путей (Фаза 5)
`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`).
Целевая структура бандла:
```
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/ ← пустая, создаётся оператором/автоматически
```
Каждый модуль, которому нужен путь к данным, сам решает dev vs frozen через
`getattr(sys, "frozen", False)` — единообразный паттерн по всему `app/`:
| Функция | Dev | Frozen |
| --- | --- | --- |
| `flash_backend._host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS/data` |
| `flash_backend.firmware_hab_path()` | `$BUILD_DIR/<Type>/*_hab.bin` (репо `build/`) | `<exe_dir>/firmware/<Type>/*_hab.bin` |
| `flasher._resolve_custom_binaries_dir()` | `tools/production/custom_binaries/` | `<exe_dir>/custom_binaries/` (override — `SERVICE_CUSTOM_BINARIES_DIR`) |
| `waiting._read_app_version()` | `tools/production/pyproject.toml` | тот же путь — `pyproject.toml` кладётся в `datas` спека (нужен для парсинга версии во frozen) |
| `main._setup_logging()` | рядом с `main.py` | рядом с исполняемым файлом (`sys.executable.parent`) |
`sys.executable` (не `sys._MEIPASS`) — единственный путь, одинаково
работающий и для onefile, и для onedir; `_MEIPASS` для onefile указывает на
временную распаковку, которая исчезает после выхода из процесса.
Нативный HID-транспорт (`libusbsio`, следствие Р7 — spsdk вместо pyusb)
означает, что `libusb-1.0.*`/Zadig в бандле **не нужны** ни на Windows, ни
на macOS — детект BootROM SDP и Flashloader работает из коробки.
> **Расхождение spec/факт:** закоммиченный `service_tui.spec` объявляет в
> `datas` только `('../shared', 'shared')` — без `dcd/*.bin`,
> `pyproject.toml` или `spsdk`-данных. Тем не менее уже собранные релизные
> бандлы в `tools/production/dist/service-tui-v0.2.0-{macos,windows}/`
> фактически содержат `_internal/data/{dcd.bin,*_fdcb.bin,ivt_flashloader.bin}`,
> `_internal/pyproject.toml` и `_internal/spsdk/` — то есть сборки, тестировавшиеся
> на железе (Фаза 5, гейт по macOS/Windows), были собраны с более полным
> набором `datas`, чем то, что сейчас лежит в репозитории. `service_tui.spec`
> нужно актуализировать (`collect_data_files("spsdk")`, `tools/host/dcd/*.bin`
> → `data/`, `pyproject.toml`) до следующей сборки релиза — см. «Известные
> открытые вопросы».
---
## 15. Логирование (Р11/Р12, Фаза 4b)
`main.py::_setup_logging()`:
- 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`.
`FlashScreen._on_progress()` (Р11) троттлит **только** запись в
`#flash-log` для фазы `write`: событие логируется раз на каждые 10%
(`progress.percent // 10`), а не на каждый пакет `spsdk` (~135 →
~10 строк). Прогресс-бар при этом обновляется на **каждом** событии —
плавность не теряется, троттлинг влияет только на текстовый лог.
---
## Известные открытые вопросы ## Известные открытые вопросы
- **Документация — Фаза 6 (текущая).** Инженерные фазы 05 (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 нестабильна** (медленное мигание — подозрение на - **Release-сборка firmware нестабильна** (медленное мигание — подозрение на
проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно
форсирует Debug через `FIRMWARE_BUILD_TYPE`. форсирует Debug через `FIRMWARE_BUILD_TYPE`.
@ -557,14 +751,19 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл
архитектурным решением, а не техдолгом. архитектурным решением, а не техдолгом.
- Пункты плана 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 всегда пишется
явно (`--fcb-path`, см. §8.4). Остаётся не до конца понятым, работает ли явно (параметр `fcb_path` в `flash_backend.flash()`, см. §8.4). Остаётся не
`configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос до конца понятым, работает ли `configure-memory 0xF000000F` для этих
снят с повестки архитектурным решением, а не исследован до конца. чипов корректно в принципе — вопрос снят с повестки архитектурным
решением, а не исследован до конца.