diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md deleted file mode 100644 index 7fa9d46..0000000 --- a/firmware/test/PLAN.md +++ /dev/null @@ -1,488 +0,0 @@ -# firmware_test — План разработки - -> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified). - ---- - -## Контекст проекта - -**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации). -Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика. - -**Стенд:** - -- Хост подключается через USB CDC ACM — единственный канал firmware_test -- HIL-тесты управляются через M5StampPLC (опционально) -- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно - ---- - -## Текущий статус - -| Компонент | Статус | Примечание | -| ------------------------------ | ------ | ------------------------------------------------ | -| `bsp_usb_cdc` | ✅ | HIL тест пройден | -| firmware_test скелет | ✅ | `main.c` + `cli.c` | -| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | -| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | -| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | -| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | -| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | -| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | -| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified | -| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified | -| `test_opto` | ✅ | Этап 6б, hardware-verified | -| `test_can` | ✅ | Этап 6в, hardware-verified | -| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | -| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | -| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | -| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified | -| Provisioning | ⬜ | Этап 7 | -| TUI сервисного инженера | ⬜ | Этап 8 | - ---- - -## Матрица тестов — итоговая - -| ID | Название | Critical | HIL | Тип | BSP | Статус | -| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ | -| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | -| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | -| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | -| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | -| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | -| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ | -| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | - -**Убранные тесты (закрытые решения):** - -- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется -- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно - ---- - -## Закрытые архитектурные решения - -> Не пересматривать без явного запроса. - -### Этапы 1–5 (ранее зафиксированные) - -- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test. -- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`. -- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует. -- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`. -- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7). -- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`. -- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL. -- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP. - -### Этап 6 (новые решения) - -- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL). - TUI фильтрует HIL-тесты если M5StampPLC не подключён. -- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста. - TUI строит UI динамически, не хардкодит список тестов. -- **`run_selected`:** запуск произвольного подмножества тестов по списку ID. - Порядок выполнения — как в реестре таргета, не как в запросе. - Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI. -- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request` - от HIL-теста TUI командует M5, получает результат, отправляет confirm. -- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты - недоступны (серые в UI, не входят в `run_selected`). -- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. -- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен - один канал. Буфер всегда стерео (L+R идентичны). -- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая), - `confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL. - `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`. -- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`. - Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M. - Нарушение порядка приводит к щелчку при старте или отсутствию звука. -- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking), - параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с. -- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно - выставлять в `true`. При инициализации через designated initializers без явного - указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит - на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого - прогона). Воспроизводится только при cold reset. -- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на - `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate - может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` - перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым — - закрывать не нужно, LPUART1 тактируется с минимальным потреблением. - `bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`. -- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при - старте, может быть пропущено). Проверяет живость через `ping → pong`. -- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина - без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется - в `test_opto.c` после settle — обходит race condition когда чётное число ISR - при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`. -- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm, - но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`. -- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`. - Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`, - `bsp_opto_force_read()` читает финальное состояние пина напрямую. - -### Этап 8 (TUI решения) - -- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost). - Оператор сам переставляет перемычку BOOT — это ок, документируется. -- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130) - или CDC firmware_test (session_start) — и показывает соответствующий экран. -- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты, - работает в SSH-сессии, вписывается в uv-экосистему. -- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/` - и `tools/production/`. - ---- - -## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН - -### 6а — Расширение протокола ✅ - -**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md` - -#### Новая команда `list_tests` - -```json -→ {"type":"cmd","cmd":"list_tests"} -← {"type":"test_list","tests":[ - {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, - {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, - {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, - {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, - {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, - {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, - {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, - {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true} - ]} -``` - -#### Новая команда `run_selected` - -```json -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} -← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi",...} -← {"type":"test_begin","id":"display",...} -← {"type":"test_result","id":"display",...} -← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"} -``` - -Если хотя бы один ID не найден в реестре: - -```json -← {"ok":false,"error":"UNKNOWN_TEST"} -``` - -**Реализация в `test_runner.c`:** - -- Новый режим `RUNNER_MODE_SELECTED` -- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc -- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция - -### 6б — test_opto.c ✅ - -**Файл:** `firmware/test/src/tests/test_opto.c` - -6 шагов, попарно ACTIVE/INACTIVE для трёх каналов: - -| Шаг | confirm_request id | M5 действие | Проверка | -| --- | ------------------- | ----------- | -------------------------------- | -| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` | -| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` | -| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` | -| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` | -| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` | -| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` | - -- Init: `bsp_opto_init()` единым вызовом для всех каналов -- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`) -- FAIL при несоответствии: `detail = " state mismatch: expected ACTIVE got INACTIVE"` -- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг - -### 6в — test_can.c ✅ - -**Файл:** `firmware/test/src/tests/test_can.c` - -2 шага, оба направления независимо: - -**Шаг 1 — RX (M5 → таргет):** - -```bash -confirm_request("can_rx_ready") -→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF]) -→ TUI: confirm(true) -→ таргет: bsp_can_receive(&frame, 500 мс) -→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF] -→ FAIL если timeout или несовпадение -``` - -**Шаг 2 — TX (таргет → M5):** - -```bash -bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс) -confirm_request("can_tx_verify") -→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data -→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет -→ FAIL если confirmed=false или timeout -``` - -- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует -- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` - -### 6г — bsp_mqs + test_mqs.c ✅ - -**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c` - -**bsp_mqs:** - -- SAI3 + eDMA (DMA0 канал 0) + MQS периферия -- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT` -- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358 -- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`, - `bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume` - -**test_mqs:** - -- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида -- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле -- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL -- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг) -- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL` - -### 6д — HIL pytest для firmware_test ✅ - -**Файлы:** - -``` -tools/hil/conftest.py ← фикстура firmware_cdc -tools/hil/06_test_firmware_opto.py -tools/hil/06_test_firmware_can.py -``` - -**Фикстура `firmware_cdc`:** - -```python -@pytest.fixture(scope="module") -def firmware_cdc(m5): - """ - Открывает USB CDC порт firmware_test. - firmware_test уже прошит в Flash (не загружается pyOCD). - Проверяет живость через ping → pong. - """ -``` - -**`FirmwareCdcClient`** — тонкий клиент: - -- `send_cmd(cmd_dict)` — отправить JSON команду -- `wait_event(type, timeout_s)` — ждать события нужного типа -- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}` -- `run_test(id)` — запустить тест, вернуть test_result dict - -**Justfile:** - -```bash -hil-firmware-opto → pytest 06_test_firmware_opto.py -v -hil-firmware-can → pytest 06_test_firmware_can.py -v -``` - ---- - -## Этап 7 — Provisioning - -### Что нужно - -1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp` -2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary` -3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста -4. Записывать статус в Flash (первый сектор после прошивки, вне XIP) - -### BSP (предварительно) - -```c -/* bsp/provisioning/include/bsp/provisioning.h */ -bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */ -``` - -### Открытые вопросы — Этап 7 - -- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID? -- [ ] Нужна ли защита от повторного provisioning (write-once)? - ---- - -## Этап 8 — TUI сервисного инженера - -### Стек технологий - -| Компонент | Выбор | Обоснование | -| ------------- | ------------- | ----------------------------------------------------- | -| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема | -| Serial | pyserial | Уже в стеке (tools/hil) | -| Прошивка | spsdk | sdphost + blhost, уже в tools/host | -| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом | - -### Структура приложения - -```bash -tools/production/ -├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv -├── uv.lock -├── main.py ← точка входа -├── app/ -│ ├── tui.py ← Textual App, экраны, layout -│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test -│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py) -│ ├── flasher.py ← USB SDP обёртка над spsdk -│ ├── orchestrator.py ← confirm_request → M5 action → confirm response -│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses) -└── README.md - -tools/shared/ -└── m5_agent.py ← общая M5-логика для hil/ и production/ -``` - -### Два режима работы - -**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) - -``` -┌─ Прошивка платы ─────────────────────────────────┐ -│ Обнаружен BootROM (SDP режим) │ -│ │ -│ Что прошить? │ -│ ◉ firmware_test (диагностика) │ -│ ○ Production (bootloader + tft_app) │ -│ │ -│ Файл: [/path/to/firmware_test_hab.bin ···] │ -│ │ -│ [ Прошить ] │ -│ │ -│ ████████████░░░░░░ 64% Запись во Flash... │ -└────────────────────────────────────────────────────┘ -``` - -**Режим B — Диагностика** (триггер: session_start получен по CDC) - -``` -┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ -│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ -├────────────────────────────────────────────────────┤ -│ Выбор тестов: │ Результаты: │ -│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ -│ ☑ QSPI Flash │ qspi ✓ PASS │ -│ ☑ microSD │ usd ✗ FAIL │ -│ ☑ TFT Display │ mount failed: 5 │ -│ ☑ Кнопки │ display ✓ PASS │ -│ ☑ MQS Audio │ buttons ✓ PASS │ -│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │ -│ ☑ Оптовходы [HIL] │ ... │ -├────────────────────────────────────────────────────┤ -│ [ Запустить выбранные ] [ Все тесты ] │ -│ ████████████████░░░░ 80% Тест: display │ -├────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└────────────────────────────────────────────────────┘ -``` - -### Поведение confirm_request в TUI - -| Тип теста | Источник confirm | Действие TUI | -| -------------------- | ------------------ | --------------------------------------------- | -| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown | -| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | -| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | - -HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt. - -### Конфигурация (.env) - -```ini -# Существующие переменные (tools/hil/.env): -HIL_VCOM_PORT=/dev/ttyACM0 -HIL_M5_PORT=/dev/ttyACM1 - -# Новые переменные для production TUI: -SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start -SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5 -FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin -PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin -PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin -``` - -### Запуск - -```bash -just host::service-tui # запустить TUI сервисного инженера -just host::service-flash # прошить без TUI (для автоматизации) -``` - -### Процесс работы сервисника - -**Диагностика (firmware_test уже в Flash):** - -```bash -1. Плата в нормальном режиме (BOOT_MOD_1 → GND) -2. Подключить USB к сервисному ПК -3. just host::service-tui → TUI обнаружил session_start → Режим B -4. Выбрать тесты → Запустить → Смотреть результаты -``` - -**Перепрошивка (нужна новая версия firmware_test или production):** - -```bash -1. Перемычка BOOT_MOD_1 → 3V3 -2. Reset, подключить USB -3. TUI обнаружил 1FC9:0130 → Режим A -4. Выбрать бинарь → Прошить -5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B -``` - ---- - -## Порядок реализации - -``` -✅ Этап 1 протокол v2 + runner -✅ Этап 2 bsp_sdram + test_sdram -✅ Этап 3 bsp_qspi_flash + test_qspi -✅ Этап 4 bsp_sd + test_usd -✅ Этап 5 display + buttons -✅ Этап 6а протокол: list_tests + run_selected -✅ Этап 6б test_opto.c + hardware верификация -✅ Этап 6в test_can.c + hardware верификация -✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) -✅ Этап 6е HIL pytest: 06_test_firmware_opto.py -✅ Этап 6ж HIL pytest: 06_test_firmware_can.py -✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация - -⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ - -⬜ Этап 8а tools/production/ скелет + models + clients -⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) -⬜ Этап 8в Экран прошивки (flasher + SDP автодетект) -⬜ Этап 8г Provisioning в TUI -⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики) - -⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру -``` - ---- - -## Зависимости между этапами - -``` -✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs) - ↓ - ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) - ↓ - ⬜ 7 (provisioning) - ↓ - ⬜ 8 (TUI) -``` \ No newline at end of file diff --git a/firmware/test/README.md b/firmware/test/README.md index 8d3fa1e..ce7aefc 100644 --- a/firmware/test/README.md +++ b/firmware/test/README.md @@ -1,10 +1,12 @@ # firmware_test -> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по -> рекламации. Запускается сервисным инженером через USB CDC ACM без -> предварительной прошивки загрузчика. -> -> Версия прошивки: `0.1.0` | Протокол: v2 +> Диагностическая прошивка для плат TFT индикаторов, вернувшихся по +> рекламации. Загружается на таргет сервисным инженером через USB (через BootROM IMXRT1052). +> +>Версия прошивки: `0.1.2` | Протокол: v2 +> +>Версия — из `project(firmware_test VERSION X.Y.Z)` в `CMakeLists.txt` +> (см. [Версионирование](#версионирование)) --- @@ -27,6 +29,7 @@ - [Как добавить новый тест](#как-добавить-новый-тест) - [Host unit-тесты](#host-unit-тесты) - [Версионирование](#версионирование) +- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты) --- @@ -45,7 +48,7 @@ just build::hab-firmware-test-debug # Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset just host::flash-test-debug -# Или через SWD (power cycle после) +# Или через SWD just host::flash-swd-test-debug ``` @@ -66,7 +69,7 @@ screen /dev/ttyACM0 После подключения таргет сразу присылает: ```json -{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} +{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} ``` Проверка связи: @@ -81,7 +84,7 @@ screen /dev/ttyACM0 ```json → {"type":"cmd","cmd":"run","id":"sdram"} ← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ``` Запуск всех тестов: @@ -110,12 +113,12 @@ just build::test-host │ JSON-lines, 1 строка = 1 сообщение ▼ [Плата MIMXRT1052 с firmware_test] - │ GPIO / LPUART / SEMC / FlexSPI / USDHC + │ GPIO / LPUART / SEMC / FlexSPI / USDHC / SAI(MQS) ▼ -[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto] +[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, MQS, Opto] ▲ [M5StampPLC — управление внешними сигналами для HIL тестов] - (реле → EXT_IN1/IN2, RS_RX, CAN, UART echo) + (реле → EXT_IN1/IN2, RS_RX; CAN loopback) ``` **Принцип разделения ответственности:** @@ -123,8 +126,8 @@ just build::test-host - Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`). - Хост — тонкий клиент: отправляет команды, отображает события, управляет интерактивными шагами через `confirm`. -- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок - не фиксирован. +- Тесты **атомарны**: инженер запускает один тест, подмножество или все сразу — + порядок не фиксирован. --- @@ -136,6 +139,8 @@ firmware/test/ └── src/ ├── main.c — инициализация BSP, главный цикл │ + ├── version.h.in — шаблон версии (CMake → generated/version.h) + │ ├── cli.h / cli.c — IO-слой │ буферизация строк, парсинг "type", │ диспатч на test_runner / protocol @@ -149,20 +154,23 @@ firmware/test/ │ ├── test_runner.h / .c — реестр + state machine │ IDLE → PRE_CONFIRM → RUNNING → IDLE - │ test_runner_wait_confirm() для display + │ test_runner_wait_confirm() для in-run confirm │ └── tests/ ├── test_sdram.c — SDRAM 32 MB (self) - ├── test_qspi.c — QSPI Flash 8 MB (self) - ├── test_usd.c — uSD SDIO (interactive) - ├── test_display.c — Display RGB888 (interactive) - ├── test_buttons.c — Test_But_1/2 (interactive) + ├── test_qspi.c — QSPI Flash W25Qxx (self) + ├── test_usd.c — microSD SDIO (interactive, pre-confirm) + ├── test_display.c — Display RGB888 (interactive, in-run confirm) + ├── test_buttons.c — Test_But_1/2 (interactive, физическое нажатие) + ├── test_opto.c — Opto-in EXT_IN1/IN2 + RS_RX (HIL) ├── test_can.c — CAN loopback (HIL) - ├── test_uart_ttl.c — UART TTL (HIL) - ├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL) - └── test_opto.c — Opto-in EXT_IN1/IN2 (HIL) + └── test_mqs.c — MQS Audio Out (interactive, in-run confirm) ``` +> Порядок файлов в `tests/` — как в `CMakeLists.txt`. Порядок **выполнения** +> тестов определяется реестром `k_registry[]` в `test_runner.c` +> (см. [Матрица тестов](#матрица-тестов)). + --- ### Граф зависимостей @@ -175,8 +183,9 @@ main.c ├── bsp_usb_cdc (USB CDC ACM, единственный транспорт) ├── cli.c │ └── bsp_usb_cdc (read / write) - │ └── protocol.c (send_error, send_pong) - │ └── test_runner.c (run_single, run_all, on_confirm) + │ └── bsp_provisioning (bsp_prov_read_uid — для get_uid) + │ └── protocol.c (send_error, send_pong, send_uid/version_response) + │ └── test_runner.c (run_single, run_all, run_selected, send_list, on_confirm) ├── protocol.c │ └── cli.c (cli_send) │ └── bsp_tick (bsp_tick_get_ms — для uptime) @@ -188,26 +197,28 @@ main.c └── tests/*.c (тест-модули через реестр) ``` -**BSP-зависимости тест-модулей:** +**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`): -| Тест | BSP модуль | -| --------------- | ------------------------------ | -| `test_sdram` | `bsp_sdram` | -| `test_qspi` | `bsp_qspi` | -| `test_usd` | `bsp_usd` | -| `test_display` | существующий display BSP | -| `test_buttons` | `bsp_button` ✅ | -| `test_can` | `bsp_can` ✅ | -| `test_uart_ttl` | `bsp_uart_host` ✅ | -| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ | -| `test_opto` | `bsp_opto` ✅ | +| Тест | BSP модуль | +| -------------- | ------------------------------ | +| `test_sdram` | `bsp_sdram` | +| `test_qspi` | `bsp_qspi_flash` | +| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) | +| `test_display` | `bsp_display` | +| `test_buttons` | `bsp_button` | +| `test_opto` | `bsp_opto` (rs_as_gpio=true) | +| `test_can` | `bsp_can` | +| `test_mqs` | `bsp_mqs` | + +> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного +> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`). --- ### State machine test_runner ```bash - cmd: run / run_all + cmd: run / run_all / run_selected │ ▼ ┌─────────────────────────────────────┐ @@ -232,9 +243,9 @@ main.c confirmed=false → SKIP │ │ timeout → SKIP │ │ │ │ - run: IDLE ──┘ │ - run_all: следующий тест в реестре ───┘ - run_all done: protocol_send_summary() + run: IDLE ──┘ │ + run_all / run_selected: следующий тест ───┘ + done: protocol_send_summary() ``` **Ключевые свойства state machine:** @@ -242,10 +253,12 @@ main.c - `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`. Пока тест выполняется, новые команды получают `BUSY`. - `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов - (display). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`. + (display, mqs). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`. USB-стек остаётся живым, confirm приходит без возврата в главный цикл. -- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают - `SKIP` немедленно, `summary.overall = "fail"`. +- `run_selected` работает по той же машине над маской выбранных тестов + (`g_s_selected[]`); порядок — по реестру, не по порядку в запросе. +- `critical=true` + `FAIL` в `run_all`/`run_selected` → все оставшиеся тесты + получают `SKIP` немедленно, `summary.overall = "fail"`. --- @@ -258,7 +271,7 @@ main.c | Интерфейс | USB CDC ACM, разъём J2 | | Кодировка | UTF-8 | | Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` | -| Максимальная длина строки | 128 байт включая `\n` | +| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) | | CR+LF | Принимается (таргет отбрасывает `\r`) | Нет хэндшейка, нет sequence number, нет подтверждений доставки. @@ -273,7 +286,7 @@ main.c │ │ │ [USB SDP: прошивка загружена] │ │ [CDC ACM: порт открыт] │ - │◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически + │◄─── {"type":"session_start","fw":"0.1.2",...} │ автоматически │ │ │──── {"type":"cmd","cmd":"ping"} ─────────────►│ │◄─── {"type":"pong"} │ @@ -290,7 +303,8 @@ main.c ``` `session_start` отправляется **автоматически** при каждом старте, до получения -первой команды. Хост должен быть готов принять его сразу после открытия порта. +первой команды. Хост должен быть готов принять его сразу после открытия порта +(либо не полагаться на него — фикстуры HIL проверяют живость через `ping`). --- @@ -310,7 +324,7 @@ main.c ```json → {"type":"cmd","cmd":"run","id":"sdram"} ← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ``` Если `id` не найден: @@ -324,13 +338,58 @@ main.c ```json → {"type":"cmd","cmd":"run_all"} ← {"type":"test_begin","id":"sdram",...} -← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} ← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} ← ... (остальные тесты) ... ← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"} ``` +#### `run_selected` — запуск подмножества тестов + +```json +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} +← {"type":"test_begin","id":"sdram",...} +← {"type":"test_result","id":"sdram","status":"pass",...} +← {"type":"test_begin","id":"opto",...} +← {"type":"test_result","id":"opto","status":"pass",...} +← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} +``` + +Порядок выполнения — по реестру таргета, не по порядку в запросе. Если хотя +бы один ID не найден — вся команда отклоняется (`UNKNOWN_TEST`), не запускается +ничего. + +#### `list_tests` — получить реестр тестов + +```json +→ {"type":"cmd","cmd":"list_tests"} +← {"type":"test_list","tests":[ + {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, + ... остальные ... + ]} +``` + +Хост (TUI) использует ответ для динамического построения списка тестов; +`requires_hil=true` тесты недоступны при отсутствии M5StampPLC. + +#### `get_uid` — прочитать UID чипа + +```json +→ {"type":"cmd","cmd":"get_uid"} +← {"type":"uid_response","uid":"A1B2C3D4E5F60011"} +``` + +`uid` — 8 байт (`BSP_PROV_UID_LEN`) big-endian, 16 hex-символов без +разделителей. При ошибке чтения: `{"ok":false,"error":"UID_READ_ERR"}`. + +#### `get_version` — прочитать версию прошивки + +```json +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + #### `confirm` — ответ оператора на интерактивный шаг ```json @@ -347,35 +406,19 @@ main.c #### `session_start` ```json -{ - "type": "session_start", - "fw": "0.1.0", - "target": "IMXRT1052", - "uptime_ms": 0 -} +{ "type":"session_start", "fw":"0.1.2", "target":"IMXRT1052", "uptime_ms":0 } ``` #### `test_begin` ```json -{ - "type": "test_begin", - "id": "sdram", - "name": "SDRAM 32 MB", - "critical": true -} +{ "type":"test_begin", "id":"sdram", "name":"SDRAM 32 MB", "critical":true } ``` #### `test_result` ```json -{ - "type": "test_result", - "id": "sdram", - "status": "pass", - "ms": 312, - "detail": "" -} +{ "type":"test_result", "id":"sdram", "status":"pass", "ms":15304, "detail":"" } ``` | `status` | Смысл | @@ -384,37 +427,39 @@ main.c | `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) | | `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше | -Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`. +Примеры `detail`: `"addr=0x80200001 exp=0x02 got=0xFF"`, `"JEDEC: mfr=0xFF exp=0xEF"`. + +#### `test_list` + +Ответ на `list_tests` — массив дескрипторов (`id`, `name`, `critical`, +`requires_hil`). #### `confirm_request` ```json -{ - "type": "confirm_request", - "id": "display_red", - "prompt": "Экран залит красным цветом?", - "timeout_ms": 15000 -} +{ "type":"confirm_request", "id":"display_red", "prompt":"Screen is solid red?", "timeout_ms":15000 } ``` Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете. Хост может дублировать countdown для UX. +#### `uid_response` / `version_response` + +Ответы на `get_uid` / `get_version` (см. соответствующие команды выше). + #### `summary` ```json -{ - "type": "summary", - "passed": 6, - "failed": 1, - "skipped": 0, - "overall": "fail" -} +{ "type":"summary", "passed":6, "failed":1, "skipped":0, "overall":"fail" } ``` `"overall":"fail"` — если хотя бы один `critical` тест провален. `"overall":"pass"` — все `critical` тесты прошли (non-critical могут fail). +#### `pong` + +Ответ на `ping`: `{"type":"pong"}`. + --- ### Ошибки протокола @@ -425,43 +470,60 @@ main.c ← {"ok":false,"error":"UNKNOWN_TEST"} — "id" не найден в реестре ← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт ← {"ok":false,"error":"BUSY"} — таргет выполняет тест +← {"ok":false,"error":"UID_READ_ERR"} — bsp_prov_read_uid() вернул ошибку ``` --- ### Интерактивные тесты -#### uSD — вставить карту +#### microSD — вставить карту (pre-confirm) ```bash -← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000} -→ {"type":"confirm","id":"usd_insert","confirmed":true} +← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} +→ {"type":"confirm","id":"usd","confirmed":true} ← {"type":"test_begin","id":"usd",...} -← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""} +← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} ``` -Если оператор отказался или таймаут: +confirm id для pre-confirm равен id теста (`usd`) — механизм `pre_confirm_prompt` +использует `mod->id`. Отказ или таймаут 30 с → `SKIP`. -```bash -← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"} -``` +#### Display RGB888 — подтвердить цвета и ротацию (in-run confirm) -#### Display RGB888 — подтвердить цвета - -Четыре шага R/G/B/W. Итог — AND всех подтверждений. +Шесть шагов: Red → Green → Blue → White, затем два ротационных +(диагностика непропаянных LR/UD пинов). Тест прерывается на **первом** +неподтверждённом шаге (короткое замыкание, не сбор всех ответов). ```bash ← {"type":"test_begin","id":"display",...} -← {"type":"confirm_request","id":"display_red","prompt":"Экран красный?","timeout_ms":15000} +← {"type":"confirm_request","id":"display_red","prompt":"Screen is solid red?","timeout_ms":15000} → {"type":"confirm","id":"display_red","confirmed":true} ← {"type":"confirm_request","id":"display_green",...} → {"type":"confirm","id":"display_green","confirmed":true} ← {"type":"confirm_request","id":"display_blue",...} → {"type":"confirm","id":"display_blue","confirmed":true} ← {"type":"confirm_request","id":"display_white",...} -→ {"type":"confirm","id":"display_white","confirmed":false} -← {"type":"test_result","id":"display","status":"fail","ms":22103, - "detail":"display_white not confirmed"} +→ {"type":"confirm","id":"display_white","confirmed":true} +← {"type":"confirm_request","id":"display_rot0","prompt":"Screen: left RED, right BLUE?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot0","confirmed":true} +← {"type":"confirm_request","id":"display_rot_base","prompt":"Left RED and right BLUE swapped sides?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot_base","confirmed":true} +← {"type":"test_result","id":"display","status":"pass",...} +``` + +При отказе/таймауте: `status:"fail"`, `detail:" not confirmed"`. + +#### MQS Audio — подтвердить слышимость тона (in-run confirm) + +Таргет ~4 с играет мелодию (A4, затем E5) через MQS + LM4875M, затем запрашивает +подтверждение: + +```bash +← {"type":"test_begin","id":"mqs",...} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} +→ {"type":"confirm","id":"mqs_tone","confirmed":true} +← {"type":"test_result","id":"mqs","status":"pass",...} ``` #### Кнопки — нажать физически @@ -472,36 +534,44 @@ main.c ```bash ← {"type":"test_begin","id":"buttons",...} -← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} [таргет ждёт bsp_button — без JSON confirm от хоста] -← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000} -← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""} +← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000} +← {"type":"test_result","id":"buttons","status":"pass",...} ``` +Таймаут 10 с → `SKIP` (не FAIL). + +> Полные потоки всех тестов (SDRAM/QSPI/opto/CAN, коды `detail`, HIL pytest) — +> в справочнике по тестированию `README_TESTING.md`. + --- ## Матрица тестов -| ID | Название | Тип | Critical | M5 HIL | Confirm | -| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- | -| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | -| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | -| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm | -| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() | -| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only | -| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | -| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ | -| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | +Порядок — как в реестре `k_registry[]` (`test_runner.c`). + +| № | ID | Название | Тип | Critical | M5 HIL | Confirm | +| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- | +| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | +| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ | +| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm | +| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() | +| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only | +| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) | +| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) | +| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() | **Типы confirm:** -- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`, - ждёт JSON-ответ через state machine (асинхронно). +- **pre_confirm** — `test_runner` отправляет `confirm_request` до вызова `run()`, + ждёт JSON-ответ через state machine (асинхронно). id = id теста. - **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`, блокируется до ответа (синхронно). -- **prompt only** — `protocol_send_confirm_request()` отправляется как UI-подсказка, +- **prompt only** — `protocol_send_confirm_request()` как UI-подсказка, хост не отвечает JSON, таргет ждёт физического события. +- **авто (HIL)** — confirm генерирует не оператор, а хост-оркестратор, командуя + M5StampPLC (см. `README_TESTING.md`). --- @@ -540,13 +610,13 @@ static test_result_t test_foo_run(void) return result; } -const test_module_t k_test_foo = { - .id = "foo", /* короткий ASCII-ключ */ +const test_module_t K_TEST_FOO = { + .id = "foo", /* короткий ASCII-ключ */ .name = "Foo Peripheral", .critical = false, /* true → run_all стопится при fail */ - .requires_hil = false, /* true → нужен M5StampPLC */ - .pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */ - .init = NULL, /* bsp_foo_init если нужен */ + .requires_hil = false, /* true → нужен M5StampPLC */ + .pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */ + .init = NULL, /* bsp_foo_init если нужен */ .run = test_foo_run, .deinit = NULL, }; @@ -558,15 +628,19 @@ const test_module_t k_test_foo = { ```c /* Forward declarations */ -extern const test_module_t k_test_sdram; -extern const test_module_t k_test_foo; /* ← добавить */ +extern const test_module_t K_TEST_SDRAM; +extern const test_module_t K_TEST_FOO; /* ← добавить */ static const test_module_t *const k_registry[] = { - &k_test_sdram, - &k_test_foo, /* ← добавить */ + &K_TEST_SDRAM, + ... + &K_TEST_FOO, /* ← добавить */ }; ``` +> При росте реестра выше `TEST_REGISTRY_MAX_SIZE` (`test_module.h`) сборка +> упадёт на `_Static_assert` в `test_runner.c` — увеличить константу. + ### Шаг 3 — Добавить в CMakeLists.txt **Файл:** `firmware/test/CMakeLists.txt` @@ -578,7 +652,6 @@ add_executable( src/cli.c src/protocol.c src/test_runner.c - src/tests/test_sdram.c src/tests/test_foo.c # ← добавить ... ) @@ -592,24 +665,18 @@ target_link_libraries( ### Шаг 4 — Обновить матрицу тестов -Добавить строку в таблицу в этом README. +Добавить строку в таблицу в этом README (и, если есть протокольный поток — +в `README_TESTING.md`). ### Шаблоны для разных типов тестов #### Self-тест с инициализацией ```c -static void test_foo_init(void) -{ - bsp_foo_init(); -} +static void test_foo_init(void) { bsp_foo_init(); } +static void test_foo_deinit(void) { bsp_foo_deinit(); } -static void test_foo_deinit(void) -{ - bsp_foo_deinit(); -} - -const test_module_t k_test_foo = { +const test_module_t K_TEST_FOO = { .id = "foo", .init = test_foo_init, .run = test_foo_run, @@ -622,7 +689,6 @@ const test_module_t k_test_foo = { ```c #include "test_runner.h" /* test_runner_wait_confirm() */ -#include "protocol.h" /* protocol_send_confirm_request() */ static test_result_t test_foo_run(void) { @@ -650,13 +716,13 @@ static test_result_t test_foo_run(void) #### Тест с pre_confirm (вставить карту, подключить кабель) ```c -const test_module_t k_test_foo = { +const test_module_t K_TEST_FOO = { .id = "foo", .pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK", .run = test_foo_run, ... }; -/* test_runner сам отправит confirm_request перед вызовом run() */ +/* test_runner сам отправит confirm_request (id = "foo") перед вызовом run() */ ``` --- @@ -727,34 +793,34 @@ void test_run_all_critical_fail_skips_remaining(void) | `cli_process()` | `FAKE_VOID_FUNC(cli_process)` | | `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению | -> **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель -> на стековую переменную внутри `execute_test()`. После возврата указатель -> инвалиден — используй `custom_fake` с `s_captured = *p_result` пока стек жив. - --- ## Версионирование -`FIRMWARE_TEST_VERSION` в `protocol.h` — единственная точка правды о версии. -Поле `"fw"` в `session_start` несёт эту строку. +Текущая версия ПО задается с помощью `project(firmware_test VERSION X.Y.Z)` +в `firmware/test/CMakeLists.txt`. CMake прокидывает её через +`configure_file(src/version.h.in → generated/version.h)`, откуда `protocol.h` +берёт `FIRMWARE_TEST_VERSION_STR`: -При несовместимых изменениях протокола (новое обязательное поле, изменение -семантики) — bumping версии + обновление этого документа. +``` +CMakeLists.txt: project(firmware_test VERSION 0.1.2) + │ configure_file(@ONLY) + ▼ +generated/version.h: FIRMWARE_TEST_VERSION_STR = "0.1.2" + │ + ▼ +protocol.h: #define FIRMWARE_TEST_VERSION FIRMWARE_TEST_VERSION_STR + │ + ▼ +session_start / version_response: "fw":"0.1.2" +``` -Хост должен сверять `"fw"` при подключении и предупреждать оператора при -несовпадении ожидаемой версии. +`version.h` генерируется, **не** редактируется вручную. Менять версию — +только в `CMakeLists.txt`. ---- +Хост может запросить версию явно (`get_version` → `version_response`) или +прочитать её из `session_start`, и предупредить оператора при несовпадении +с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле, +смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`. -## Архитектурные решения (закрыты) - -> Не пересматривать без явного запроса. - -| Решение | Обоснование | -| -------------------------------------------- | ------------------------------------------------------------------ | -| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) | -| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен | -| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC | -| IR и RTC — не реализуются | Вне scope рекламационной диагностики | -| Тесты атомарны | Инженер сам решает что проверять | -| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики | +## diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index 6738adf..0f4a221 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -1,10 +1,15 @@ # firmware_test — Руководство по тестированию -Тестовая прошивка входного контроля платы MIMXRT1052CVJ5B. +Тестовая прошивка входного контроля платы TFT индикатора. Транспорт: USB CDC ACM (J2). Протокол: JSON-lines v2, один JSON-объект на строку. Загружается в RAM через BootROM USB SDP — без предварительной прошивки загрузчика. -> Версия прошивки: `0.1.4` (`FIRMWARE_TEST_VERSION` в `protocol.h`) +> Версия прошивки: `0.1.2` — из `project(firmware_test VERSION X.Y.Z)` в +> `CMakeLists.txt` (через `version.h.in` → `version.h` → +> `FIRMWARE_TEST_VERSION_STR` в `protocol.h`). +> +> Архитектура, сборка, добавление тестов и host unit-тесты — в основном +> `README.md`. Этот документ — протокольные потоки по каждому тесту. --- @@ -17,8 +22,10 @@ | `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | | `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | | `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | -| `cmd` | `{"type":"cmd","cmd":"list_tests"}` | Получить реестр тестов с метаданными | | `cmd` | `{"type":"cmd","cmd":"run_selected","tests":["sdram","qspi"]}` | Запустить подмножество тестов | +| `cmd` | `{"type":"cmd","cmd":"list_tests"}` | Получить реестр тестов с метаданными | +| `cmd` | `{"type":"cmd","cmd":"get_uid"}` | Прочитать UID чипа (OCOTP) | +| `cmd` | `{"type":"cmd","cmd":"get_version"}` | Прочитать версию прошивки | | `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос | ### Исходящие события (target → host) @@ -30,8 +37,10 @@ | `test_begin` | `id`, `name`, `critical` | Тест запущен | | `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | | `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | -| `progress` | `test`, `step`, `status` | Прогресс внутри теста | +| `progress` | `test`, `step`, `status` | Прогресс внутри теста (usd) | | `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` / `run_selected` | +| `uid_response` | `uid` (16 hex, 8 байт big-endian) | Ответ на `get_uid` | +| `version_response` | `fw` (`X.Y.Z`) | Ответ на `get_version` | | `pong` | — | Ответ на `ping` | | `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | @@ -39,12 +48,14 @@ **Коды ошибок в `error`:** -| Код | Причина | -| -------------- | --------------------------------------------------------------- | -| `BUSY` | Предыдущий тест ещё выполняется | -| `UNKNOWN_TEST` | ID теста не найден в реестре | -| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) | -| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда | +| Код | Причина | +| --------------- | --------------------------------------------------------------- | +| `BUSY` | Предыдущий тест ещё выполняется | +| `UNKNOWN_TEST` | ID теста не найден в реестре | +| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) | +| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда | +| `LINE_TOO_LONG` | Входящая строка превысила 128 байт (`CLI_LINE_BUF_SIZE`) | +| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) | --- @@ -54,7 +65,7 @@ После подключения немедленно отправляет `session_start`: ```bash -← {"type":"session_start","fw":"0.1.4","target":"IMXRT1052","uptime_ms":1108} +← {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":1108} ``` ### Проверка канала (ping) @@ -64,6 +75,19 @@ ← {"type":"pong"} ``` +### Идентификация платы (get_uid / get_version) + +```bash +→ {"type":"cmd","cmd":"get_uid"} +← {"type":"uid_response","uid":"A1B2C3D4E5F60011"} + +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + +`uid` — 8 байт (`BSP_PROV_UID_LEN`) big-endian, 16 hex-символов без разделителей. +При ошибке чтения UID: `{"ok":false,"error":"UID_READ_ERR"}`. + --- ## SDRAM — контроль оперативной памяти @@ -179,7 +203,7 @@ sequenceDiagram | Время выполнения | < 1 с после вставки карты | Оператор вставляет карту по запросу. Тест запускается только после подтверждения. -Отказ или таймаут 30 с → `SKIP`. +Отказ или таймаут 30 с → `SKIP`. confirm id для pre-confirm равен id теста (`usd`). Пять шагов с `progress`-событиями: card detect → mount → write 4 KB → read/compare → unmount. @@ -246,7 +270,8 @@ sequenceDiagram **pre-confirm отсутствует** — `test_begin` отправляется сразу после `run`. -Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL): +Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL); +тест прерывается на первом неподтверждённом шаге: - **Этап 1 (все дисплеи):** Red → Green → Blue → White - **Этап 2 (TFT7/8/10):** паттерн Red/Blue + горизонтальный флип — диагностика непропаянных LR/UD пинов @@ -379,8 +404,9 @@ sequenceDiagram | Тип | Interactive (in-run confirm) | | Время выполнения | ~4 с воспроизведение + до 15 с на confirm | -Тест воспроизводит мелодию (~4 с: нота A4 затем E5) через MQS-выход -(`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M. +Тест воспроизводит мелодию (~4 с: нота A4 затем E5, стерео PCM16 44100 Гц) +через MQS-выход (`MQS_RIGHT`, `GPIO_AD_B0_04`) и усилитель LM4875M. +Буфер — статический в некэшируемой секции (OCRAM NonCacheable), L == R. Оператор подтверждает слышимость тона. ```mermaid @@ -422,147 +448,6 @@ sequenceDiagram --- -## Запуск всего набора (run_all) - -Тесты запускаются строго в порядке реестра. При провале критичного теста -(`sdram` или `qspi`) все последующие тесты получают `SKIP` с `detail:"critical test failed"`. - -```bash -→ {"type":"cmd","cmd":"run_all"} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} -← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} -← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} -← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} - ... оператор вставляет карту и подтверждает ... -← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} - ...progress events... -← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} -← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} - ...confirm цикл 6 шагов... -← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} -← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} -← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} - ... -← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} -← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} -← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} - ... оператор слышит и подтверждает ... -← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} -← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} - ...confirm цикл 6 HIL шагов... -← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} -← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} - ...confirm цикл 2 HIL шагов... -← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} -← {"type":"summary","passed":8,"failed":0,"skipped":0,"overall":"pass"} -``` - -**SKIP-каскад при critical fail:** - -```bash -← {"type":"test_result","id":"sdram","status":"fail","ms":1203,"detail":"addr=0x80200001..."} -← {"type":"test_begin","id":"qspi",...} -← {"type":"test_result","id":"qspi","status":"skip","ms":0,"detail":"critical test failed"} -← {"type":"test_begin","id":"usd",...} -← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} - ... -← {"type":"summary","passed":0,"failed":1,"skipped":7,"overall":"fail"} -``` - ---- - -## Реестр тестов — порядок выполнения - -| № | ID | Название | Critical | HIL | Тип | -| --- | --------- | ------------------ | -------- | --- | ---------------------------- | -| 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test | -| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test | -| 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | -| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | -| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | -| 6 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) | -| 7 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | -| 8 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | - ---- - -## Диагностика — строки detail - -| Тест | Значение `detail` | Диагноз | -| --------- | ----------------------------------------------- | ------------------------------------------- | -| `sdram` | `addr=0x... exp=0x.. got=0x..` | Сбой ячейки по адресу | -| `sdram` | `SEMC not ready — DCD failed?` | DCD не инициализировал SEMC | -| `qspi` | `JEDEC: mfr=0xFF exp=0xEF` | Чип не отвечает / не пропаян | -| `qspi` | `JEDEC: unknown cap=0x..` | Неизвестный тип чипа | -| `qspi` | `erase verify failed at 0x...` | Сектор не стирается | -| `qspi` | `rw mismatch at 0x... exp=0x.. got=0x..` | Ошибка записи или чтения | -| `qspi` | `addr alias: 0x... mirrors 0x... (3-byte wrap)` | Dedicated 4-byte opcodes не работают | -| `usd` | `no card detected` | Карта не вставлена в слот | -| `usd` | `mount failed: ` | `f_mount()` вернул FRESULT N | -| `usd` | `write failed: ` | `f_write()` вернул FRESULT N | -| `usd` | `compare failed at offset ` | Данные после чтения не совпадают | -| `display` | `display init failed` | `bsp_display_init()` вернул ошибку | -| `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | -| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | -| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | -| `mqs` | `mqs play error` | SAI3/DMA не запустился | -| `mqs` | `operator: no sound` | Нет звука / усилитель не работает | -| `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | -| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | -| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | -| `can` | `rx data mismatch: got XX XX XX XX` | Данные фрейма не совпадают | -| `can` | `tx failed: bsp_can_send returned ` | TX timeout или шина недоступна | -| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета | -| любой | `confirm timeout` | pre-confirm не получен за 30 с | -| любой | `operator declined` | Получен `"confirmed":false` | -| любой | `critical test failed` | Предшествующий критичный тест провалился | - ---- - -## list_tests — получить реестр тестов - -```bash -→ {"type":"cmd","cmd":"list_tests"} -← {"type":"test_list","tests":[ - {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, - {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, - {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, - {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, - {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, - {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, - {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, - {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true} - ]} -``` - -TUI использует этот ответ для динамического построения списка тестов. -HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC не подключён. - ---- - -## run_selected — запустить подмножество тестов - -```bash -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} -← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} -← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} -← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} - ...confirm цикл 6 шагов (HIL)... -← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} -← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} -``` - -Порядок выполнения — как в реестре таргета, не как в запросе. -Если хотя бы один ID не найден — вся команда отклоняется: - -```bash -→ {"type":"cmd","cmd":"run_selected","tests":["sdram","unknown_test"]} -← {"ok":false,"error":"UNKNOWN_TEST"} -``` - ---- - ## Оптоизолированные входы (HIL) | Параметр | Значение | @@ -583,7 +468,8 @@ HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC н | 6 | `opto_rs_inactive` | RLY2 OFF | `BSP_OPTO_CH_RS == INACTIVE` | HIL confirm полностью автоматический — TUI командует M5 и отправляет confirm -без участия оператора. +без участия оператора. После confirm тест выжидает settle (~30 мс, перекрывает +debounce 10 мс) и читает состояние через `bsp_opto_force_read()`. ```bash → {"type":"cmd","cmd":"run","id":"opto"} @@ -625,6 +511,150 @@ HIL confirm полностью автоматический — TUI команд --- +## list_tests — получить реестр тестов + +```bash +→ {"type":"cmd","cmd":"list_tests"} +← {"type":"test_list","tests":[ + {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, + {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, + {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, + {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, + {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, + {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, + {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, + {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false} + ]} +``` + +TUI использует этот ответ для динамического построения списка тестов. +HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC не подключён. +Порядок в ответе — порядок реестра `k_registry[]` (см. ниже). + +--- + +## run_selected — запустить подмножество тестов + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 шагов (HIL)... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} +``` + +Порядок выполнения — как в реестре таргета, не как в запросе. +Если хотя бы один ID не найден — вся команда отклоняется: + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","unknown_test"]} +← {"ok":false,"error":"UNKNOWN_TEST"} +``` + +--- + +## Запуск всего набора (run_all) + +Тесты запускаются строго в порядке реестра. При провале критичного теста +(`sdram` или `qspi`) все последующие тесты получают `SKIP` с `detail:"critical test failed"`. + +```bash +→ {"type":"cmd","cmd":"run_all"} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} +← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} + ... оператор вставляет карту и подтверждает ... +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} + ...progress events... +← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} +← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} + ...confirm цикл 6 шагов... +← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} +← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} + ... +← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 HIL шагов... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} + ...confirm цикл 2 HIL шагов... +← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} +← {"type":"test_begin","id":"mqs","name":"MQS Audio Out","critical":false} +← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} + ... оператор слышит и подтверждает ... +← {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +← {"type":"summary","passed":8,"failed":0,"skipped":0,"overall":"pass"} +``` + +**SKIP-каскад при critical fail:** + +```bash +← {"type":"test_result","id":"sdram","status":"fail","ms":1203,"detail":"addr=0x80200001..."} +← {"type":"test_begin","id":"qspi",...} +← {"type":"test_result","id":"qspi","status":"skip","ms":0,"detail":"critical test failed"} +← {"type":"test_begin","id":"usd",...} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} + ... +← {"type":"summary","passed":0,"failed":1,"skipped":7,"overall":"fail"} +``` + +--- + +## Реестр тестов — порядок выполнения + +Порядок соответствует `k_registry[]` в `test_runner.c`. + +| № | ID | Название | Critical | HIL | Тип | +| --- | --------- | ------------------ | -------- | --- | ---------------------------- | +| 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test | +| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test | +| 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | +| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | +| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | +| 6 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | +| 7 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | +| 8 | `mqs` | MQS Audio Out | ❌ | ❌ | Interactive (in-run confirm) | + +--- + +## Диагностика — строки detail + +| Тест | Значение `detail` | Диагноз | +| --------- | ----------------------------------------------- | ------------------------------------------- | +| `sdram` | `addr=0x... exp=0x.. got=0x..` | Сбой ячейки по адресу | +| `sdram` | `SEMC not ready — DCD failed?` | DCD не инициализировал SEMC | +| `qspi` | `JEDEC: mfr=0xFF exp=0xEF` | Чип не отвечает / не пропаян | +| `qspi` | `JEDEC: unknown cap=0x..` | Неизвестный тип чипа | +| `qspi` | `erase verify failed at 0x...` | Сектор не стирается | +| `qspi` | `rw mismatch at 0x... exp=0x.. got=0x..` | Ошибка записи или чтения | +| `qspi` | `addr alias: 0x... mirrors 0x... (3-byte wrap)` | Dedicated 4-byte opcodes не работают | +| `usd` | `no card detected` | Карта не вставлена в слот | +| `usd` | `mount failed: ` | `f_mount()` вернул FRESULT N | +| `usd` | `write failed: ` | `f_write()` вернул FRESULT N | +| `usd` | `compare failed at offset ` | Данные после чтения не совпадают | +| `display` | `display init failed` | `bsp_display_init()` вернул ошибку | +| `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | +| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | +| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | +| `mqs` | `mqs play error` | SAI3/DMA не запустился | +| `mqs` | `operator: no sound` | Нет звука / усилитель не работает | +| `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | +| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | +| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | +| `can` | `rx data mismatch: got XX XX XX XX` | Данные фрейма не совпадают | +| `can` | `tx failed: bsp_can_send returned ` | TX timeout или шина недоступна | +| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета | +| любой | `confirm timeout` | pre-confirm не получен за 30 с | +| любой | `operator declined` | Получен `"confirmed":false` | +| любой | `critical test failed` | Предшествующий критичный тест провалился | + +--- + ## HIL pytest — автоматическая верификация через firmware_test CDC Два файла тестируют `test_opto` и `test_can` через реальный CDC-протокол v2. diff --git a/tools/production/docs/RELEASE_ROADMAP.md b/tools/production/docs/RELEASE_ROADMAP.md new file mode 100644 index 0000000..3ccee56 --- /dev/null +++ b/tools/production/docs/RELEASE_ROADMAP.md @@ -0,0 +1,467 @@ +# service-tui — Roadmap до релиза v1 (фазы 4a → 4b → 5 → 6) + +> Продолжение `MONOLITH_APP_PLAN.md` (V4). Фазы 0–3 закрыты, Фаза 4 +> закрыта частично — деструктивные гейты на железе вскрыли пробел в +> типизации обрыва USB (см. Фаза 4a). Этот документ — дорожная карта +> оставшегося пути до релиза. +> +> Ветка: `feature-tui-monolith` от `dev`. Каждая фаза = коммит(ы) с +> зелёным гейтом; откат любой фазы не ломает предыдущие. + +--- + +## Статус на входе + +| Фаза | Статус | +| --- | --- | +| 0 — Спайк / де-риск | ✅ Закрыта (⚠В1/⚠В2/Р7/О1 сняты, golden-тест byte-exact) | +| 1 — Backend-модуль | ✅ Закрыта (`flash_backend.py`, `usb_ports.py`, 41 тест) | +| 2 — Пересадка `Flasher` | ✅ Закрыта (async-мост, гейт на железе) | +| 3 — Custom HAB in-process | ✅ Закрыта (де-факто вместе с фазой 1, подтверждена на железе) | +| 4 — Обработка отвала USB | ⚠️ **Частично** — see 4a | +| 4a — Добор типизации обрыва | ⏳ **Следующая** | +| 4b — Сокращение логов | ⏳ | +| 5 — Упаковка PyInstaller | ⏳ | +| 6 — Документация / релиз | ⏳ | + +### Почему Фаза 4 не закрыта + +Деструктивные гейты на железе (macOS) показали: **выдёргивание USB +проявляется тремя разными способами**, а код Фазы 4 корректно +типизирует только один. + +| Проявление в spsdk | Что реально прилетает | Фаза 4 (сейчас) | Пользователь видит | +| --- | --- | --- | --- | +| write-фаза, обрыв при записи | `SPSDKConnectionError` | обёрнут только вокруг `with SDP`/`with McuBoot`, но реальный путь `write_memory` прошёл мимо | «Непредвиденная ошибка» (safety net) | +| read-фаза после write | `SPSDKTimeoutError` (потомок `SPSDKError`, **не** `SPSDKConnectionError`) | не ловится обёрткой обрыва | «Непредвиденная ошибка» (safety net) | +| chip erase по таймауту | `flash_erase_all()` вернул `False` (без исключения) | ветка `if not ok: raise FlashBackendError(...)` | «flash_erase_all вернул False» | + +План Фазы 4 буквально требовал «`McuBootConnectionError`/**таймауты** → +`FlashBackendError(connection_lost=True)`». Таймаут (`SPSDKTimeoutError`) +и `False`-по-таймауту не были покрыты — значит Гейт 4 по букве не пройден. +Это добор, а не новая работа сверх плана. + +> **Важно (UX-надёжность уже работает):** safety net (`except Exception` +> в `flasher.py`) во всех трёх случаях корректно вернул `ok=False`, +> разблокировал кнопки, оставил приложение живым. Проблема +> исключительно в *формулировке* сообщения, не в устойчивости. + +--- + +## Принятые решения этого этапа + +| ID | Решение | +| --- | --- | +| **Р10** | Erase-таймаут (`False` без исключения) переклассифицируется в `ConnectionLostError` **вариантом B**: после `False` выполнить быстрый `detect_sdp()` — если устройство пропало с шины, это обрыв; если на месте — честная ошибка операции. | +| **Р11** | Троттлинг `write`-событий в `#flash-log` — каждые **10%** (≈10 строк вместо ~135). Прогресс-бар обновляется на **каждом** событии (плавность не теряется), в лог пишется прореженно. | +| **Р12** | Логирование: root/`spsdk`/`libusbsio` понижаются до `WARNING` по умолчанию; полный DEBUG доступен через env-переключатель (диагностика не теряется совсем). | +| **О2 (закрыт)** | Состав `firmware/` в релизе — только `firmware_test`; тип сборки (Debug/Release) управляется через `.env` (`FIRMWARE_BUILD_TYPE`), механизм уже реализован в `flasher.py`. | +| **POST-1** | Циклический прогон неинтерактивных тестов (SDRAM/NOR/OPTO/CAN) на `DiagScreen` — **отложен на пост-релиз**, вне `MONOLITH_APP_PLAN.md`. Зафиксирован, чтобы не потерять. | +| **Предложение 2** | Кнопка «Выйти из приложения» на `WaitingScreen` — принято, включается в Фазу 5 (UI-полировка перед упаковкой). | + +--- + +## Фаза 4a — Добор: корректная типизация обрыва USB + +**Цель:** все три проявления обрыва USB дают пользователю единое +понятное сообщение «Соединение с платой потеряно», а не «Непредвиденная +ошибка» / «flash_erase_all вернул False». + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/flash_backend.py` | правки: расширить обёртку обрыва на `SPSDKTimeoutError`; реализовать вариант B для erase | +| `tests/test_flash_backend.py` | правки: тесты на `SPSDKTimeoutError`-путь и на erase-переклассификацию | + +### Содержание + +1. **Обёртка обрыва расширяется** с `SPSDKConnectionError` на общий + родительский случай, покрывающий и `SPSDKTimeoutError`. Оба — + потомки `SPSDKError`, но `SPSDKTimeoutError` **не** наследует + `SPSDKConnectionError`, поэтому текущий `except SPSDKConnectionError` + его пропускает. Ловим оба явным кортежем + `(SPSDKConnectionError, SPSDKTimeoutError)` в трёх местах: + `load_flashloader`, `flash` (основная + ram_only ветки), `erase_chip`. + +2. **Вариант B для erase** (Р10): в `erase_chip` (и в `flash`, где + команды возвращают `False` по тем же причинам) — при `ok == False` + выполнить быстрый `detect_sdp()`; если устройство исчезло с шины → + `ConnectionLostError`, иначе → обычный `FlashBackendError` с прежним + текстом. Проверка `detect_sdp()` добавляется **только в error-путь**, + на happy path не влияет. + +3. **`_format_error_message` в `flasher.py` не трогается** — он уже + корректно даёт префикс «Соединение с платой потеряно» для любого + `connection_lost=True`. Достаточно, чтобы backend правильно поднял + `ConnectionLostError`. + +### Гейт 4a + +- [ ] Юнит-тест: `SPSDKTimeoutError` из `write_memory` → `ConnectionLostError` + (мок). +- [ ] Юнит-тест: `flash_erase_all` → `False` + `detect_sdp()==False` → + `ConnectionLostError`; `False` + `detect_sdp()==True` → + обычный `FlashBackendError` (мок). +- [ ] Существующие 41 тест зелёные (регрессии нет). +- [ ] **Железо (повтор деструктивных сценариев):** + - [ ] Выдернуть USB во время `write-memory` → в `#flash-log` + «Соединение с платой потеряно», не «Непредвиденная ошибка». + - [ ] Выдернуть во время chip erase → то же сообщение. + - [ ] Повторная вставка → прошивка успешна (порт не «занят»). +- [ ] macOS + Windows. + +--- + +## Фаза 4b — Сокращение логов + +**Цель:** лог-файл прошивки уменьшается на порядок; `#flash-log` в TUI +показывает осмысленный прогресс, а не ~135 однотипных строк. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/main.py` | правки: уровни логгеров (Р12) + env-переключатель DEBUG | +| `app/screens/flash.py` | правки: троттлинг `write`-событий в `_on_progress` (Р11) | + +### Содержание + +1. **Уровни логгеров (Р12):** root по умолчанию `INFO`; `spsdk`, + `libusbsio`, `libusbsio.hidapi.dev`, `spsdk.mboot.protocol.bulk_protocol` + → `WARNING` (именно они дают портянки HID-байтов). Полный DEBUG + включается через переменную окружения (например + `SERVICE_LOG_LEVEL=DEBUG`) — точное имя согласовать, не изобретаю. + +2. **Троттлинг `#flash-log` (Р11):** в `flash.py::_on_progress` + прогресс-бар обновляется всегда, а `write_line` в лог для фазы + `write` — только при пересечении 10%-границы (0/10/20/…/100). + Остальные фазы (`configure`/`erase`/`fcb`/`reset`/`done`/`error`/ + `hab_build`) логируются как есть — их немного. + +### Гейт 4b + +- [ ] Лог-файл одной прошивки на порядок короче; DEBUG-портянок + spsdk/libusbsio нет при дефолтном уровне. +- [ ] `SERVICE_LOG_LEVEL=DEBUG` (или согласованное имя) возвращает + полный DEBUG — диагностика доступна. +- [ ] `#flash-log`: ~10 строк прогресса записи вместо ~135, бар + по-прежнему плавный. +- [ ] Регрессия: прошивка/erase/диагностика на железе работают. + +--- + +## Фаза 5 — Упаковка PyInstaller + UI-полировка + +**Цель:** один исполняемый бандл на Windows и macOS, проходящий полный +полевой цикл на чистой машине без Zadig/сети/Python. Плюс кнопка +«Выйти» на `WaitingScreen`. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `tools/production/service_tui.spec` | новый — PyInstaller spec | +| `app/screens/waiting.py` | правки: кнопка «Выйти из приложения» (Предложение 2) | +| just-рецепт | новый — имя задачи согласовать, **не изобретаю** | +| `app/app.tcss` | правки при необходимости — стиль кнопки Quit на Waiting | + +### Содержание spec (из плана V4, §Фаза 5) + +- `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` при необходимости — + документированный NXP механизм для frozen); +- **`collect_dynamic_libs("libusbsio")`** — нативный HID-транспорт + (следствие Р7); `libusb-1.0.*` в бандле **отсутствует**; +- `datas`: `tools/host/dcd/{dcd.bin, w25q128_fdcb.bin, w25q512_fdcb.bin, + ivt_flashloader.bin}` → `data/`; +- `datas`: `firmware//firmware_test_hab.bin` (Type из `.env`, О2); +- `datas`: `pyproject.toml` (для `_read_app_version` во frozen); +- onedir (не onefile — onefile замедляет старт распаковкой); +- резолвер путей backend'а уже готов: frozen → `sys.executable.parent` + (`firmware_hab_path`, `_resolve_custom_binaries_dir`). + +Целевая структура бандла: + +``` +service-tui-vX.Y.Z-/ +├── service_tui[.exe] +├── _internal/ +│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data +│ └── ... ← рантайм PyInstaller, libusbsio +├── firmware/ +│ └── /firmware_test_hab.bin +└── custom_binaries/ ← пустая +``` + +### UI-полировка (Предложение 2) + +Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично +`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`). + +### Гейт 5 (Windows + macOS) + +- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный + полевой цикл — детект SDP → firmware_test → диагностика → + custom (W25Q128 и W25Q512) → chip erase. +- [ ] То же на macOS. +- [ ] Версия на `WaitingScreen` корректна во frozen. +- [ ] Порты резолвятся при перетыкании в другой физический USB-порт + (проверка Р8 на обеих ОС). +- [ ] Кнопка «Выйти» на `WaitingScreen` работает. +- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны — + подтверждено О1) виден во frozen-бандле. + +--- + +## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз + +**Цель:** синхронизировать документацию с реальностью монолита, +провести отложенную зачистку комментариев/grep, собрать релизный +артефакт из тега. + +### Файлы + +| Файл | Тип правки | +| --- | --- | +| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) | +| `CHANGELOG.md` | правки | +| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap | +| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) | +| `HOW_TO_FLASH.md` | правки | +| `tools/production/README.md` | правки | +| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | + +### Содержание + +1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход + по всему коду — актуализировать docstring-провенансы («прямой порт + flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита. + Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто + в `app/`) — либо достигается, либо остаётся осознанно как + документация происхождения (решение по каждому вхождению). +2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess), + нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8), + нативная обработка отвала USB, упаковка одним exe. +3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план + RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1). +4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты), + `flash_backend.py` — production-TUI; независимые реализации (Р2). +5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk. +6. **Ограничение «одна плата на столе»** (О3) — в README. +7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как + запланированную пост-релизную фичу. +8. Тег релиза = версия из `pyproject.toml`. + +### Гейт 6 + +- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5). +- [ ] `just host::flash*`, `incoming`, `production` работают как раньше — + регрессия dev-пути. +- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на + релизном бинаре. +- [ ] POST-1 зафиксирован в бэклоге, не потерян. + +--- + +## Сводная последовательность и зависимости + +``` +4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1 +│ │ │ │ +│ │ │ └── доки, зачистка, тег, регрессия dev-пути +│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting +│ └── уровни логов + троттлинг #flash-log +└── типизация обрыва (SPSDKTimeoutError + erase вариант B) + +Блокеры перед фазами: + 4a: нет — старт сразу + 4b: нет — после 4a + 5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG + 6: все гейты 4a/4b/5 зелёные на железе +``` + +## Открытые мелочи (согласовать по ходу, не блокируют старт 4a) + +| Вопрос | Когда нужен | Предложение | +| --- | --- | --- | +| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) | +| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю | +| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-` (из плана) | + +--- + +## Риски этого этапа + +| Риск | Фаза | Митигация | +| --- | --- | --- | +| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает | +| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель | +| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле | +| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` | +| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет | + +--- + +# Приложение: работа в новом треде + +Этот roadmap рассчитан на продолжение в **новом чате без контекста** +предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы +новый тред стартовал без потерь. + +## A. Какой набор правил к чему применяется + +Проектные правила «Role & Hardware Context» (senior embedded C, i.MX +RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`, +CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`). + +**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в +`tools/production`. Поэтому: + +| Правило | Применимо к Python-работе roadmap? | +| --- | --- | +| Unified diffs, не полные переписывания | ✅ Да | +| «Какой файл / какая функция затронуты» — первым | ✅ Да | +| ASK при неоднозначности/противоречии | ✅ Да | +| Не изобретать just-таски / пути / структуру | ✅ Да | +| Проверять существующие файлы перед правкой | ✅ Да | +| No malloc/free в драйверах и ISR | ❌ C-специфично | +| NXP SDK HAL вместо raw-регистров | ❌ C-специфично | +| Doxygen на public API | ❌ (Python — docstrings, уже используются) | +| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) | +| CMake target_compile_options | ❌ Неприменимо | + +Когда/если roadmap коснётся C-части — C-правила снова в силе. + +## B. Первый вопрос на старте нового треда (не потерять) + +**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip` +предлагается обернуть в `try/except`, и **любую ошибку самой проверки** +(не только «устройство отсутствует») трактовать как обрыв — потому что +проверка и так выполняется только после уже случившегося сбоя, шина +нестабильна, и «не смог проверить» практически всегда означает «платы +нет». Требуется явное подтверждение этой трактовки перед написанием +кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный +`FlashBackendError`.) + +## C. Файлы, которые нужно предоставить — по фазам + +Пути относительно `tools/production/`, если не указано иное. Пометка +**[есть в этом треде]** — файл уже фигурировал и его актуальная версия +известна; в новом треде его всё равно нужно приложить заново. + +### Фаза 4a — типизация обрыва + +| Файл | Зачем | +| --- | --- | +| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B | +| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию | +| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) | +| `app/models.py` | контекст: `FlashProgress` | + +### Фаза 4b — логи + +| Файл | Зачем | +| --- | --- | +| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) | +| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) | +| `.env` / `.env.example` (`tools/production/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными | + +### Фаза 5 — упаковка PyInstaller + UI + +| Файл | Зачем | +| --- | --- | +| `pyproject.toml` (`tools/production/`) | зависимости, версия, `requires-python` — база для spec | +| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта | +| `app/main.py` | entry point для PyInstaller | +| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen | +| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit | +| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) | +| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла | +| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) | +| `project_tree.txt` или `ls -R tools/production` | реальная структура пакета `app/` для spec | +| существующий `.spec`, если уже есть | не изобретать заново | + +### Фаза 6 — документация и релиз + +| Файл | Зачем | +| --- | --- | +| `CHANGELOG.md` | дописать секцию монолита | +| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap | +| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) | +| `HOW_TO_FLASH.md` | актуализировать под TUI-backend | +| `tools/production/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI | +| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | +| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) | +| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) | + +## D. Полный список актуальных файлов монолита (снимок на входе) + +Чтобы в новом треде можно было приложить всё разом, если удобнее не +дробить по фазам. Актуальные (пост-Фаза-4) версии: + +``` +tools/production/ +├── pyproject.toml +├── app/ +│ ├── __init__.py +│ ├── app.py +│ ├── app.tcss +│ ├── main.py (точка входа — фактически в tools/production/main.py, см. pyproject scripts) +│ ├── models.py +│ ├── flasher.py ← Фаза 2/4, актуальная версия +│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест) +│ ├── usb_ports.py ← Фаза 1 +│ ├── firmware_client.py +│ ├── m5_client.py +│ ├── orchestrator.py +│ ├── boot_art.py +│ ├── widgets.py (или widgets/) +│ └── screens/ +│ ├── __init__.py +│ ├── waiting.py +│ ├── flash.py ← Фаза 4 (правлены комментарии) +│ ├── post_flash.py +│ ├── connection_watcher.py +│ └── diag/ +│ ├── __init__.py +│ ├── confirm_panel.py +│ ├── results.py +│ └── test_list.py +├── tests/ +│ ├── __init__.py +│ └── test_flash_backend.py ← 41 тест +├── spike/ (Фаза 0, в релиз не идёт) +│ ├── spike_hab.py +│ ├── spike_flash.py +│ └── spike_readback.py (диагностика Гейта 3, на будущее) +└── custom_binaries/ (пустая, для оператора) + +tools/host/ (dev-CLI, Р2 — НЕ трогается) +├── flash_usb.py +└── dcd/ + ├── ivt_flashloader.bin + ├── dcd.bin + ├── w25q128_fdcb.bin + └── w25q512_fdcb.bin +``` + +> Примечание: `main.py` в `pyproject.toml` прописан как +> `service-tui = "main:main"` — точка входа лежит в +> `tools/production/main.py` (не в `app/`), а `app/app.py` содержит +> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5. + +## E. Что уже решено и не пересматривается (сводка для нового треда) + +- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже). +- **Р10** — erase-таймаут → вариант B (detect_sdp после False). +- **Р11** — троттлинг лога 10%. +- **Р12** — уровни логов + env DEBUG. +- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны. +- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`. +- **О3** — одна плата на столе, ограничение v1. +- **POST-1** — циклический прогон тестов, после релиза. +- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py` + меняются только там, где явно указано в roadmap. +- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе. +- Порядок ревью: один файл за раз, полные файлы для новых/целиком + переписываемых, unified diff для точечных правок.