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