# Added: cli, protocol, runner modules for firmware_test + host tests

This commit is contained in:
Dmitry Akimov 2026-04-17 17:24:22 +03:00
parent 74cef512c7
commit bf324b2aa2
36 changed files with 26965 additions and 1001 deletions

1
.gitignore vendored
View file

@ -69,6 +69,7 @@ compile_commands.json
.venv/
__pycache__/
*.pyc
*.bak
.env
.claude
tools/host/.venv-host/

View file

@ -144,12 +144,13 @@
"test_bsp_opto",
"test_bsp_button",
"test_bsp_can",
"test_cli",
"test_protocol",
"test_firmware_runner",
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_cli",
"test_prio_queue",
"test_timeout_pattern",
"uart_host_mock_example"
"test_timeout_pattern"
]
},
{
@ -163,11 +164,12 @@
"test_bsp_button",
"test_bsp_can",
"test_cli",
"test_protocol",
"test_firmware_runner",
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern",
"uart_host_mock_example"
"test_timeout_pattern"
]
},
{

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

520
docs/testing/PROTOCOL.md Normal file
View file

@ -0,0 +1,520 @@
# firmware_test — Протокол диагностики v2
> **Расположение в репозитории:** `docs/testing/PROTOCOL.md`
>
> Документ описывает протокол обмена между диагностической прошивкой
> (`firmware_test`) и хостовым ПО сервисного инженера.
> Актуален для: `firmware_test v0.1.0+`, `protocol.h v2`.
---
## Назначение и контекст
`firmware_test` — специализированная прошивка для диагностики плат
**MIMXRT1052CVJ5B**, вернувшихся по рекламации. Запускается сервисным
инженером через USB CDC ACM (разъём J2). Загружается через BootROM (USB SDP)
без предварительной прошивки загрузчика.
**Стенд:**
```bash
[Хост-ПК сервисного инженера]
│ USB CDC ACM (J2)
│ JSON-lines, 1 строка = 1 сообщение
[Плата MIMXRT1052 с firmware_test]
│ GPIO / LPUART / SEMC / FlexSPI / USDHC
[Периферия: SDRAM, QSPI Flash, uSD, Display, CAN, UART, Opto]
[M5StampPLC — управление внешними сигналами для HIL тестов]
```
**Принцип работы:**
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
интерактивными шагами.
- Инженер запускает тесты **атомарно** (один тест за раз) или все подряд
(`run_all`). Последовательность не фиксирована — инженер сам решает,
что проверять.
---
## Транспорт
| Параметр | Значение |
|---|---|
| Интерфейс | USB CDC ACM |
| Разъём | J2 |
| Кодировка | UTF-8 |
| Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается `\n` |
| Максимальная длина строки | 128 байт (включая `\n`) |
| CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) |
| Направление | Двунаправленный, half-duplex по логике |
Нет хэндшейка, нет sequence number, нет подтверждений доставки. При потере
строки хост повторяет команду — таргет идемпотентен для `ping` и `run`.
---
## Формат сообщений
Все сообщения — JSON-объекты в одну строку (`\n` в конце).
### Ключевые поля
Каждое сообщение содержит поле `"type"`, определяющее его смысл:
```
Хост → Таргет: "type": "cmd" — команда
"type": "confirm" — ответ оператора на интерактивный шаг
Таргет → Хост: "type": "session_start" — таргет готов
"type": "pong" — ответ на ping
"type": "test_begin" — тест стартовал
"type": "test_result" — тест завершён
"type": "confirm_request" — ожидание действия оператора
"type": "summary" — итог run_all
"ok": false, "error": "…" — ошибка протокола
```
---
## Жизненный цикл сессии
```bash
Хост Таргет
│ │
│ [прошивка загружена через USB SDP] │
│ [USB CDC установлен] │
│◄─── {"type":"session_start","fw":"0.1.0", │
│ "target":"IMXRT1052","uptime_ms":0} │
│ │
│──── {"type":"cmd","cmd":"ping"} ──────────►│
│◄─── {"type":"pong"} ────────────────────── │
│ │
│ [инженер выбирает тест] │
│ │
│──── {"type":"cmd","cmd":"run","id":"sdram"} ►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result","id":"sdram",...} │
│ │
│──── {"type":"cmd","cmd":"run_all"} ────────►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result","id":"sdram",...} │
│ … (каждый тест в реестре) … │
│◄─── {"type":"summary","overall":"pass",...} │
│ │
```
`session_start` отправляется **автоматически** при каждом старте таргета,
до получения первой команды. Хост должен быть готов принять его сразу после
открытия CDC порта.
---
## Команды хоста → таргет (`"type":"cmd"`)
### `ping`
Проверка связи. Таргет отвечает немедленно.
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
```
---
### `run` — запуск одного теста
Запустить тест по идентификатору. Если тест требует предварительного
подтверждения оператора (`pre_confirm_prompt` задан), таргет сначала пошлёт
`confirm_request`.
```json
→ {"type":"cmd","cmd":"run","id":"sdram"}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Если `id` не найден в реестре:
```json
← {"ok":false,"error":"UNKNOWN_TEST"}
```
---
### `run_all` — запуск всех тестов по реестру
Запускает все тест-модули в порядке реестра. Если тест помечен `"critical":true`
и вернул `"status":"fail"` — выполнение прерывается, остальные тесты
получают `"status":"skip"` в итоге (но `summary` всё равно отправляется).
```json
→ {"type":"cmd","cmd":"run_all"}
← {"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","name":"QSPI Flash 8 MB","critical":true}
← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""}
← ... (остальные тесты) ...
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
```
---
## События таргета → хост
### `session_start`
Таргет готов к работе. Отправляется автоматически при старте.
```json
{
"type": "session_start",
"fw": "0.1.0",
"target": "IMXRT1052",
"uptime_ms": 0
}
```
| Поле | Тип | Описание |
|---|---|---|
| `fw` | string | Версия firmware_test |
| `target` | string | Идентификатор платформы |
| `uptime_ms` | number | Время с момента старта, мс |
---
### `test_begin`
Тест начат. Отправляется непосредственно перед вызовом `run()`.
```json
{
"type": "test_begin",
"id": "sdram",
"name": "SDRAM 32 MB",
"critical": true
}
```
---
### `test_result`
Тест завершён (pass / fail / skip).
```json
{
"type": "test_result",
"id": "sdram",
"status": "pass",
"ms": 312,
"detail": ""
}
```
| `status` | Смысл |
|---|---|
| `"pass"` | Тест пройден |
| `"fail"` | Тест провален; поле `detail` содержит описание |
| `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) |
Поле `detail` — произвольная ASCII-строка до 95 символов. При `pass` — пустая.
Примеры: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
---
### `confirm_request`
Таргет ожидает действия оператора. Используется интерактивными тестами:
display (подтвердить цвет), кнопки (нажать кнопку), uSD (вставить карту).
```json
{
"type": "confirm_request",
"id": "display_red",
"prompt": "Экран залит красным цветом?",
"timeout_ms": 15000
}
```
Хост должен отобразить `prompt` оператору и ждать его реакции. Если оператор
не ответил за `timeout_ms` — таргет переходит в `SKIP` для этого шага
автоматически. Хост может дублировать таймаут на своей стороне для UX, но
авторитетный таймаут — на таргете.
---
### `summary`
Итог `run_all`. Отправляется после завершения последнего теста в реестре или
после прерывания по critical fail.
```json
{
"type": "summary",
"passed": 6,
"failed": 1,
"skipped": 0,
"overall": "fail"
}
```
`"overall": "fail"` если хотя бы один `critical` тест провален.
`"overall": "pass"` если все `critical` тесты прошли (non-critical могут fail).
---
### `confirm` (хост → таргет)
Ответ оператора на `confirm_request`. Поле `"id"` должно совпадать с `id`
из `confirm_request`.
```json
→ {"type":"confirm","id":"display_red","confirmed":true}
```
Если `"confirmed": false` — таргет записывает `TEST_STATUS_FAIL` для этого шага.
Если ответ пришёл после истечения `timeout_ms` — таргет игнорирует его
(уже перешёл в SKIP).
---
### Ошибки протокола
```json
← {"ok":false,"error":"PARSE_ERR"}
← {"ok":false,"error":"UNKNOWN_CMD"}
← {"ok":false,"error":"UNKNOWN_TEST"}
← {"ok":false,"error":"LINE_TOO_LONG"}
← {"ok":false,"error":"BUSY"}
```
| Код | Причина |
|---|---|
| `PARSE_ERR` | Строка не является валидным JSON-lines запросом |
| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение |
| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре |
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
---
## Матрица тестов
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|---|---|---|---|---|---|
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) |
| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
| `can` | CAN | HIL | ❌ | ✅ | ❌ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ |
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы тестов:**
- **self** — таргет тестирует периферию самостоятельно, без внешних сигналов.
- **interactive** — требует действия оператора через механизм `confirm_request`.
- **HIL** — требует M5StampPLC для генерации внешних сигналов
(реле, CAN фреймы, UART echo).
---
## Интерактивные тесты — детальный поток
### uSD
Карта вставляется оператором по запросу. Тест не входит в критический путь.
```bash
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте карту microSD и нажмите OK","timeout_ms":30000}
→ {"type":"confirm","id":"usd_insert","confirmed":true}
← {"type":"test_begin","id":"usd",...}
[запись + верификация блока данных]
← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""}
```
Если оператор отказался (`"confirmed":false`) или истёк таймаут:
```bash
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}
```
---
### Display (RGB888)
Четыре шага: красный, зелёный, синий, белый. Итог — AND всех подтверждений.
Одновременно верифицируется подсветка (PWM включён).
```bash
← {"type":"test_begin","id":"display",...}
← {"type":"confirm_request","id":"display_red","prompt":"Экран залит красным?","timeout_ms":15000}
→ {"type":"confirm","id":"display_red","confirmed":true}
← {"type":"confirm_request","id":"display_green","prompt":"Экран залит зелёным?","timeout_ms":15000}
→ {"type":"confirm","id":"display_green","confirmed":true}
← {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
→ {"type":"confirm","id":"display_blue","confirmed":true}
← {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
→ {"type":"confirm","id":"display_white","confirmed":false}
← {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"}
```
---
### Кнопки (Test_But_1 / Test_But_2)
Два шага. Таргет ждёт физического нажатия через `bsp_button`, не через confirm.
`confirm_request` здесь используется как инструкция оператору — ответом является
не JSON, а сам факт нажатия кнопки, который таргет детектирует самостоятельно.
```bash
← {"type":"test_begin","id":"buttons",...}
← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите кнопку Test_But_1","timeout_ms":10000}
[таргет ждёт bsp_button_get(BTN_TEST_1) == PRESSED, таймаут 10 с]
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите кнопку Test_But_2","timeout_ms":10000}
[таргет ждёт bsp_button_get(BTN_TEST_2) == PRESSED, таймаут 10 с]
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
```
> **Важно:** для теста кнопок таргет не ждёт `{"type":"confirm",…}` от хоста.
> Хост отображает `prompt` оператору и ждёт следующего события от таргета.
> Нажатие детектируется прошивкой через `bsp_button`, не через CDC.
---
## Рекомендации для разработчика хостового ПО
### Открытие порта
```bash
1. Найти CDC ACM устройство (VID/PID NXP или зарегистрированный).
2. Открыть порт (любой baud rate — CDC игнорирует его).
3. Ждать строку с "type":"session_start" — таймаут 10 с.
4. Если не получен — переоткрыть порт или перезагрузить таргет.
```
### Чтение событий
```bash
- Читать побайтово или буфером, буферизировать до '\n'.
- Одна строка = одно JSON-сообщение.
- Неизвестное поле "type" — игнорировать (forward-compatibility).
- Парсить минимально: поле "type" определяет дальнейший разбор.
```
### Отправка команд
```bash
- Завершать каждую строку '\n' (не '\r\n').
- Не отправлять следующую команду до получения финального события
предыдущей (test_result или error).
- Исключение: "ping" можно отправлять в любой момент, если таргет не BUSY.
```
### Обработка confirm_request
```bash
1. Получить "confirm_request" → отобразить "prompt" оператору.
2. Дождаться реакции оператора (кнопка в UI, клавиша в TUI).
3. Исключение — тест "buttons": не отправлять confirm, просто ждать
следующего события от таргета.
4. Для всех остальных тестов — отправить:
{"type":"confirm","id":"<тот же id>","confirmed":true/false}
5. Хост может показывать countdown по timeout_ms для UX,
но не обязан — таргет сам завершит по таймауту.
```
---
## Реализация на стороне таргета
### Модули прошивки
```bash
firmware/test/src/
├── main.c — инициализация, главный цикл, вызов cli_process()
├── cli.h / cli.c — IO-слой: буферизация строк, диспатч по "type"
├── protocol.h / .c — сериализация исходящих событий через cli_send()
├── test_module.h — интерфейс тест-модуля (структура test_module_t)
├── test_runner.h/.c — реестр тестов, state machine, confirm механизм
└── tests/
├── test_sdram.c
├── test_qspi.c
├── test_usd.c
├── test_display.c
├── test_buttons.c
├── test_can.c
├── test_uart_ttl.c
├── test_uart_iso.c
└── test_opto.c
```
### State machine test_runner
```
┌─────────────────────────────────────────┐
│ IDLE │◄──────────────────┐
│ Ждём команду от хоста │ │
└───────────────┬─────────────────────────┘ │
│ cmd: run / run_all │
▼ │
┌─────────────────────────────────────────┐ │
│ PRE_CONFIRM │ │
│ pre_confirm_prompt != NULL? │ │
│ → protocol_send_confirm_request() │ │
└───────────────┬─────────────────────────┘ │
│ confirm получен / NULL │
▼ │
┌─────────────────────────────────────────┐ │
│ RUNNING │ │
│ protocol_send_test_begin() │ │
│ mod->init() если задан │ │
│ result = mod->run() │ │
│ mod->deinit() если задан │ │
│ protocol_send_test_result() │ │
└───────────────┬─────────────────────────┘ │
│ │
├── run_all: следующий тест ──────────────────┤
│ │
└── run_all завершён: protocol_send_summary() ┘
run: сразу → IDLE
```
### Добавление нового теста
1. Создать `firmware/test/src/tests/test_foo.c` с реализацией `test_result_t test_foo_run(void)`.
2. Объявить дескриптор:
```c
const test_module_t k_test_foo = {
.id = "foo",
.name = "Foo Peripheral",
.critical = false,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = NULL,
.run = test_foo_run,
.deinit = NULL,
};
```
3. Добавить `&k_test_foo` в реестр `test_runner.c` — одна строка.
4. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета.
---
## Версионирование протокола
Поле `"fw"` в `session_start` — версия прошивки, а не версия протокола.
При несовместимых изменениях протокола (новое обязательное поле, изменение
семантики существующего) — bumping `FIRMWARE_TEST_VERSION` в `protocol.h`
с соответствующим обновлением этого документа.
Хост должен проверять `"fw"` и предупреждать оператора при несовпадении
ожидаемой версии.

View file

@ -4,8 +4,14 @@
set(TARGET_NAME firmware_test)
add_executable(
${TARGET_NAME} src/main.c src/cli.c ${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
${TARGET_NAME}
src/main.c
src/cli.c
src/protocol.c
src/test_runner.c
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE})
target_include_directories(firmware_test PRIVATE src/)
@ -29,9 +35,14 @@ target_link_libraries(
# map-файл для анализа размещения символов -T — линкерный
# скрипт с описанием карты памяти IMXRT1052
target_link_options(
${TARGET_NAME} PRIVATE -Wl,--gc-sections -Wl,--print-memory-usage
${TARGET_NAME}
PRIVATE
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map
-T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor.ld)
-Wl,--defsym=__stack_size__=0x1000
-Wl,--defsym=__heap_size__=0x1000
-T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld)
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR})

View file

@ -1,162 +1,760 @@
# firmware_test
Bare-metal прошивка входного контроля платы **MIMXRT1052CVJ5B**.
Загружается через USB ROM (SDP) в Flash. Запускается автономно при включении.
> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по
> рекламации. Запускается сервисным инженером через USB CDC ACM без
> предварительной прошивки загрузчика.
>
> Версия прошивки: `0.1.0` | Протокол: v2
---
## Канал связи
## Содержание
Единственный транспорт: **USB CDC ACM** (USB1 / EHCI0, разъём J2).
UART (MCU-Link VCOM) не используется — только в HIL ELF прошивках.
- [Быстрый старт](#быстрый-старт)
- [Архитектура](#архитектура)
- [Стенд](#стенд)
- [Модульная структура](#модульная-структура)
- [Граф зависимостей](#граф-зависимостей)
- [State machine test_runner](#state-machine-test_runner)
- [Протокол v2](#протокол-v2)
- [Транспорт](#транспорт)
- [Жизненный цикл сессии](#жизненный-цикл-сессии)
- [Команды хоста → таргет](#команды-хоста--таргет)
- [События таргета → хост](#события-таргета--хост)
- [Ошибки протокола](#ошибки-протокола)
- [Интерактивные тесты](#интерактивные-тесты)
- [Матрица тестов](#матрица-тестов)
- [Как добавить новый тест](#как-добавить-новый-тест)
- [Host unit-тесты](#host-unit-тесты)
- [Версионирование](#версионирование)
---
## Быстрый старт
### 1. Сборка (devcontainer)
```bash
Хост (pytest / minicom / pyserial)
└── USB CDC ACM (J2)
└── firmware_test
└── cli.c → dispatch → обработчик команды
just build::build-firmware-test-debug
just build::hab-firmware-test-debug
```
### 2. Прошивка (хост)
```bash
# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
just host::flash-test-debug
# Или через SWD (power cycle после)
just host::flash-swd-test-debug
```
### 3. Подключение
Подключить USB к разъёму **J2** (USB CDC ACM). Открыть любой терминал:
```bash
# macOS
screen /dev/cu.usbmodemXXXX
# Linux
screen /dev/ttyACM0
```
### 4. Работа с прошивкой
После подключения таргет сразу присылает:
```json
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
```
Проверка связи:
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
```
Запуск одного теста:
```json
→ {"type":"cmd","cmd":"run","id":"sdram"}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Запуск всех тестов:
```json
→ {"type":"cmd","cmd":"run_all"}
← ... (события каждого теста) ...
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
```
### 5. Host unit-тесты (devcontainer)
```bash
just build::test-host
```
---
## Протокол
## Архитектура
**JSON-lines**: каждая строка завершается `\n`.
| Направление | Формат |
|---|---|
| Запрос | `{"cmd":"NAME"}\n` |
| Успех | `{"ok":true,...}\n` |
| Ошибка | `{"ok":false,"error":"CODE"}\n` |
Ограничения:
- Максимальная длина строки: **128 байт** включая `\n` (`CLI_LINE_BUF_SIZE`)
- Терминатор: `\n` (LF, `0x0A`). `\r\n` (CR+LF) тоже принимается
---
## Команды
| Команда | Запрос | Успешный ответ | Описание |
|---|---|---|---|
| `PING` | `{"cmd":"PING"}` | `{"ok":true,"result":"PONG"}` | Проверка связи |
| `SDRAM_TEST` | `{"cmd":"SDRAM_TEST"}` | `{"ok":true,"time_ms":N}` | Быстрый тест SDRAM (≤100 мс) |
| `SDRAM_TEST_FULL` | `{"cmd":"SDRAM_TEST_FULL"}` | `{"ok":true,"time_ms":N}` | Полный тест SDRAM (~3060 с) |
Коды ошибок:
| Код | Причина |
|---|---|
| `UNKNOWN_CMD` | Команда не найдена в таблице |
| `PARSE_ERR` | Не найдено поле `"cmd"` в JSON |
| `LINE_TOO_LONG` | Строка превысила `CLI_LINE_BUF_SIZE` |
| `SDRAM_FAIL` | Тест SDRAM выявил ошибку чтения/записи |
---
## Последовательность старта
### Стенд
```bash
board_hw_init() тактирование, MPU, кэш, пины
bsp_tick_init() SysTick 1 мс
bsp_led_init() оба LED выключены
bsp_usb_cdc_init() PHY + USB стек + NVIC
[ожидание хоста] LED_HEARTBEAT мигает 200 мс
bsp_usb_cdc_is_ready() == true
LED_APP on
→ {"ok":true,"result":"READY"} сигнал готовности хосту
[главный цикл]
bsp_usb_cdc_poll()
cli_process()
[Хост-ПК сервисного инженера]
│ USB CDC ACM (J2) — единственный канал
│ JSON-lines, 1 строка = 1 сообщение
[Плата MIMXRT1052 с firmware_test]
│ GPIO / LPUART / SEMC / FlexSPI / USDHC
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto]
[M5StampPLC — управление внешними сигналами для HIL тестов]
(реле → EXT_IN1/IN2, RS_RX, CAN, UART echo)
```
**Принцип разделения ответственности:**
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
интерактивными шагами через `confirm`.
- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок
не фиксирован.
---
## Структура файлов
### Модульная структура
```bash
firmware/test/
├── CMakeLists.txt
├── README.md ← этот файл
└── src/
├── main.c # точка входа, init, main loop
├── cli.h # публичный API CLI
├── cli.c # буферизация RX, парсинг "cmd", dispatch
├── main.c — инициализация BSP, главный цикл
├── cli.h / cli.c — IO-слой
│ буферизация строк, парсинг "type",
│ диспатч на test_runner / protocol
├── protocol.h / .c — сериализация исходящих событий
│ все protocol_send_*() → cli_send()
├── test_module.h — интерфейс тест-модуля
│ test_module_t, test_result_t,
│ confirm_params_t, test_status_t
├── test_runner.h / .c — реестр + state machine
│ IDLE → PRE_CONFIRM → RUNNING → IDLE
│ test_runner_wait_confirm() для display
└── tests/
├── test_sdram.c # SDRAM_TEST / SDRAM_TEST_FULL [TODO]
├── test_sdram.h
├── test_led.c # LED_ON / LED_OFF [TODO]
├── test_led.h
├── test_button.c # BUTTON_READ [TODO]
└── test_button.h
├── test_sdram.c — SDRAM 32 MB (self)
├── test_qspi.c — QSPI Flash 8 MB (self)
├── test_usd.c — uSD SDIO (interactive)
├── test_display.c — Display RGB888 (interactive)
├── test_buttons.c — Test_But_1/2 (interactive)
├── test_can.c — CAN loopback (HIL)
├── test_uart_ttl.c — UART TTL (HIL)
├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL)
└── test_opto.c — Opto-in EXT_IN1/IN2 (HIL)
```
---
## Добавление новой команды
1. В `cli.c` объявить обработчик: `static void cmd_foo(void);`
2. Добавить строку в таблицу `k_cmds[]`: `{ "FOO", cmd_foo }`
3. Реализовать обработчик в секции `Command handlers`
4. Добавить тест в `tests/host/cli/test_cli.c`
5. Обновить таблицу команд в этом README
---
## Сборка и прошивка
### Граф зависимостей
```bash
# devcontainer
just build::build-firmware-test-debug
just build::hab-firmware-test-debug
# хост (плата в SDP-режиме: BOOT_MOD_1 → 3V3 → Reset)
just host::flash-test-debug
# вернуть в нормальный режим
# BOOT_MOD_1 → GND → Reset
main.c
├── bsp_board (тактирование, MPU, кэш, пины)
├── bsp_tick (SysTick 1 мс)
├── bsp_led (LED_HEARTBEAT, LED_APP)
├── bsp_usb_cdc (USB CDC ACM, единственный транспорт)
├── cli.c
│ └── bsp_usb_cdc (read / write)
│ └── protocol.c (send_error, send_pong)
│ └── test_runner.c (run_single, run_all, on_confirm)
├── protocol.c
│ └── cli.c (cli_send)
│ └── bsp_tick (bsp_tick_get_ms — для uptime)
└── test_runner.c
└── protocol.c (все protocol_send_*)
└── bsp_tick (bsp_tick_get_ms — таймауты confirm)
└── bsp_usb_cdc (bsp_usb_cdc_poll — в wait_confirm)
└── cli.c (cli_process — в wait_confirm)
└── tests/*.c (тест-модули через реестр)
```
**BSP-зависимости тест-модулей:**
| Тест | BSP модуль |
|---|---|
| `test_sdram` | `bsp_sdram` |
| `test_qspi` | `bsp_qspi` |
| `test_usd` | `bsp_usd` |
| `test_display` | существующий display BSP |
| `test_buttons` | `bsp_button` ✅ |
| `test_can` | `bsp_can` ✅ |
| `test_uart_ttl` | `bsp_uart_host` ✅ |
| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ |
| `test_opto` | `bsp_opto` ✅ |
---
## Тестирование
### State machine test_runner
```bash
# Host unit-тесты CLI парсера (devcontainer, без железа)
just build::test-host # → test_cli
# HIL тест USB CDC (хост, требует подключённой платы)
just host::hil-usb-cdc
# HIL тест SDRAM (хост, firmware_test прошит в Flash) ????????
just host::hil-sdram # быстрый
just host::hil-sdram-full # полный (~60 с)
cmd: run / run_all
┌─────────────────────────────────────┐
│ IDLE │◄──────────────────────────┐
│ Ждём команду от хоста │ │
└──────────────────┬──────────────────┘ │
│ │
pre_confirm_prompt != NULL? │
│ │
YES │ NO │
▼ │ ▼ │
┌───────────────┐ │ ┌──────────────────────────────────────┐ │
│ PRE_CONFIRM │ │ │ RUNNING │ │
│ │ │ │ protocol_send_test_begin() │ │
│ confirm_req │ │ │ mod->init() (если задан) │ │
│ отправлен, │ │ │ result = mod->run() ← блокирует │ │
│ ждём JSON │ │ │ mod->deinit() (если задан) │ │
│ от хоста │ └─►│ protocol_send_test_result() │ │
└──────┬────────┘ └──────────────────┬───────────────────┘ │
│ │ │
confirmed=true ──────────────────────► │ │
confirmed=false → SKIP │ │
timeout → SKIP │ │
│ │
run: IDLE ──┘ │
run_all: следующий тест в реестре ───┘
run_all done: protocol_send_summary()
```
**Ключевые свойства state machine:**
- `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`.
Пока тест выполняется, новые команды получают `BUSY`.
- `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов
(display). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`.
USB-стек остаётся живым, confirm приходит без возврата в главный цикл.
- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают
`SKIP` немедленно, `summary.overall = "fail"`.
---
## Переменные окружения
## Протокол v2
### Транспорт
| Параметр | Значение |
|---|---|
| Интерфейс | USB CDC ACM, разъём J2 |
| Кодировка | UTF-8 |
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
| Максимальная длина строки | 128 байт включая `\n` |
| CR+LF | Принимается (таргет отбрасывает `\r`) |
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
Таргет идемпотентен для `ping` и `run` — при потере строки хост повторяет.
---
### Жизненный цикл сессии
```bash
HIL_USB_CDC_PORT= # порт USB CDC таргета, например /dev/ttyACM1
HIL_USB_CDC_BAUD= # 115200
HIL_USB_CDC_TIMEOUT= # 5.0
Хост Таргет
│ │
│ [USB SDP: прошивка загружена] │
│ [CDC ACM: порт открыт] │
│◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически
│ │
│──── {"type":"cmd","cmd":"ping"} ─────────────►│
│◄─── {"type":"pong"} │
│ │
│──── {"type":"cmd","cmd":"run","id":"sdram"} ──►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result","id":"sdram",...} │
│ │
│──── {"type":"cmd","cmd":"run_all"} ───────────►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result",...} │
│ … по одному для каждого теста … │
│◄─── {"type":"summary","overall":"pass",...} │
```
Шаблон: `.env.example`. Актуальные значения: `.env` (не коммитится).
`session_start` отправляется **автоматически** при каждом старте, до получения
первой команды. Хост должен быть готов принять его сразу после открытия порта.
---
## Известные ограничения
### Команды хоста → таргет
- **Один пакет за цикл**: `cli_process()` читает один USB bulk-пакет за вызов
главного цикла. При потоке команд без задержки буфер может не успеть
обработаться — добавить задержку на стороне хоста между командами (≥10 мс).
- **TX неблокирующий**: если TX занят — ответ теряется. Хост должен ждать
предыдущий ответ перед отправкой следующей команды.
- **Нет персистентного состояния**: при сбросе питания все результаты теряются.
pytest должен повторно подключаться и дожидаться `READY`.
Все команды имеют `"type":"cmd"`. Поле `"cmd"` определяет действие.
#### `ping` — проверка связи
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
```
#### `run` — запуск одного теста
```json
→ {"type":"cmd","cmd":"run","id":"sdram"}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Если `id` не найден:
```json
← {"ok":false,"error":"UNKNOWN_TEST"}
```
#### `run_all` — запуск всех тестов по реестру
```json
→ {"type":"cmd","cmd":"run_all"}
← {"type":"test_begin","id":"sdram",...}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
← {"type":"test_begin","id":"qspi",...}
← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""}
← ... (остальные тесты) ...
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
```
#### `confirm` — ответ оператора на интерактивный шаг
```json
→ {"type":"confirm","id":"display_red","confirmed":true}
```
Поле `id` должно совпадать с `id` из `confirm_request`.
Ответ после истечения `timeout_ms` игнорируется — таргет уже перешёл в SKIP.
---
### События таргета → хост
#### `session_start`
```json
{
"type": "session_start",
"fw": "0.1.0",
"target": "IMXRT1052",
"uptime_ms": 0
}
```
#### `test_begin`
```json
{
"type": "test_begin",
"id": "sdram",
"name": "SDRAM 32 MB",
"critical": true
}
```
#### `test_result`
```json
{
"type": "test_result",
"id": "sdram",
"status": "pass",
"ms": 312,
"detail": ""
}
```
| `status` | Смысл |
|---|---|
| `"pass"` | Тест пройден |
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
#### `confirm_request`
```json
{
"type": "confirm_request",
"id": "display_red",
"prompt": "Экран залит красным цветом?",
"timeout_ms": 15000
}
```
Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете.
Хост может дублировать countdown для UX.
#### `summary`
```json
{
"type": "summary",
"passed": 6,
"failed": 1,
"skipped": 0,
"overall": "fail"
}
```
`"overall":"fail"` — если хотя бы один `critical` тест провален.
`"overall":"pass"` — все `critical` тесты прошли (non-critical могут fail).
---
### Ошибки протокола
```json
← {"ok":false,"error":"PARSE_ERR"} — строка не распознана как JSON-lines
← {"ok":false,"error":"UNKNOWN_CMD"} — неизвестный "cmd" или "type"
← {"ok":false,"error":"UNKNOWN_TEST"} — "id" не найден в реестре
← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт
← {"ok":false,"error":"BUSY"} — таргет выполняет тест
```
---
### Интерактивные тесты
#### uSD — вставить карту
```bash
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000}
→ {"type":"confirm","id":"usd_insert","confirmed":true}
← {"type":"test_begin","id":"usd",...}
← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""}
```
Если оператор отказался или таймаут:
```bash
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}
```
#### Display RGB888 — подтвердить цвета
Четыре шага R/G/B/W. Итог — AND всех подтверждений.
```bash
← {"type":"test_begin","id":"display",...}
← {"type":"confirm_request","id":"display_red","prompt":"Экран красный?","timeout_ms":15000}
→ {"type":"confirm","id":"display_red","confirmed":true}
← {"type":"confirm_request","id":"display_green",...}
→ {"type":"confirm","id":"display_green","confirmed":true}
← {"type":"confirm_request","id":"display_blue",...}
→ {"type":"confirm","id":"display_blue","confirmed":true}
← {"type":"confirm_request","id":"display_white",...}
→ {"type":"confirm","id":"display_white","confirmed":false}
← {"type":"test_result","id":"display","status":"fail","ms":22103,
"detail":"display_white not confirmed"}
```
#### Кнопки — нажать физически
**Особый случай:** `confirm_request` используется как инструкция оператору,
но хост **не отправляет `confirm`**. Таргет сам детектирует нажатие через
`bsp_button` и переходит к следующему событию.
```bash
← {"type":"test_begin","id":"buttons",...}
← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000}
[таргет ждёт bsp_button — без JSON confirm от хоста]
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000}
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
```
---
## Матрица тестов
| ID | Название | Тип | Critical | M5 HIL | Confirm |
|---|---|---|---|---|---|
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only |
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ |
| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы confirm:**
- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`,
ждёт JSON-ответ через state machine (асинхронно).
- **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`,
блокируется до ответа (синхронно).
- **prompt only**`protocol_send_confirm_request()` отправляется как UI-подсказка,
хост не отвечает JSON, таргет ждёт физического события.
---
## Как добавить новый тест
### Шаг 1 — Создать файл теста
```c
/* firmware/test/src/tests/test_foo.c */
#include "test_module.h"
#include "bsp/foo.h" /* BSP модуль тестируемой периферии */
#include "bsp/usb_cdc.h" /* bsp_usb_cdc_poll() для длинных тестов */
#include <stdint.h>
#include <stdio.h>
static test_result_t test_foo_run(void)
{
test_result_t result = { .status = TEST_STATUS_PASS, .duration_ms = 0U };
result.detail[0] = '\0';
/* Длинные операции должны периодически звать bsp_usb_cdc_poll(),
* чтобы USB-стек оставался живым пока run() блокирует главный цикл. */
bsp_foo_status_t status = bsp_foo_test();
if (status != BSP_OK)
{
result.status = TEST_STATUS_FAIL;
(void) snprintf(result.detail, TEST_DETAIL_SIZE,
"bsp_foo_test returned %d", (int) status);
}
return result;
}
const test_module_t k_test_foo = {
.id = "foo", /* короткий ASCII-ключ */
.name = "Foo Peripheral",
.critical = false, /* true → run_all стопится при fail */
.requires_hil = false, /* true → нужен M5StampPLC */
.pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */
.init = NULL, /* bsp_foo_init если нужен */
.run = test_foo_run,
.deinit = NULL,
};
```
### Шаг 2 — Зарегистрировать в реестре
**Файл:** `firmware/test/src/test_runner.c`
```c
/* Forward declarations */
extern const test_module_t k_test_sdram;
extern const test_module_t k_test_foo; /* ← добавить */
static const test_module_t *const k_registry[] = {
&k_test_sdram,
&k_test_foo, /* ← добавить */
};
```
### Шаг 3 — Добавить в CMakeLists.txt
**Файл:** `firmware/test/CMakeLists.txt`
```cmake
add_executable(
${TARGET_NAME}
src/main.c
src/cli.c
src/protocol.c
src/test_runner.c
src/tests/test_sdram.c
src/tests/test_foo.c # ← добавить
...
)
target_link_libraries(
${TARGET_NAME} PRIVATE
...
bsp_foo # ← добавить BSP модуль
)
```
### Шаг 4 — Обновить матрицу тестов
Добавить строку в таблицу в этом README.
### Шаблоны для разных типов тестов
#### Self-тест с инициализацией
```c
static void test_foo_init(void)
{
bsp_foo_init();
}
static void test_foo_deinit(void)
{
bsp_foo_deinit();
}
const test_module_t k_test_foo = {
.id = "foo",
.init = test_foo_init,
.run = test_foo_run,
.deinit = test_foo_deinit,
...
};
```
#### Интерактивный тест (confirm внутри run)
```c
#include "test_runner.h" /* test_runner_wait_confirm() */
#include "protocol.h" /* protocol_send_confirm_request() */
static test_result_t test_foo_run(void)
{
test_result_t result = { .status = TEST_STATUS_PASS };
confirm_params_t step = {
.id = "foo_step1",
.prompt = "Выполните действие и подтвердите",
.timeout_ms = 15000U,
};
if (!test_runner_wait_confirm(&step))
{
/* таймаут или отказ */
result.status = TEST_STATUS_SKIP;
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", "foo_step1 not confirmed");
return result;
}
/* продолжаем тест */
return result;
}
```
#### Тест с pre_confirm (вставить карту, подключить кабель)
```c
const test_module_t k_test_foo = {
.id = "foo",
.pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK",
.run = test_foo_run,
...
};
/* test_runner сам отправит confirm_request перед вызовом run() */
```
---
## Host unit-тесты
Фреймворк модулей firmware_test покрыт host unit-тестами (Unity + fff).
Тесты компилируются clang-17 на хосте без ARM-специфики.
### Что покрыто
| Таргет | Что тестирует | Тест-файл |
|---|---|---|
| `test_protocol` | сериализация JSON (все event types) | `tests/host/protocol/test_protocol.c` |
| `test_cli` | парсинг входящих строк, диспатч по type | `tests/host/cli/test_cli.c` |
| `test_firmware_runner` | state machine (IDLE/PRE_CONFIRM/RUNNING), реестр | `tests/host/runner/test_firmware_runner.c` |
### Запуск
```bash
# Все host-тесты
just build::test-host
# Только один тест (вербозный вывод Unity)
ctest --preset host-debug-test -R test_cli -V
# Напрямую
./build/host-debug/tests/host/test_cli
```
### Добавление host-теста для нового тест-модуля
Host unit-тесты для `test_sdram.c` и подобных — опциональны. Тест-модули
проверяются через HIL pytest (`tools/hil/`). Если в тест-модуле есть
нетривиальная логика (парсинг результатов, конечный автомат, retry) —
стоит добавить host-тест.
Гайд: `docs/testing/host/HOST_CREATE_TEST.md`.
### Моки и UNIT_TEST seam
`test_runner.c` компилируется с `-DUNIT_TEST` — это открывает seam для
подстановки тестового реестра без изменения production-кода:
```c
/* В тест-файле предоставляем свои модули */
const test_module_t *g_unit_test_registry[8];
size_t g_unit_test_registry_size = 0U;
static void set_registry(const test_module_t **pp_mods, size_t count) { ... }
void test_run_all_critical_fail_skips_remaining(void)
{
const test_module_t *mods[] = { &K_MOD_PASS, &K_MOD_CRIT_FAIL, &K_MOD_PASS };
set_registry(mods, 3U);
test_runner_run_all();
/* assertions... */
}
```
Стандартный список моков для каждого теста:
| Зависимость | fff fake |
|---|---|
| `cli_send()` | `FAKE_VOID_FUNC(cli_send, const char *)` + custom_fake с копией |
| `bsp_tick_get_ms()` | `FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms)` |
| `bsp_usb_cdc_poll()` | `FAKE_VOID_FUNC(bsp_usb_cdc_poll)` |
| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` |
| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению |
> **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель
> на стековую переменную внутри `execute_test()`. После возврата указатель
> инвалиден — используй `custom_fake` с `s_captured = *p_result` пока стек жив.
---
## Версионирование
`FIRMWARE_TEST_VERSION` в `protocol.h` — единственная точка правды о версии.
Поле `"fw"` в `session_start` несёт эту строку.
При несовместимых изменениях протокола (новое обязательное поле, изменение
семантики) — bumping версии + обновление этого документа.
Хост должен сверять `"fw"` при подключении и предупреждать оператора при
несовпадении ожидаемой версии.
---
## Архитектурные решения (закрыты)
> Не пересматривать без явного запроса.
| Решение | Обоснование |
|---|---|
| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) |
| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен |
| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC |
| IR и RTC — не реализуются | Вне scope рекламационной диагностики |
| Тесты атомарны | Инженер сам решает что проверять |
| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики |

View file

@ -1,244 +0,0 @@
# firmware_test — Architecture
> Target: NXP IMXRT1052CVJ5B
> Версия документа: 0.1
> Статус: draft
---
## 1. Назначение
`firmware_test` — входная тестовая прошивка для проверки работоспособности платы при производстве и во время разработки. Запускается напрямую через BootROM (USB Serial Download), без предварительной прошивки загрузчика. После успешного прохождения всех тестов инициирует фазу провижининга (привязка Chip UID к версиям ПО).
---
## 2. Workflow прошивки платы
```bash
BootROM (USB Serial Download, встроен в IMXRT1052)
firmware_test (залит напрямую)
↓ [тесты прошли, provisioning выполнен]
Флашим: Bootloader + App
Ждём heartbeat Bootloader → App
Плата принята
```
Вариант с предварительной заливкой загрузчика не используется — BootROM является надёжным и всегда доступным recovery-path, не зависящим от состояния Flash.
---
## 3. Высокоуровневая архитектура
```bash
┌─────────────────────────────────────────────────────┐
│ firmware_test │
│ │
│ USB CDC ──► Protocol ──► Test runner ──► Local UI │
│ (JSON-lines) (sequencer) (LED+disp) │
│ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ self-tests │ │ HIL tests │ │
│ │ SDRAM QSPI uSD │ │ CAN UART Opto-in │ │
│ │ RTC Display IR │ │ UART ISO IR burst │ │
│ └─────────────────────┘ └──────────────────────┘ │
│ │
│ HAL / BSP │
└─────────────────────────────────────────────────────┘
```
---
## 4. Компоненты
### 4.1 USB CDC
Единственный канал связи с внешним миром. Представляется хосту как виртуальный COM-порт. Инициализируется первым, до запуска тестов. При старте ожидает подключения хоста с таймаутом — если хост не подключился, тесты запускаются автономно.
### 4.2 Protocol
Протокол — **JSON-lines**: каждое сообщение является отдельным JSON-объектом, завершённым символом `\n`. Библиотека: cJSON из NXP SDK.
Направление **хост → плата** (команды):
```json
{"type":"cmd","cmd":"run_all"}
{"type":"cmd","cmd":"run","id":"sdram"}
{"type":"confirm","id":"display","confirmed":true}
```
Направление **плата → хост** (события):
```json
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
{"type":"test_begin","id":"sdram","name":"SDRAM 32MB","critical":true}
{"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":"32MB R/W OK"}
{"type":"confirm_request","id":"display","timeout_ms":15000}
{"type":"abort","reason":"critical_fail","id":"usd"}
{"type":"summary","passed":7,"failed":0,"skipped":2,"aborted":false,"overall":"pass"}
{"type":"provision_ready","chip_uid":"A3F2C1B400E70012"}
{"type":"provision_ack","fw":"1.0.0","bootloader":"1.0.0","recorded":true}
```
> **Примечание:** `uptime_ms` вместо Unix timestamp — RTC может быть не инициализирован на новой плате. Хост приклеивает реальное время самостоятельно.
### 4.3 Test runner
Центральный компонент. Хранит реестр тест-модулей, управляет порядком запуска, обрабатывает критические сбои, формирует `summary`.
**Порядок выполнения:**
1. Self-tests в порядке реестра
2. Проверка критических сбоев — если есть, HIL не запускается
3. HIL tests (только если Firefly подключён и self-tests прошли)
4. Summary report
5. Provisioning (только при `overall == pass`)
**Интерфейс тест-модуля (`test_module.h`):**
```cpp
typedef enum {
TEST_STATUS_PASS = 0,
TEST_STATUS_FAIL,
TEST_STATUS_SKIP,
} test_status_t;
typedef struct {
test_status_t status;
uint32_t duration_ms;
char detail[96]; /* диагностическая строка, опционально */
} test_result_t;
typedef struct {
const char *id; /* "sdram", "qspi", "can" — ключ в JSON */
const char *name; /* "SDRAM 32MB" — для display/лога */
bool critical; /* abort HIL если FAIL */
bool requires_hil; /* пропустить если Firefly не готов */
void (*init)(void);
test_result_t (*run)(void);
void (*deinit)(void);
} test_module_t;
```
### 4.4 Local UI
Отображает текущее состояние тестирования на светодиодах и дисплее. Получает события от Test runner. Дисплей при этом является частью тест-процесса (display_test).
**LED-паттерны:**
| Состояние | LED1 | LED2 |
|------------------------|-------------|-------------|
| Тест выполняется | мигает | выкл |
| Все тесты PASS | вкл | выкл |
| Есть FAIL | выкл | вкл |
| Ожидание подтверждения | оба мигают | |
---
## 5. Тест-модули
### 5.1 Self-tests
| ID | Название | Critical | Описание |
|------------|------------------|----------|--------------------------------------------------|
| `sdram` | SDRAM 32MB | ✅ | Write/read паттерны по всему объёму |
| `qspi` | QSPI Flash | ✅ | JEDEC ID + запись/чтение тестового сектора |
| `usd` | uSD (SDIO) | ✅ | Mount + R/W тестового файла (SKIP если нет карты)|
| `rtc` | RTC BM8563 | — | I2C presence, set/get time |
| `display` | Display RGB888 | — | R/G/B/W заливки, подтверждение оператором |
| `ir` | IR receiver | — | GPIO idle state HIGH, peripheral init |
**Display test — логика подтверждения:**
- Плата посылает `confirm_request` с `timeout_ms: 15000`
- Оператор нажимает **одну** кнопку: PASS или FAIL
- Если кнопка не нажата за 15 секунд — статус `SKIP` (ответственность на операторе)
- Одновременно проверяются обе кнопки — это полноценный тест кнопок
### 5.2 HIL tests (требуют Firefly AIO-3588Q)
| ID | Название | Стенд | Описание |
|--------------|------------------|------------------------------|----------------------------------------|
| `can` | CAN | Firefly CAN | Обмен фреймами, full-duplex |
| `uart_ttl` | UART TTL | Firefly UART | Echo паттерн |
| `uart_iso` | UART ISO +24V | Firefly UART + интерф. плата | Только RX, Firefly посылает |
| `opto` | Opto-in +24V | Firefly GPIO + интерф. плата | Все каналы, Firefly дёргает GPIO |
| `ir_hil` | IR burst | Firefly GPIO + IR LED | Приём burst 38 кГц, факт прерывания |
---
## 6. Provisioning
Выполняется после `summary: overall == pass`. Не является тестом — это отдельный этап жизненного цикла платы.
**Источник UID:** регистры OCOTP (One-Time Programmable fuses), 64-bit Chip UID. Читается через HAL.
**Интерфейс (`provisioning.h`):**
```c
typedef struct {
char chip_uid[17]; /* 64-bit UID как hex-строка, null-terminated */
char fw_version[16];
char bootloader_version[16];
bool provisioned;
} provision_info_t;
/* вызывается только при overall == PASS */
void provisioning_run(provision_info_t *out);
```
**Поток:**
```bash
плата посылает provision_ready + chip_uid
хост записывает в БД: uid ↔ fw_version ↔ bootloader_version
хост посылает provision_ack
плата устанавливает provisioned = true
```
Вся логика на стороне хоста (запись в БД, генерация сертификата, привязка партии). Прошивка только читает UID и ждёт подтверждения.
---
## 7. Реестр тестов
```c
/* test_registry.c */
static const test_module_t *tests[] = {
&test_sdram, /* critical */
&test_qspi, /* critical */
&test_usd, /* critical, SKIP если нет карты */
&test_rtc,
&test_display, /* operator confirm */
&test_ir, /* self-test уровень */
&test_can, /* requires_hil */
&test_uart_ttl, /* requires_hil */
&test_uart_iso, /* requires_hil */
&test_opto, /* requires_hil */
&test_ir_hil, /* requires_hil, SKIP если нет IR на стенде */
};
```
---
## 8. Статусы тестов
| Статус | Значение |
|--------|-------------------------------------------------------|
| `PASS` | Тест прошёл успешно |
| `FAIL` | Тест провален, в `detail` диагностическая информация |
| `SKIP` | Тест пропущен (нет карты, нет Firefly, таймаут оператора) |
---
## 9. Открытые вопросы
- [ ] Формат `detail` при FAIL для каждого теста (договориться между разработчиками)
- [ ] Handshake-протокол между firmware_test и Firefly (как плата узнаёт о готовности стенда)
- [ ] Полная схема интерфейсной платы для Firefly (оптовходы, IR LED, уровни +24V)
- [ ] GUI на сервере: формат отображения `summary` и хранение истории плат

View file

@ -1,268 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1360" viewBox="0 0 680 1180">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<style>
text { font-family: system-ui, sans-serif; fill: #1a1a1a; }
.th { font-size: 14px; font-weight: 500; }
.ts { font-size: 12px; font-weight: 400; fill: #555; }
.label-section { font-size: 11px; font-weight: 400; fill: #888; }
/* teal */
.c-teal rect, .c-teal circle { fill: #E1F5EE; stroke: #0F6E56; }
.c-teal .th { fill: #085041; }
.c-teal .ts { fill: #0F6E56; }
/* purple */
.c-purple rect { fill: #EEEDFE; stroke: #534AB7; }
.c-purple .th { fill: #3C3489; }
.c-purple .ts { fill: #534AB7; }
/* amber */
.c-amber rect { fill: #FAEEDA; stroke: #854F0B; }
.c-amber .th { fill: #633806; }
.c-amber .ts { fill: #854F0B; }
/* gray */
.c-gray rect { fill: #F1EFE8; stroke: #5F5E5A; }
.c-gray .th { fill: #2C2C2A; }
.c-gray .ts { fill: #5F5E5A; }
/* red */
.c-red rect { fill: #FCEBEB; stroke: #A32D2D; }
.c-red .th { fill: #501313; }
.c-red .ts { fill: #A32D2D; }
.arr { stroke: #888; stroke-width: 1; fill: none; }
.box-dashed { fill: none; stroke: #bbb; stroke-width: 0.8; stroke-dasharray: 5 3; }
</style>
</defs>
<!-- ═══════════════════════════════════════════════════════
DIAGRAM 1: High-level architecture
═══════════════════════════════════════════════════════ -->
<text x="340" y="30" text-anchor="middle" class="th" style="font-size:16px;fill:#1a1a1a;">firmware_test · high-level architecture</text>
<text x="340" y="48" text-anchor="middle" class="ts">NXP IMXRT1052CVJ5B</text>
<rect x="20" y="60" width="640" height="440" rx="16" class="box-dashed"/>
<text x="340" y="78" text-anchor="middle" class="label-section">firmware_test</text>
<!-- USB CDC -->
<g class="c-teal">
<rect x="40" y="88" width="160" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="120" y="108" text-anchor="middle" dominant-baseline="central">USB CDC</text>
<text class="ts" x="120" y="126" text-anchor="middle" dominant-baseline="central">virtual serial port</text>
</g>
<text class="ts" x="212" y="107" fill="#888">↔ Host PC</text>
<text class="ts" x="212" y="123" fill="#888">(терминал / GUI)</text>
<line x1="120" y1="138" x2="120" y2="180" class="arr" marker-end="url(#arrow)"/>
<!-- Protocol -->
<g class="c-purple">
<rect x="40" y="180" width="160" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="120" y="200" text-anchor="middle" dominant-baseline="central">Protocol</text>
<text class="ts" x="120" y="218" text-anchor="middle" dominant-baseline="central">JSON-lines · cJSON</text>
</g>
<!-- Test runner -->
<g class="c-purple">
<rect x="250" y="180" width="190" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="345" y="200" text-anchor="middle" dominant-baseline="central">Test runner</text>
<text class="ts" x="345" y="218" text-anchor="middle" dominant-baseline="central">sequencer + registry</text>
</g>
<!-- Local UI -->
<g class="c-amber">
<rect x="490" y="180" width="150" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="565" y="200" text-anchor="middle" dominant-baseline="central">Local UI</text>
<text class="ts" x="565" y="218" text-anchor="middle" dominant-baseline="central">LEDs + display</text>
</g>
<line x1="200" y1="205" x2="250" y2="205" class="arr" marker-start="url(#arrow)" marker-end="url(#arrow)" stroke="#666"/>
<line x1="440" y1="205" x2="490" y2="205" class="arr" marker-end="url(#arrow)" stroke="#666"/>
<path d="M345,230 L345,260 L180,260 L180,270" fill="none" stroke="#bbb" stroke-width="0.8" marker-end="url(#arrow)"/>
<path d="M345,230 L345,260 L505,260 L505,270" fill="none" stroke="#bbb" stroke-width="0.8" marker-end="url(#arrow)"/>
<!-- Self-tests region -->
<rect x="30" y="270" width="300" height="150" rx="8" class="box-dashed"/>
<text class="label-section" x="56" y="287">self-tests</text>
<g class="c-gray"><rect x="46" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="84" y="312" text-anchor="middle" dominant-baseline="central">SDRAM</text>
<text class="ts" x="84" y="328" text-anchor="middle" dominant-baseline="central">32 MB</text></g>
<g class="c-gray"><rect x="132" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="170" y="312" text-anchor="middle" dominant-baseline="central">QSPI</text>
<text class="ts" x="170" y="328" text-anchor="middle" dominant-baseline="central">W25Q128</text></g>
<g class="c-gray"><rect x="218" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="256" y="312" text-anchor="middle" dominant-baseline="central">uSD</text>
<text class="ts" x="256" y="328" text-anchor="middle" dominant-baseline="central">SDIO</text></g>
<g class="c-gray"><rect x="46" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="84" y="366" text-anchor="middle" dominant-baseline="central">RTC</text>
<text class="ts" x="84" y="382" text-anchor="middle" dominant-baseline="central">BM8563</text></g>
<g class="c-gray"><rect x="132" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="170" y="366" text-anchor="middle" dominant-baseline="central">Display</text>
<text class="ts" x="170" y="382" text-anchor="middle" dominant-baseline="central">operator ✓</text></g>
<g class="c-gray"><rect x="218" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="256" y="366" text-anchor="middle" dominant-baseline="central">IR</text>
<text class="ts" x="256" y="382" text-anchor="middle" dominant-baseline="central">idle GPIO</text></g>
<!-- HIL tests region -->
<rect x="350" y="270" width="290" height="150" rx="8" class="box-dashed"/>
<text class="label-section" x="376" y="287">HIL tests · Firefly AIO-3588Q</text>
<g class="c-teal"><rect x="366" y="295" width="120" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="426" y="312" text-anchor="middle" dominant-baseline="central">CAN</text>
<text class="ts" x="426" y="328" text-anchor="middle" dominant-baseline="central">frame exchange</text></g>
<g class="c-teal"><rect x="496" y="295" width="120" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="556" y="312" text-anchor="middle" dominant-baseline="central">UART TTL</text>
<text class="ts" x="556" y="328" text-anchor="middle" dominant-baseline="central">echo pattern</text></g>
<g class="c-teal"><rect x="366" y="349" width="120" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="426" y="366" text-anchor="middle" dominant-baseline="central">UART ISO</text>
<text class="ts" x="426" y="382" text-anchor="middle" dominant-baseline="central">+24V RX only</text></g>
<g class="c-teal"><rect x="496" y="349" width="120" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="556" y="366" text-anchor="middle" dominant-baseline="central">Opto-in</text>
<text class="ts" x="556" y="382" text-anchor="middle" dominant-baseline="central">+24V all ch.</text></g>
<!-- HAL / BSP -->
<line x1="340" y1="420" x2="340" y2="448" class="arr" marker-end="url(#arrow)" stroke="#aaa"/>
<g class="c-gray">
<rect x="30" y="448" width="620" height="40" rx="8" stroke-width="0.5"/>
<text class="th" x="340" y="468" text-anchor="middle" dominant-baseline="central">HAL / BSP</text>
</g>
<!-- ═══════════════════════════════════════════════════════
DIAGRAM 2: Test runner flow
═══════════════════════════════════════════════════════ -->
<text x="340" y="540" text-anchor="middle" class="th" style="font-size:16px;fill:#1a1a1a;">firmware_test · test runner flow</text>
<!-- Boot -->
<g class="c-gray">
<rect x="240" y="558" width="200" height="44" rx="8" stroke-width="0.5"/>
<text class="th" x="340" y="576" text-anchor="middle" dominant-baseline="central">Boot</text>
<text class="ts" x="340" y="592" text-anchor="middle" dominant-baseline="central">clock, USB CDC, HAL init</text>
</g>
<line x1="340" y1="602" x2="340" y2="630" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<!-- Wait for host -->
<g class="c-purple">
<rect x="240" y="630" width="200" height="44" rx="8" stroke-width="0.5"/>
<text class="th" x="340" y="648" text-anchor="middle" dominant-baseline="central">Wait for host</text>
<text class="ts" x="340" y="664" text-anchor="middle" dominant-baseline="central">USB connect or timeout</text>
</g>
<line x1="340" y1="674" x2="340" y2="698" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<!-- Self-tests block -->
<rect x="30" y="698" width="620" height="168" rx="10" class="box-dashed"/>
<text class="label-section" x="56" y="714">self-tests</text>
<g class="c-red">
<rect x="46" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
<text class="th" x="101" y="740" text-anchor="middle" dominant-baseline="central">SDRAM</text>
<text class="ts" x="101" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
<text class="ts" x="101" y="768" text-anchor="middle" dominant-baseline="central">write/read pattern</text>
</g>
<g class="c-red">
<rect x="166" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
<text class="th" x="221" y="740" text-anchor="middle" dominant-baseline="central">QSPI Flash</text>
<text class="ts" x="221" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
<text class="ts" x="221" y="768" text-anchor="middle" dominant-baseline="central">JEDEC ID + R/W</text>
</g>
<g class="c-red">
<rect x="286" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
<text class="th" x="341" y="740" text-anchor="middle" dominant-baseline="central">uSD</text>
<text class="ts" x="341" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
<text class="ts" x="341" y="768" text-anchor="middle" dominant-baseline="central">mount + R/W</text>
</g>
<g class="c-gray">
<rect x="406" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
<text class="th" x="461" y="740" text-anchor="middle" dominant-baseline="central">RTC</text>
<text class="ts" x="461" y="756" text-anchor="middle" dominant-baseline="central">I2C presence</text>
<text class="ts" x="461" y="768" text-anchor="middle" dominant-baseline="central">set / get</text>
</g>
<g class="c-amber">
<rect x="526" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
<text class="th" x="581" y="740" text-anchor="middle" dominant-baseline="central">Display</text>
<text class="ts" x="581" y="756" text-anchor="middle" dominant-baseline="central">R/G/B/W fill</text>
<text class="ts" x="581" y="768" text-anchor="middle" dominant-baseline="central">btn confirm</text>
</g>
<line x1="101" y1="774" x2="101" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<line x1="221" y1="774" x2="221" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<line x1="341" y1="774" x2="341" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<line x1="461" y1="774" x2="461" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<line x1="581" y1="774" x2="581" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<!-- Check critical failures -->
<g class="c-gray">
<rect x="46" y="794" width="590" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="341" y="812" text-anchor="middle" dominant-baseline="central">Check critical failures</text>
<text class="ts" x="341" y="828" text-anchor="middle" dominant-baseline="central">SDRAM || QSPI || uSD FAIL → abort HIL, report immediately</text>
</g>
<line x1="340" y1="866" x2="340" y2="890" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<!-- HIL block -->
<rect x="30" y="890" width="620" height="78" rx="10" class="box-dashed"/>
<text class="label-section" x="56" y="906">HIL tests · requires Firefly</text>
<g class="c-teal">
<rect x="46" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="96" y="930" text-anchor="middle" dominant-baseline="central">CAN</text>
<text class="ts" x="96" y="946" text-anchor="middle" dominant-baseline="central">full-duplex</text>
</g>
<g class="c-teal">
<rect x="156" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="206" y="930" text-anchor="middle" dominant-baseline="central">UART TTL</text>
<text class="ts" x="206" y="946" text-anchor="middle" dominant-baseline="central">echo</text>
</g>
<g class="c-teal">
<rect x="266" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="316" y="930" text-anchor="middle" dominant-baseline="central">UART ISO</text>
<text class="ts" x="316" y="946" text-anchor="middle" dominant-baseline="central">RX only</text>
</g>
<g class="c-teal">
<rect x="376" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="426" y="930" text-anchor="middle" dominant-baseline="central">Opto-in</text>
<text class="ts" x="426" y="946" text-anchor="middle" dominant-baseline="central">all channels</text>
</g>
<g class="c-gray">
<rect x="486" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
<text class="th" x="536" y="930" text-anchor="middle" dominant-baseline="central">IR burst</text>
<text class="ts" x="536" y="946" text-anchor="middle" dominant-baseline="central">38 kHz RX</text>
</g>
<line x1="340" y1="968" x2="340" y2="996" class="arr" marker-end="url(#arrow)" stroke="#888"/>
<!-- Summary -->
<g class="c-teal">
<rect x="160" y="996" width="360" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="340" y="1016" text-anchor="middle" dominant-baseline="central">Summary report</text>
<text class="ts" x="340" y="1034" text-anchor="middle" dominant-baseline="central">JSON · pass/fail/skip per test · overall</text>
</g>
<line x1="340" y1="1046" x2="340" y2="1074" class="arr" marker-end="url(#arrow)" stroke="#aaa" stroke-dasharray="4 3"/>
<!-- Provisioning -->
<g class="c-purple">
<rect x="160" y="1074" width="360" height="50" rx="8" stroke-width="0.5"/>
<text class="th" x="340" y="1094" text-anchor="middle" dominant-baseline="central">Provisioning</text>
<text class="ts" x="340" y="1112" text-anchor="middle" dominant-baseline="central">OCOTP chip_uid → host DB · only on pass</text>
</g>
<text class="ts" x="536" y="1050" fill="#aaa">only if overall == pass</text>
<!-- Legend -->
<rect x="30" y="1140" width="260" height="30" rx="6" fill="none" stroke="#eee" stroke-width="0.5"/>
<rect x="40" y="1151" width="12" height="8" rx="2" fill="#FCEBEB" stroke="#A32D2D" stroke-width="0.5"/>
<text class="ts" x="58" y="1159">critical test</text>
<rect x="130" y="1151" width="12" height="8" rx="2" fill="#E1F5EE" stroke="#0F6E56" stroke-width="0.5"/>
<text class="ts" x="148" y="1159">HIL test</text>
<rect x="200" y="1151" width="12" height="8" rx="2" fill="#FAEEDA" stroke="#854F0B" stroke-width="0.5"/>
<text class="ts" x="218" y="1159">operator</text>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,72 +1,60 @@
/**
* @file cli.c
* @brief Реализация CLI для firmware_test.
* @brief IO-слой и диспатчер сообщений v2 для firmware_test.
*
* Транспорт: USB CDC ACM через bsp_usb_cdc единственный канал.
* Парсинг JSON минималистичный: strstr по полю "cmd".
* Полноценный JSON-парсер (cJSON) не используется намеренно
* схема фиксирована, единственное входящее поле "cmd".
* Транспорт: USB CDC ACM через bsp_usb_cdc.
*
* Добавление новой команды:
* 1. Объявить static void cmd_foo(void); выше таблицы.
* 2. Добавить { "FOO", cmd_foo } в k_cmds[].
* 3. Реализовать обработчик ниже раздела "Command handlers".
* Парсинг минималистичный: strstr по фиксированным полям.
* cJSON не используется намеренно схема входящих сообщений фиксирована.
*
* Входящие типы:
* "cmd" handle_cmd() test_runner или protocol_send_pong()
* "confirm" handle_confirm() test_runner_on_confirm()
*
* Добавление новой команды типа "cmd":
* 1. Добавить ветку if (strcmp(cmd_name, "FOO") == 0) в handle_cmd().
* 2. Вызвать нужный обработчик из test_runner.h или protocol.h.
*
* Добавление нового входящего типа:
* 1. Добавить static void handle_<type>(const char *) ниже.
* 2. Добавить ветку if (strcmp(msg_type, "<type>") == 0) в process_line().
*/
#include "cli.h"
#include "bsp/usb_cdc.h"
#include "protocol.h"
#include "test_runner.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
/* ── Forward declarations ──────────────────────────────────────────────── */
/* ── Ключи полей JSON ──────────────────────────────────────────────────── */
static void cmd_ping(void);
/* ── Command table ─────────────────────────────────────────────────────── */
typedef void (*cli_handler_t)(void);
typedef struct
{
const char *name;
cli_handler_t handler;
} cli_cmd_t;
static const cli_cmd_t k_cmds[] = {
{ "PING", cmd_ping },
};
#define CLI_CMD_COUNT (sizeof(k_cmds) / sizeof(k_cmds[0]))
static const char K_FIELD_TYPE[] = "\"type\"";
static const char K_FIELD_CMD[] = "\"cmd\"";
static const char K_FIELD_ID[] = "\"id\"";
static const char K_FIELD_CONFIRMED[] = "\"confirmed\"";
/* ── RX line buffer ────────────────────────────────────────────────────── */
static uint8_t g_s_line_buf[CLI_LINE_BUF_SIZE];
static size_t g_s_line_len = 0U;
/* ── Internal helpers ──────────────────────────────────────────────────── */
/* ── Парсинг полей ─────────────────────────────────────────────────────── */
/**
* @brief Извлечь значение поля "cmd" из JSON-строки.
* @brief Извлечь строковое значение в кавычках после двоеточия.
*
* Ищет паттерн ` "cmd":"<ASCII без кавычек и обратных слэшей>"\n`. Без рекурсии и динамической памяти.
*
* @param[in] line NULL-terminated входная строка.
* @param[out] out Буфер для записи значения.
* @param[in] out_size Размер out (включая место под '\0').
* @return true если поле найдено и значение помещается в out.
* @param[in] p_after_key Позиция сразу после ключа в строке JSON.
* @param[out] p_out Буфер для результата.
* @param[in] out_size Размер p_out (включая место под '\0').
* @return true если значение найдено и помещается в p_out.
*/
static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
static bool extract_string_value(const char *p_after_key, char *p_out, size_t out_size)
{
const char *key = strstr(p_line, "\"cmd\"");
if (key == NULL)
{
return false;
}
const char *colon = strchr(key + 5U, ':');
const char *colon = strchr(p_after_key, ':');
if (colon == NULL)
{
return false;
@ -77,7 +65,7 @@ static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
{
return false;
}
open_q++; /* skip the opening quote */
open_q++;
const char *close_q = strchr(open_q, '"');
if (close_q == NULL)
@ -97,39 +85,197 @@ static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
}
/**
* @brief Найти и вызвать обработчик команды.
*
* Если команда не найдена отправить UNKNOWN_CMD.
* @brief Извлечь значение поля "type".
*/
static void dispatch(const char *p_cmd_name)
static bool parse_type_field(const char *p_line, char *p_out, size_t out_size)
{
for (size_t i = 0U; i < CLI_CMD_COUNT; i++)
const char *key = strstr(p_line, K_FIELD_TYPE);
if (key == NULL)
{
if (strcmp(k_cmds[i].name, p_cmd_name) == 0)
{
k_cmds[i].handler();
return;
return false;
}
}
cli_send("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n");
return extract_string_value(key + sizeof(K_FIELD_TYPE) - 1U, p_out, out_size);
}
/**
* @brief Обработать одну накопленную строку (без завершающего '\n').
* @brief Извлечь значение поля "cmd".
*/
static void process_line(const char *p_line)
static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
{
const char *key = strstr(p_line, K_FIELD_CMD);
if (key == NULL)
{
return false;
}
return extract_string_value(key + sizeof(K_FIELD_CMD) - 1U, p_out, out_size);
}
/**
* @brief Извлечь значение поля "id".
*/
static bool parse_id_field(const char *p_line, char *p_out, size_t out_size)
{
const char *key = strstr(p_line, K_FIELD_ID);
if (key == NULL)
{
return false;
}
return extract_string_value(key + sizeof(K_FIELD_ID) - 1U, p_out, out_size);
}
/**
* @brief Извлечь булево значение поля "confirmed".
*
* Ищет "confirmed":true или "confirmed":false без пробелов после двоеточия.
*
* @param[in] p_line NULL-terminated входная строка.
* @param[out] p_out Результат (true/false).
* @return true если поле найдено и значение распознано.
*/
static bool parse_confirmed_field(const char *p_line, bool *p_out)
{
const char *key = strstr(p_line, K_FIELD_CONFIRMED);
if (key == NULL)
{
return false;
}
const char *colon = strchr(key + sizeof(K_FIELD_CONFIRMED) - 1U, ':');
if (colon == NULL)
{
return false;
}
colon++;
while (*colon == ' ')
{
colon++;
}
if (strncmp(colon, "true", sizeof("true") - 1U) == 0)
{
*p_out = true;
return true;
}
if (strncmp(colon, "false", sizeof("false") - 1U) == 0)
{
*p_out = false;
return true;
}
return false;
}
/* ── Обработчики входящих сообщений ────────────────────────────────────── */
/**
* @brief Обработать команду "run": извлечь id и передать в test_runner.
*/
static void handle_cmd_run(const char *p_line)
{
const uint8_t MAX_ID_LEN = 32U;
char test_id[MAX_ID_LEN];
if (!parse_id_field(p_line, test_id, sizeof(test_id)))
{
protocol_send_error("PARSE_ERR");
return;
}
test_runner_run_single(test_id);
}
/**
* @brief Обработать сообщение {"type":"cmd",...}.
*
* Команды: ping pong, run_all test_runner, run test_runner.
*/
static void handle_cmd(const char *p_line)
{
const uint8_t MAX_CMD_LEN = 32U;
char cmd_name[MAX_CMD_LEN];
if (!parse_cmd_field(p_line, cmd_name, sizeof(cmd_name)))
{
cli_send("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n");
protocol_send_error("PARSE_ERR");
return;
}
dispatch(cmd_name);
if (strcmp(cmd_name, "ping") == 0)
{
protocol_send_pong();
return;
}
if (strcmp(cmd_name, "run_all") == 0)
{
test_runner_run_all();
return;
}
if (strcmp(cmd_name, "run") == 0)
{
handle_cmd_run(p_line);
return;
}
protocol_send_error("UNKNOWN_CMD");
}
/**
* @brief Обработать сообщение {"type":"confirm",...}.
*
* Извлекает id и confirmed, передаёт в test_runner_on_confirm().
*/
static void handle_confirm(const char *p_line)
{
const uint8_t MAX_ID_LEN = 32U;
char id_buf[MAX_ID_LEN];
bool confirmed = false;
if (!parse_id_field(p_line, id_buf, sizeof(id_buf)))
{
protocol_send_error("PARSE_ERR");
return;
}
if (!parse_confirmed_field(p_line, &confirmed))
{
protocol_send_error("PARSE_ERR");
return;
}
test_runner_on_confirm(id_buf, confirmed);
}
/**
* @brief Диспатчить накопленную строку по полю "type".
*/
static void process_line(const char *p_line)
{
const uint8_t MAX_TYPE_LEN = 16U;
char msg_type[MAX_TYPE_LEN];
if (!parse_type_field(p_line, msg_type, sizeof(msg_type)))
{
protocol_send_error("PARSE_ERR");
return;
}
if (strcmp(msg_type, "cmd") == 0)
{
handle_cmd(p_line);
return;
}
if (strcmp(msg_type, "confirm") == 0)
{
handle_confirm(p_line);
return;
}
protocol_send_error("UNKNOWN_CMD");
}
/* ── Public API ────────────────────────────────────────────────────────── */
@ -147,7 +293,6 @@ void cli_send(const char *p_resp)
void cli_process(void)
{
uint8_t chunk[CLI_LINE_BUF_SIZE];
size_t nbytes = bsp_usb_cdc_read(chunk, sizeof(chunk));
for (size_t byte_idx = 0U; byte_idx < nbytes; byte_idx++)
@ -157,7 +302,7 @@ void cli_process(void)
if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U))
{
g_s_line_len = 0U;
cli_send("{\"ok\":false,\"error\":\"LINE_TOO_LONG\"}\n");
protocol_send_error("LINE_TOO_LONG");
continue;
}
@ -168,11 +313,14 @@ void cli_process(void)
{
g_s_line_len--;
}
g_s_line_buf[g_s_line_len] = '\0';
if (g_s_line_len > 0U)
{
process_line((const char *) g_s_line_buf);
}
g_s_line_len = 0U;
}
else
@ -182,10 +330,3 @@ void cli_process(void)
}
}
}
/* ── Command handlers ──────────────────────────────────────────────────── */
static void cmd_ping(void)
{
cli_send("{\"ok\":true,\"result\":\"PONG\"}\n");
}

View file

@ -1,16 +1,22 @@
/**
* @file cli.h
* @brief Command-line interface для firmware_test.
* @brief IO-слой CLI для firmware_test.
*
* Единственный транспорт USB CDC ACM (bsp_usb_cdc).
* Протокол JSON-lines: каждая строка завершается '\n'.
* Запрос: {"cmd":"NAME"}\n
* Ответ: {"ok":true,...}\n или {"ok":false,"error":"CODE"}\n
* Транспорт: USB CDC ACM (bsp_usb_cdc) единственный канал.
* Протокол v2: JSON-lines, каждая строка завершается '\n'.
*
* @note Архитектурное ограничение: этот модуль жёстко связан с
* bsp_usb_cdc как единственным IO-каналом. Замена транспорта
* не предусмотрена firmware_test работает только через USB CDC.
* Для HIL ELF-прошивок используется отдельный канал связи (UART + bsp_uart_host).
* Входящие типы:
* {"type":"cmd", "cmd":"ping"}
* {"type":"cmd", "cmd":"run", "id":"sdram"}
* {"type":"cmd", "cmd":"run_all"}
* {"type":"confirm", "id":"...", "confirmed":true}
*
* Исходящие события формируются через protocol.h, а не напрямую через cli_send().
* cli_send() остаётся публичным: его использует protocol.c как единственную
* точку вывода.
*
* @note Архитектурное ограничение: единственный транспорт USB CDC.
* HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host).
*/
#ifndef CLI_H_
@ -18,13 +24,12 @@
#include <stddef.h>
/** @brief Максимальная длина входящей JSON-строки включая завершающий '\n'. */
/** @brief Максимальная длина входящей JSON-строки включая '\n'. */
#define CLI_LINE_BUF_SIZE 128U
/**
* @brief Инициализировать CLI.
* @brief Инициализировать CLI. Сбрасывает внутренний буфер строки.
*
* Сбрасывает внутренний буфер строки.
* Вызывать после bsp_usb_cdc_init() и до первого cli_process().
*/
void cli_init(void);
@ -32,15 +37,14 @@ void cli_init(void);
/**
* @brief Отправить готовую JSON-строку через USB CDC.
*
* @param[in] resp NUL-terminated строка, завершённая '\n'.
* @param[in] p_resp NUL-terminated строка, завершённая '\n'.
*
* @note Неблокирующий вызов. Если TX занят запись теряется.
* Для firmware_test это приемлемо: хост повторит запрос.
* @note Неблокирующий. Если TX занят запись теряется.
*/
void cli_send(const char *resp);
void cli_send(const char *p_resp);
/**
* @brief Обработать входящие байты и диспатчить команду при получении '\n'.
* @brief Обработать входящие байты, диспатчить сообщение при получении '\n'.
*
* Вызывать в главном цикле после bsp_usb_cdc_poll().
* Неблокирующий: если данных нет возвращается немедленно.

View file

@ -22,6 +22,8 @@
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
#include "protocol.h"
#include "test_runner.h"
#include <stdbool.h>
#include <stdint.h>
@ -52,10 +54,12 @@ int main(void)
bsp_led_on(LED_APP);
cli_init();
cli_send("{\"ok\":true,\"result\":\"READY\"}\n");
test_runner_init();
protocol_send_session_start();
while (1)
{
bsp_usb_cdc_poll();
cli_process();
test_runner_process();
}
}

View file

@ -0,0 +1,127 @@
/**
* @file protocol.c
* @brief Протокол firmware_test v2 реализация сериализации.
*/
#include "protocol.h"
#include "bsp/tick.h"
#include "cli.h"
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/**
* @brief Размер внутреннего TX-буфера.
*
* Должен вмещать самое длинное сообщение:
* test_result c detail[96] 185 байт, берём с запасом.
*/
#define PROTO_BUF_SIZE 256U
/* ── Вспомогательные функции ───────────────────────────────────────────── */
/**
* @brief Преобразовать статус теста в строку для JSON.
*
* @param[in] status Статус теста.
* @return Строковое представление статуса.
*/
static const char *status_to_str(test_status_t status)
{
switch (status)
{
case TEST_STATUS_PASS:
return "pass";
case TEST_STATUS_FAIL:
return "fail";
case TEST_STATUS_SKIP:
return "skip";
default:
return "unknown";
}
}
/* ── Public API ────────────────────────────────────────────────────────── */
void protocol_send_session_start(void)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"session_start\","
"\"fw\":\"" FIRMWARE_TEST_VERSION "\","
"\"target\":\"IMXRT1052\","
"\"uptime_ms\":%" PRIu32 "}\n",
bsp_tick_get_ms());
cli_send(buf);
}
void protocol_send_test_begin(const test_module_t *p_mod)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"test_begin\","
"\"id\":\"%s\","
"\"name\":\"%s\","
"\"critical\":%s}\n",
p_mod->id, p_mod->name, p_mod->critical ? "true" : "false");
cli_send(buf);
}
void protocol_send_test_result(const test_module_t *p_mod, const test_result_t *p_result)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"test_result\","
"\"id\":\"%s\","
"\"status\":\"%s\","
"\"ms\":%" PRIu32 ","
"\"detail\":\"%s\"}\n",
p_mod->id, status_to_str(p_result->status), p_result->duration_ms,
p_result->detail);
cli_send(buf);
}
void protocol_send_summary(uint8_t passed, uint8_t failed, uint8_t skipped, bool overall_pass)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"summary\","
"\"passed\":%u,"
"\"failed\":%u,"
"\"skipped\":%u,"
"\"overall\":\"%s\"}\n",
(unsigned int) passed, (unsigned int) failed, (unsigned int) skipped,
overall_pass ? "pass" : "fail");
cli_send(buf);
}
void protocol_send_confirm_request(const confirm_params_t *p_params)
{
const uint32_t EFFECTIVE_TIMEOUT =
(p_params->timeout_ms == 0U) ? PROTOCOL_CONFIRM_TIMEOUT_MS : p_params->timeout_ms;
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"confirm_request\","
"\"id\":\"%s\","
"\"prompt\":\"%s\","
"\"timeout_ms\":%" PRIu32 "}\n",
p_params->id, p_params->prompt, EFFECTIVE_TIMEOUT);
cli_send(buf);
}
void protocol_send_pong(void)
{
cli_send("{\"type\":\"pong\"}\n");
}
void protocol_send_error(const char *p_code)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
cli_send(buf);
}

View file

@ -0,0 +1,84 @@
/**
* @file protocol.h
* @brief Протокол firmware_test v2 сериализация исходящих событий.
*
* Все функции формируют JSON-строку и отправляют через cli_send().
* Без динамической памяти каждая функция пишет в стековый буфер.
*
* Типы исходящих событий:
* session_start при старте (однократно)
* test_begin перед вызовом run() каждого теста
* test_result после вызова run()
* summary после завершения run_all()
* confirm_request интерактивный шаг (ожидание оператора)
* pong ответ на {"type":"cmd","cmd":"ping"}
* error ошибка протокола или парсинга
*/
#ifndef PROTOCOL_H_
#define PROTOCOL_H_
#include "test_module.h"
#include <stdbool.h>
#include <stdint.h>
/** @brief Строка версии прошивки, вставляемая в session_start. */
#define FIRMWARE_TEST_VERSION "0.1.0"
/** @brief Таймаут подтверждения по умолчанию, мс. */
#define PROTOCOL_CONFIRM_TIMEOUT_MS 15000U
/**
* @brief Отправить событие session_start.
*
* Вызывается однократно при старте, до приёма первой команды.
* Пример: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
*/
void protocol_send_session_start(void);
/**
* @brief Отправить событие test_begin.
*
* @param[in] p_mod Дескриптор теста (поля id, name, critical).
*/
void protocol_send_test_begin(const test_module_t *p_mod);
/**
* @brief Отправить событие test_result.
*
* @param[in] p_mod Дескриптор теста (поле id).
* @param[in] p_result Заполненный результат теста.
*/
void protocol_send_test_result(const test_module_t *p_mod, const test_result_t *p_result);
/**
* @brief Отправить событие summary после завершения run_all().
*
* @param[in] passed Количество пройденных тестов.
* @param[in] failed Количество проваленных тестов.
* @param[in] skipped Количество пропущенных тестов.
* @param[in] overall_pass true если все critical тесты прошли.
*/
void protocol_send_summary(uint8_t passed, uint8_t failed, uint8_t skipped, bool overall_pass);
/**
* @brief Отправить confirm_request запрос подтверждения оператора.
*
* @param[in] p_params Параметры подтверждения (id, prompt, timeout_ms).
*/
void protocol_send_confirm_request(const confirm_params_t *p_params);
/**
* @brief Отправить pong ответ на ping.
*/
void protocol_send_pong(void);
/**
* @brief Отправить событие error.
*
* @param[in] p_code Короткий ASCII-код ошибки, напр. "PARSE_ERR".
*/
void protocol_send_error(const char *p_code);
#endif /* PROTOCOL_H_ */

View file

@ -0,0 +1,89 @@
/**
* @file test_module.h
* @brief Интерфейс тест-модуля для firmware_test.
*
* Каждый тест периферии реализует этот интерфейс и регистрируется
* в реестре test_runner. Добавление нового теста одна строка в реестре,
* без изменений в CLI или протокольном слое.
*/
#ifndef TEST_MODULE_H_
#define TEST_MODULE_H_
#include <stdbool.h>
#include <stdint.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/** @brief Размер буфера детального описания результата (включая NUL). */
#define TEST_DETAIL_SIZE 96U
/** @brief Максимальная длина идентификатора теста (включая NUL). */
#define TEST_ID_MAX_SIZE 24U
/* ── Типы результата ───────────────────────────────────────────────────── */
/**
* @brief Итог выполнения теста.
*/
typedef enum
{
TEST_STATUS_PASS = 0, /**< Тест пройден. */
TEST_STATUS_FAIL, /**< Тест провален. */
TEST_STATUS_SKIP, /**< Тест пропущен (нет оборудования, отказ оператора). */
} test_status_t;
/**
* @brief Результат, возвращаемый из run().
*
* @note Поле duration_ms заполняет test_runner, не сам модуль.
*/
typedef struct
{
test_status_t status; /**< Итог теста. */
uint32_t duration_ms; /**< Время выполнения, мс. */
char detail[TEST_DETAIL_SIZE]; /**< Описание ошибки или пустая строка. */
} test_result_t;
/* ── Параметры подтверждения ───────────────────────────────────────────── */
/**
* @brief Параметры запроса оператора (confirm_request).
*
* Используется test_runner_request_confirm() для интерактивных шагов теста.
*/
typedef struct
{
const char *id; /**< Идентификатор подтверждения. */
const char *prompt; /**< Инструкция оператору. */
uint32_t timeout_ms; /**< Таймаут ожидания в мс. 0 — использовать дефолт. */
} confirm_params_t;
/* ── Дескриптор модуля ─────────────────────────────────────────────────── */
/**
* @brief Дескриптор одного тест-модуля.
*
* Все функциональные поля, кроме run(), могут быть NULL:
* - init() : однократная инициализация перед run(). Может быть NULL.
* - run() : выполняет тест. Не может быть NULL. Интерактивные шаги
* реализуются через test_runner_request_confirm().
* - deinit() : освобождение ресурсов после run(), вызывается даже при FAIL.
* Может быть NULL.
*
* @note Долгие run() ДОЛЖНЫ периодически вызывать bsp_usb_cdc_poll(),
* чтобы USB стек оставался живым.
*/
typedef struct
{
const char *id; /**< Короткий ASCII-ключ, напр. "sdram". */
const char *name; /**< Читаемое имя, напр. "SDRAM 32 MB". */
bool critical; /**< true → run_all() останавливается при FAIL. */
bool requires_hil; /**< true → нужен M5StampPLC. */
const char *pre_confirm_prompt; /**< Запрос оператору до запуска, или NULL. */
void (*init)(void);
test_result_t (*run)(void);
void (*deinit)(void);
} test_module_t;
#endif /* TEST_MODULE_H_ */

View file

@ -0,0 +1,426 @@
/**
* @file test_runner.c
* @brief Реализация реестра тест-модулей и state machine запуска.
*
* State machine:
* IDLE ожидание команды от хоста.
* PRE_CONFIRM отправлен confirm_request до запуска теста (pre_confirm_prompt),
* ожидание JSON-ответа оператора или таймаута.
* RUNNING тест выполняется (dispatch_test блокирует).
* Защищает от ложного is_busy()==false во время
* внутреннего polling loop в test_runner_wait_confirm().
*/
#include "test_runner.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
#include "protocol.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
/* ── Реестр тестов ─────────────────────────────────────────────────────────
*
* Добавление нового теста (Этап 2+):
* 1. Раскомментировать extern-объявление нужного модуля.
* 2. Добавить &k_test_<name> в k_registry[].
*
* extern const test_module_t k_test_sdram;
*extern const test_module_t k_test_qspi;
*extern const test_module_t k_test_usd;
*extern const test_module_t k_test_display;
*extern const test_module_t k_test_buttons;
*extern const test_module_t k_test_can;
*extern const test_module_t k_test_uart_ttl;
*extern const test_module_t k_test_uart_iso;
*extern const test_module_t k_test_opto;
* */
#ifndef UNIT_TEST
static const test_module_t *const k_registry[] = {
/* populated starting from Этап 2 */
};
#define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0]))
#else /* UNIT_TEST — реестр предоставляется тест-файлом */
extern const test_module_t *g_unit_test_registry[];
extern size_t g_unit_test_registry_size;
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
#define k_registry g_unit_test_registry
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
#define REGISTRY_SIZE g_unit_test_registry_size
#endif /* UNIT_TEST */
/* ── Константы ─────────────────────────────────────────────────────────── */
#define RUNNER_CONFIRM_ID_SIZE 32U
/* ── Типы ──────────────────────────────────────────────────────────────── */
typedef enum runner_state_e
{
RUNNER_STATE_IDLE,
RUNNER_STATE_PRE_CONFIRM,
RUNNER_STATE_RUNNING,
} runner_state_t;
typedef enum runner_mode_e
{
RUNNER_MODE_SINGLE,
RUNNER_MODE_ALL,
} runner_mode_t;
/* ── Статическое состояние ─────────────────────────────────────────────── */
static runner_state_t g_s_state;
static runner_mode_t g_s_mode;
static size_t g_s_current_idx;
static volatile bool g_s_confirm_received;
static volatile bool g_s_confirm_value;
static char g_s_pending_confirm_id[RUNNER_CONFIRM_ID_SIZE];
static uint32_t g_s_confirm_deadline_ms;
/* Счётчики итога (используются только в RUNNER_MODE_ALL) */
static uint8_t g_s_passed;
static uint8_t g_s_failed;
static uint8_t g_s_skipped;
static bool g_s_critical_failed;
/* ── Forward declaration ───────────────────────────────────────────────── */
static void start_test_at(size_t idx);
/* ── Внутренние вспомогательные функции ────────────────────────────────── */
/**
* @brief Найти индекс теста по id. Возвращает REGISTRY_SIZE если не найден.
*/
static size_t find_test_by_id(const char *p_id)
{
for (size_t i = 0U; i < REGISTRY_SIZE; i++)
{
if (strcmp(k_registry[i]->id, p_id) == 0)
{
return i;
}
}
return REGISTRY_SIZE;
}
/**
* @brief Создать результат SKIP с текстовой причиной.
*/
static test_result_t make_skip_result(const char *p_reason)
{
test_result_t result = {
.status = TEST_STATUS_SKIP,
.duration_ms = 0U,
};
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", p_reason);
return result;
}
/**
* @brief Обновить счётчики pass/fail/skip и флаг critical_failed.
*/
static void update_counters(const test_result_t *p_result, bool critical)
{
switch (p_result->status)
{
case TEST_STATUS_PASS:
g_s_passed++;
break;
case TEST_STATUS_FAIL:
g_s_failed++;
if (critical)
{
g_s_critical_failed = true;
}
break;
case TEST_STATUS_SKIP:
g_s_skipped++;
break;
default:
break;
}
}
/**
* @brief Взвести механизм ожидания confirm (id + deadline).
*/
static void arm_confirm(const char *p_id, uint32_t timeout_ms)
{
(void) snprintf(g_s_pending_confirm_id, sizeof(g_s_pending_confirm_id), "%s", p_id);
g_s_confirm_received = false;
g_s_confirm_value = false;
g_s_confirm_deadline_ms = bsp_tick_get_ms() + timeout_ms;
}
/**
* @brief Выполнить тест: init run deinit. Замерить время.
*/
static test_result_t execute_test(const test_module_t *p_mod)
{
const uint32_t START_MS = bsp_tick_get_ms();
if (p_mod->init != NULL)
{
p_mod->init();
}
test_result_t result = p_mod->run();
if (p_mod->deinit != NULL)
{
p_mod->deinit();
}
result.duration_ms = bsp_tick_get_ms() - START_MS;
return result;
}
/**
* @brief Выслать test_begin, выполнить тест, обновить счётчики, выслать test_result.
*/
static void dispatch_test(const test_module_t *p_mod)
{
protocol_send_test_begin(p_mod);
test_result_t result = execute_test(p_mod);
update_counters(&result, p_mod->critical);
protocol_send_test_result(p_mod, &result);
}
/**
* @brief Отправить SKIP для всех тестов начиная с from_idx и обновить счётчик.
*
* Вызывается при critical fail в run_all оставшиеся тесты скипаются.
*/
static void skip_from(size_t from_idx)
{
for (size_t i = from_idx; i < REGISTRY_SIZE; i++)
{
const test_module_t *mod = k_registry[i];
test_result_t result = make_skip_result("critical test failed");
protocol_send_test_begin(mod);
protocol_send_test_result(mod, &result);
g_s_skipped++;
}
g_s_current_idx = REGISTRY_SIZE;
}
/**
* @brief Продвинуть runner после завершения текущего теста.
*
* В режиме SINGLE переходит в IDLE.
* В режиме ALL запускает следующий тест или отправляет summary.
*/
static void advance_after_current(void)
{
if (g_s_mode == RUNNER_MODE_SINGLE)
{
g_s_state = RUNNER_STATE_IDLE;
return;
}
g_s_current_idx++;
if (g_s_current_idx >= REGISTRY_SIZE)
{
protocol_send_summary(g_s_passed, g_s_failed, g_s_skipped, !g_s_critical_failed);
g_s_state = RUNNER_STATE_IDLE;
return;
}
start_test_at(g_s_current_idx);
}
/**
* @brief Выставить RUNNING, выполнить текущий тест, вернуть IDLE, продвинуть runner.
*/
static void execute_and_advance(void)
{
g_s_state = RUNNER_STATE_RUNNING;
dispatch_test(k_registry[g_s_current_idx]);
g_s_state = RUNNER_STATE_IDLE;
advance_after_current();
}
/**
* @brief Запустить тест по индексу: с pre_confirm или сразу.
*
* При critical_failed в режиме ALL скипает все оставшиеся тесты и
* отправляет summary.
*/
static void start_test_at(size_t idx)
{
g_s_current_idx = idx;
if (g_s_critical_failed)
{
skip_from(idx);
protocol_send_summary(g_s_passed, g_s_failed, g_s_skipped, false);
g_s_state = RUNNER_STATE_IDLE;
return;
}
const test_module_t *mod = k_registry[idx];
if (mod->pre_confirm_prompt != NULL)
{
confirm_params_t params = {
.id = mod->id,
.prompt = mod->pre_confirm_prompt,
.timeout_ms = 0U,
};
protocol_send_confirm_request(&params);
arm_confirm(mod->id, PROTOCOL_CONFIRM_TIMEOUT_MS);
g_s_state = RUNNER_STATE_PRE_CONFIRM;
return;
}
execute_and_advance();
}
/**
* @brief Обработать состояние PRE_CONFIRM: проверить confirm или таймаут.
*/
static void process_pre_confirm(void)
{
bool timed_out = (bsp_tick_get_ms() >= g_s_confirm_deadline_ms);
if (!g_s_confirm_received && !timed_out)
{
return;
}
if (g_s_confirm_received && g_s_confirm_value)
{
execute_and_advance();
return;
}
/* Таймаут или отказ оператора — скипаем тест */
const test_module_t *mod = k_registry[g_s_current_idx];
const char *p_reason = g_s_confirm_received ? "operator declined" : "confirm timeout";
test_result_t result = make_skip_result(p_reason);
g_s_state = RUNNER_STATE_IDLE;
protocol_send_test_begin(mod);
protocol_send_test_result(mod, &result);
update_counters(&result, mod->critical);
advance_after_current();
}
/* ── Public API ────────────────────────────────────────────────────────── */
void test_runner_init(void)
{
g_s_state = RUNNER_STATE_IDLE;
g_s_mode = RUNNER_MODE_SINGLE;
g_s_current_idx = 0U;
g_s_confirm_received = false;
g_s_confirm_value = false;
g_s_confirm_deadline_ms = 0U;
g_s_passed = 0U;
g_s_failed = 0U;
g_s_skipped = 0U;
g_s_critical_failed = false;
g_s_pending_confirm_id[0] = '\0';
}
void test_runner_process(void)
{
if (g_s_state == RUNNER_STATE_PRE_CONFIRM)
{
process_pre_confirm();
}
}
void test_runner_run_single(const char *p_id)
{
if (g_s_state != RUNNER_STATE_IDLE)
{
protocol_send_error("BUSY");
return;
}
size_t idx = find_test_by_id(p_id);
if (idx >= REGISTRY_SIZE)
{
protocol_send_error("UNKNOWN_TEST");
return;
}
g_s_mode = RUNNER_MODE_SINGLE;
start_test_at(idx);
}
void test_runner_run_all(void)
{
if (g_s_state != RUNNER_STATE_IDLE)
{
protocol_send_error("BUSY");
return;
}
if (REGISTRY_SIZE == 0U)
{
protocol_send_summary(0U, 0U, 0U, true);
return;
}
g_s_passed = 0U;
g_s_failed = 0U;
g_s_skipped = 0U;
g_s_critical_failed = false;
g_s_mode = RUNNER_MODE_ALL;
start_test_at(0U);
}
void test_runner_on_confirm(const char *p_id, bool confirmed)
{
if (strcmp(p_id, g_s_pending_confirm_id) != 0)
{
return; /* stale или несовпадение id — игнорировать */
}
g_s_confirm_value = confirmed;
g_s_confirm_received = true;
}
bool test_runner_is_busy(void)
{
return (g_s_state != RUNNER_STATE_IDLE);
}
bool test_runner_wait_confirm(const confirm_params_t *p_params)
{
const uint32_t EFFECTIVE_TIMEOUT =
(p_params->timeout_ms == 0U) ? PROTOCOL_CONFIRM_TIMEOUT_MS : p_params->timeout_ms;
protocol_send_confirm_request(p_params);
arm_confirm(p_params->id, EFFECTIVE_TIMEOUT);
while (!g_s_confirm_received)
{
if (bsp_tick_get_ms() >= g_s_confirm_deadline_ms)
{
return false; /* таймаут */
}
bsp_usb_cdc_poll();
cli_process();
}
return g_s_confirm_value;
}

View file

@ -0,0 +1,89 @@
/**
* @file test_runner.h
* @brief Реестр тест-модулей и state machine запуска для firmware_test.
*
* Добавление нового теста одна строка в реестре test_runner.c.
* Никаких изменений в CLI или протокольном слое не требуется.
*
* Архитектурные ограничения:
* - Без динамической памяти.
* - run() тест-модуля блокирует вызывающий контекст.
* - Долгие run() обязаны вызывать bsp_usb_cdc_poll() внутри.
*/
#ifndef TEST_RUNNER_H_
#define TEST_RUNNER_H_
#include "test_module.h"
#include <stdbool.h>
/**
* @brief Инициализировать runner. Сбрасывает состояние в IDLE и обнуляет счётчики.
*
* Вызывать после cli_init(), до первого cli_process().
*/
void test_runner_init(void);
/**
* @brief Обработать текущее состояние runner в главном цикле.
*
* Неблокирующий. Обслуживает ожидание confirm в состоянии PRE_CONFIRM.
* Вызывать в каждой итерации главного цикла после cli_process().
*/
void test_runner_process(void);
/**
* @brief Запустить один тест по идентификатору.
*
* Если runner занят отправляет {"ok":false,"error":"BUSY"}.
* Если id не найден отправляет {"ok":false,"error":"UNKNOWN_TEST"}.
*
* @param[in] p_id ASCII-идентификатор теста, напр. "sdram".
*/
void test_runner_run_single(const char *p_id);
/**
* @brief Запустить все тесты из реестра по порядку.
*
* Если runner занят отправляет {"ok":false,"error":"BUSY"}.
* После завершения всех тестов отправляет protocol_send_summary().
* Critical fail останавливает выполнение: оставшиеся тесты получают SKIP.
*/
void test_runner_run_all(void);
/**
* @brief Принять ответ оператора на confirm_request.
*
* Вызывается из cli.c при получении {"type":"confirm","id":"...","confirmed":...}.
* Если id не совпадает с ожидаемым игнорируется (stale confirm).
*
* @param[in] p_id Идентификатор подтверждения из поля "id".
* @param[in] confirmed true если оператор подтвердил.
*/
void test_runner_on_confirm(const char *p_id, bool confirmed);
/**
* @brief Проверить, занят ли runner.
*
* @return true если runner не в IDLE cli.c должен отвечать BUSY.
*/
bool test_runner_is_busy(void);
/**
* @brief Отправить confirm_request и заблокироваться до ответа оператора или таймаута.
*
* Предназначен для вызова из run() тест-модуля (display, интерактивные шаги).
* Внутри polling loop вызывает bsp_usb_cdc_poll() + cli_process() USB-стек
* остаётся живым и confirm может быть принят без возврата в главный цикл.
*
* @note Не использовать для теста кнопок: там подтверждение физическое нажатие,
* а не JSON. Кнопочный тест вызывает protocol_send_confirm_request() напрямую,
* затем поллит bsp_button сам.
*
* @param[in] p_params Параметры запроса (id, prompt, timeout_ms).
* @return true если оператор подтвердил, false при отказе или таймауте.
*/
bool test_runner_wait_confirm(const confirm_params_t *p_params);
#endif /* TEST_RUNNER_H_ */

View file

@ -49,6 +49,7 @@ add_sdk_driver(usdhc fsl_usdhc.c)
add_sdk_driver(lpi2c fsl_lpi2c.c)
add_sdk_driver(pwm fsl_pwm.c)
add_sdk_driver(adc fsl_adc.c)
add_sdk_driver(elcdif fsl_elcdif.c)
# Драйверы которые зависят от clock
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
@ -57,6 +58,7 @@ target_link_libraries(sdk_usdhc PUBLIC sdk_clock)
target_link_libraries(sdk_lpi2c PUBLIC sdk_clock)
target_link_libraries(sdk_pwm PUBLIC sdk_clock)
target_link_libraries(sdk_adc PUBLIC sdk_clock)
target_link_libraries(sdk_elcdif PUBLIC sdk_clock)
# -----------------------------------------------------------------------------
# SEMC и FlexSPI — инициализируются через DCD и FDCB, драйверы нужны только если

View file

@ -167,10 +167,10 @@ add_host_test(
test_cli
SOURCES
cli/test_cli.c
${CMAKE_SOURCE_DIR}/firmware/test/src/cli.c
INCLUDES
${CMAKE_SOURCE_DIR}/firmware/test/src/
${CMAKE_SOURCE_DIR}/bsp/common/include
${CMAKE_SOURCE_DIR}/bsp/usb_cdc/include
MOCKS
${BSP_MOCKS_DIR})
@ -185,3 +185,35 @@ add_host_test(
${PROJECT_SOURCE_DIR}/utils/prio_queue/prio_queue.c
INCLUDES
${PROJECT_SOURCE_DIR}/utils)
# -----------------------------------------------------------------------------
# firmware/test/src/protocol.c
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_protocol
SOURCES
protocol/test_protocol.c
${PROJECT_SOURCE_DIR}/firmware/test/src/protocol.c
INCLUDES
${PROJECT_SOURCE_DIR}/firmware/test/src
MOCKS
${BSP_MOCKS_DIR})
# -----------------------------------------------------------------------------
# firmware_test: test_runner — реестр тестов и state machine Категория B:
# protocol_send_* + bsp_tick + bsp_usb_cdc + cli_process мокируются.
# Компилируется с -DUNIT_TEST для подстановки реестра из теста.
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_firmware_runner
SOURCES
runner/test_firmware_runner.c
${PROJECT_SOURCE_DIR}/firmware/test/src/test_runner.c
INCLUDES
${PROJECT_SOURCE_DIR}/firmware/test/src
MOCKS
${BSP_MOCKS_DIR})
target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST)

View file

@ -1,113 +1,110 @@
/**
* @file test_cli.c
* @brief Host unit-тест для firmware_test/src/cli.c
* @brief Host unit-тесты IO-слоя cli.c.
*
* Тестирует: parse_cmd_field (через cli_process), dispatch, обработку
* граничных случаев буфера. Без железа, без fff stub-ы для
* bsp_usb_cdc_read/write определены прямо здесь.
* Категория B (fff): мокируются bsp_usb_cdc, protocol_send_*, test_runner_*.
*
* Добавление теста для новой команды:
* 1. stub_rx_feed() подать строку в RX буфер.
* 2. cli_process() запустить.
* 3. TEST_ASSERT_EQUAL_STRING() проверить TX буфер.
* Паттерн ввода: inject() наполняет внутренний буфер «как USB», затем
* вызывает cli_process() полностью изолируем I/O.
*
* Паттерн захвата строковых аргументов: custom_fake копирует id/cmd
* пока стек ещё жив (аргументы указатели на локальные буферы cli.c).
*/
#include "bsp/status.h"
#include "bsp/usb_cdc.h"
#include "fff.h"
#include "unity.h"
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
DEFINE_FFF_GLOBALS;
/* ── Stub: bsp_usb_cdc ─────────────────────────────────────────────────
*
* RX: тест кладёт байты через stub_rx_feed().
* cli_process() читает их по одному через bsp_usb_cdc_read().
*
* TX: cli_send() пишет через bsp_usb_cdc_write() в s_tx_buf.
* тест проверяет s_tx_buf через stub_tx_get().
*
* */
/* ── Фейки зависимостей cli.c ──────────────────────────────────────────── */
static uint8_t s_rx_buf[256];
static size_t s_rx_len = 0U;
static size_t s_rx_pos = 0U;
FAKE_VOID_FUNC(bsp_usb_cdc_write, const uint8_t *, size_t);
FAKE_VALUE_FUNC(size_t, bsp_usb_cdc_read, uint8_t *, size_t);
static char s_tx_buf[512];
static size_t s_tx_len = 0U;
FAKE_VOID_FUNC(protocol_send_pong);
FAKE_VOID_FUNC(protocol_send_error, const char *);
/** @brief Заполнить RX буфер данными для теста. */
static void stub_rx_feed(const char *data)
FAKE_VOID_FUNC(test_runner_run_all);
FAKE_VOID_FUNC(test_runner_run_single, const char *);
FAKE_VOID_FUNC(test_runner_on_confirm, const char *, bool);
/* ── Модуль под тестом ─────────────────────────────────────────────────── */
#include "cli.h"
/* ── Инъекция ввода ────────────────────────────────────────────────────── */
#define INJECT_BUF_SIZE (CLI_LINE_BUF_SIZE * 2U)
static uint8_t s_inject_buf[INJECT_BUF_SIZE];
static size_t s_inject_len = 0U;
static size_t fake_usb_read(uint8_t *p_buf, size_t size)
{
size_t len = strlen(data);
memcpy(s_rx_buf, data, len);
s_rx_len = len;
s_rx_pos = 0U;
if (s_inject_len == 0U)
{
return 0U;
}
/** @brief Получить TX буфер как C-строку. */
static const char *stub_tx_get(void)
{
s_tx_buf[s_tx_len] = '\0';
return s_tx_buf;
}
/** @brief Сбросить оба буфера. */
static void stub_reset(void)
{
s_rx_len = 0U;
s_rx_pos = 0U;
s_tx_len = 0U;
memset(s_tx_buf, 0, sizeof(s_tx_buf));
}
/* bsp_usb_cdc stubs */
size_t bsp_usb_cdc_read(uint8_t *buf, size_t max_len)
{
size_t available = s_rx_len - s_rx_pos;
size_t to_copy = (available < max_len) ? available : max_len;
memcpy(buf, s_rx_buf + s_rx_pos, to_copy);
s_rx_pos += to_copy;
size_t to_copy = (s_inject_len < size) ? s_inject_len : size;
memcpy(p_buf, s_inject_buf, to_copy);
s_inject_len = 0U;
return to_copy;
}
bsp_status_t bsp_usb_cdc_write(const uint8_t *data, size_t len)
static void inject(const char *p_line)
{
if ((s_tx_len + len) < sizeof(s_tx_buf))
{
memcpy(s_tx_buf + s_tx_len, data, len);
s_tx_len += len;
}
return BSP_OK;
size_t len = strlen(p_line);
size_t limit = (len < INJECT_BUF_SIZE) ? len : INJECT_BUF_SIZE - 1U;
memcpy(s_inject_buf, p_line, limit);
s_inject_len = limit;
cli_process();
}
/* Остальные символы bsp_usb_cdc — заглушки, cli.c не вызывает их */
bsp_status_t bsp_usb_cdc_init(void)
{
return BSP_OK;
}
bool bsp_usb_cdc_is_ready(void)
{
return true;
}
bool bsp_usb_cdc_write_ready(void)
{
return true;
}
void bsp_usb_cdc_poll(void)
/* ── Захват аргументов-строк ───────────────────────────────────────────── */
/*
* test_runner_run_single и test_runner_on_confirm получают указатели
* на локальные буферы cli.c. После возврата cli_process() стек мёртв
* нельзя читать fake.arg0_val. Копируем через custom_fake.
*/
static char s_run_single_id[32U];
static char s_confirm_id[32U];
static bool s_confirm_value;
static void capture_run_single(const char *p_id)
{
(void) snprintf(s_run_single_id, sizeof(s_run_single_id), "%s", p_id);
}
/* ── Включаем тестируемый модуль ПОСЛЕ stub-ов ─────────────────────── */
static void capture_on_confirm(const char *p_id, bool confirmed)
{
(void) snprintf(s_confirm_id, sizeof(s_confirm_id), "%s", p_id);
s_confirm_value = confirmed;
}
#include "cli.c" /* NOLINT(bugprone-suspicious-include) */
/* ── Фикстуры ──────────────────────────────────────────────────────── */
/* ── setUp / tearDown ──────────────────────────────────────────────────── */
void setUp(void)
{
stub_reset();
RESET_FAKE(bsp_usb_cdc_read);
RESET_FAKE(bsp_usb_cdc_write);
RESET_FAKE(protocol_send_pong);
RESET_FAKE(protocol_send_error);
RESET_FAKE(test_runner_run_all);
RESET_FAKE(test_runner_run_single);
RESET_FAKE(test_runner_on_confirm);
FFF_RESET_HISTORY();
bsp_usb_cdc_read_fake.custom_fake = fake_usb_read;
test_runner_run_single_fake.custom_fake = capture_run_single;
test_runner_on_confirm_fake.custom_fake = capture_on_confirm;
s_inject_len = 0U;
s_run_single_id[0] = '\0';
s_confirm_id[0] = '\0';
s_confirm_value = false;
cli_init();
}
@ -115,87 +112,155 @@ void tearDown(void)
{
}
/* ── Тесты ─────────────────────────────────────────────────────────── */
/* ── Тесты: type = cmd ─────────────────────────────────────────────────── */
void test_ping_returns_pong(void)
void test_ping_dispatches_to_pong(void)
{
stub_rx_feed("{\"cmd\":\"PING\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"ping\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_pong_fake.call_count);
TEST_ASSERT_EQUAL_INT(0, protocol_send_error_fake.call_count);
}
void test_unknown_command(void)
void test_run_all_dispatches_to_runner(void)
{
stub_rx_feed("{\"cmd\":\"FOOBAR\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"run_all\"}\n");
TEST_ASSERT_EQUAL_INT(1, test_runner_run_all_fake.call_count);
TEST_ASSERT_EQUAL_INT(0, protocol_send_error_fake.call_count);
}
void test_missing_cmd_field(void)
void test_run_single_dispatches_with_id(void)
{
stub_rx_feed("{\"foo\":\"bar\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"run\",\"id\":\"sdram\"}\n");
TEST_ASSERT_EQUAL_INT(1, test_runner_run_single_fake.call_count);
TEST_ASSERT_EQUAL_STRING("sdram", s_run_single_id);
}
void test_empty_line_no_response(void)
void test_unknown_cmd_sends_unknown_cmd_error(void)
{
/* Пустая строка '\n' — буфер пуст, dispatch не вызывается */
stub_rx_feed("\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"reboot\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("UNKNOWN_CMD", protocol_send_error_fake.arg0_val);
}
void test_line_too_long(void)
void test_run_missing_id_sends_parse_err(void)
{
/* 127 байт + '\n' = ровно граница CLI_LINE_BUF_SIZE */
inject("{\"type\":\"cmd\",\"cmd\":\"run\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("PARSE_ERR", protocol_send_error_fake.arg0_val);
}
/* ── Тесты: type = confirm ─────────────────────────────────────────────── */
void test_confirm_true_dispatches(void)
{
inject("{\"type\":\"confirm\",\"id\":\"display_red\",\"confirmed\":true}\n");
TEST_ASSERT_EQUAL_INT(1, test_runner_on_confirm_fake.call_count);
TEST_ASSERT_EQUAL_STRING("display_red", s_confirm_id);
TEST_ASSERT_TRUE(s_confirm_value);
}
void test_confirm_false_dispatches(void)
{
inject("{\"type\":\"confirm\",\"id\":\"usd_insert\",\"confirmed\":false}\n");
TEST_ASSERT_EQUAL_INT(1, test_runner_on_confirm_fake.call_count);
TEST_ASSERT_EQUAL_STRING("usd_insert", s_confirm_id);
TEST_ASSERT_FALSE(s_confirm_value);
}
void test_confirm_missing_confirmed_field_sends_parse_err(void)
{
inject("{\"type\":\"confirm\",\"id\":\"display_red\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("PARSE_ERR", protocol_send_error_fake.arg0_val);
}
/* ── Тесты: ошибки протокола ───────────────────────────────────────────── */
void test_missing_type_field_sends_parse_err(void)
{
inject("{\"cmd\":\"ping\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("PARSE_ERR", protocol_send_error_fake.arg0_val);
}
void test_unknown_type_sends_unknown_cmd(void)
{
inject("{\"type\":\"status\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("UNKNOWN_CMD", protocol_send_error_fake.arg0_val);
}
void test_line_too_long_sends_error(void)
{
/* CLI_LINE_BUF_SIZE - 1 символов без '\n' = переполнение буфера */
char long_line[CLI_LINE_BUF_SIZE + 2U];
memset(long_line, 'A', CLI_LINE_BUF_SIZE);
memset(long_line, 'x', CLI_LINE_BUF_SIZE);
long_line[CLI_LINE_BUF_SIZE] = '\n';
long_line[CLI_LINE_BUF_SIZE + 1U] = '\0';
stub_rx_feed(long_line);
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"LINE_TOO_LONG\"}\n", stub_tx_get());
inject(long_line);
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("LINE_TOO_LONG", protocol_send_error_fake.arg0_val);
}
void test_two_commands_in_sequence(void)
void test_empty_line_ignored(void)
{
/* Два PING подряд — оба должны дать PONG */
stub_rx_feed("{\"cmd\":\"PING\"}\n{\"cmd\":\"PING\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n"
"{\"ok\":true,\"result\":\"PONG\"}\n",
/* Нет: первый вызов — первый PONG. Сбросим и проверим второй. */
stub_tx_get());
inject("\n");
TEST_ASSERT_EQUAL_INT(0, protocol_send_pong_fake.call_count);
TEST_ASSERT_EQUAL_INT(0, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_INT(0, test_runner_run_all_fake.call_count);
}
void test_cmd_field_with_spaces(void)
void test_crlf_handled_same_as_lf(void)
{
/* Пробелы вокруг ':' — реальные клиенты могут так форматировать */
stub_rx_feed("{\"cmd\" : \"PING\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"ping\"}\r\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_pong_fake.call_count);
}
void test_partial_input_no_response_until_newline(void)
void test_two_lines_in_one_chunk_both_dispatched(void)
{
/* Подать строку без '\n' — ответа быть не должно */
stub_rx_feed("{\"cmd\":\"PING\"}");
cli_process();
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
inject("{\"type\":\"cmd\",\"cmd\":\"ping\"}\n"
"{\"type\":\"cmd\",\"cmd\":\"run_all\"}\n");
TEST_ASSERT_EQUAL_INT(1, protocol_send_pong_fake.call_count);
TEST_ASSERT_EQUAL_INT(1, test_runner_run_all_fake.call_count);
}
/* ── main ──────────────────────────────────────────────────────────────── */
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_ping_returns_pong);
RUN_TEST(test_unknown_command);
RUN_TEST(test_missing_cmd_field);
RUN_TEST(test_empty_line_no_response);
RUN_TEST(test_line_too_long);
RUN_TEST(test_two_commands_in_sequence);
RUN_TEST(test_cmd_field_with_spaces);
RUN_TEST(test_partial_input_no_response_until_newline);
RUN_TEST(test_ping_dispatches_to_pong);
RUN_TEST(test_run_all_dispatches_to_runner);
RUN_TEST(test_run_single_dispatches_with_id);
RUN_TEST(test_unknown_cmd_sends_unknown_cmd_error);
RUN_TEST(test_run_missing_id_sends_parse_err);
RUN_TEST(test_confirm_true_dispatches);
RUN_TEST(test_confirm_false_dispatches);
RUN_TEST(test_confirm_missing_confirmed_field_sends_parse_err);
RUN_TEST(test_missing_type_field_sends_parse_err);
RUN_TEST(test_unknown_type_sends_unknown_cmd);
RUN_TEST(test_line_too_long_sends_error);
RUN_TEST(test_empty_line_ignored);
RUN_TEST(test_crlf_handled_same_as_lf);
RUN_TEST(test_two_lines_in_one_chunk_both_dispatched);
return UNITY_END();
}

View file

@ -0,0 +1,13 @@
/**
* @file bsp/usb_cdc.h
* @brief Stub для host-тестов. Функции мокируются через fff.
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
void bsp_usb_cdc_poll(void);
size_t bsp_usb_cdc_read(uint8_t *p_buf, size_t size);
void bsp_usb_cdc_write(const uint8_t *p_data, size_t size);

View file

@ -0,0 +1,281 @@
/**
* @file test_protocol.c
* @brief Host unit-тесты для protocol.c сериализация JSON-событий.
*
* Категория B (fff): две зависимости мокируются через fff.
* cli_send() захватываем строку через custom_fake.
* bsp_tick_get_ms() контролируем uptime в session_start.
*
* Паттерн захвата строки: custom_fake копирует аргумент в s_captured
* до возврата protocol_send_*(), пока стековый буфер ещё жив.
*/
#include "fff.h"
#include "unity.h"
DEFINE_FFF_GLOBALS;
/* ── Фейки зависимостей protocol.c ────────────────────────────────────── */
FAKE_VOID_FUNC(cli_send, const char *);
FAKE_VOID_FUNC(bsp_delay, uint32_t);
FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms);
/* ── Модуль под тестом ─────────────────────────────────────────────────── */
#include "protocol.h"
/* ── Захват вывода cli_send ────────────────────────────────────────────── */
#define CAPTURE_BUF_SIZE 256U
static char s_captured[CAPTURE_BUF_SIZE];
static void capture_cli_send(const char *p_resp)
{
(void) snprintf(s_captured, sizeof(s_captured), "%s", p_resp);
}
/* ── Вспомогательные данные ────────────────────────────────────────────── */
/* Минимальный дескриптор — protocol_send_* использует только нужные поля */
static const test_module_t K_MOD_SDRAM = {
.id = "sdram",
.name = "SDRAM 32 MB",
.critical = true,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = NULL,
.run = NULL,
.deinit = NULL,
};
static const test_module_t K_MOD_OPTO = {
.id = "opto",
.name = "Opto-in EXT",
.critical = false,
.requires_hil = true,
.pre_confirm_prompt = NULL,
.init = NULL,
.run = NULL,
.deinit = NULL,
};
/* ── setUp / tearDown ──────────────────────────────────────────────────── */
void setUp(void)
{
RESET_FAKE(cli_send);
RESET_FAKE(bsp_tick_get_ms);
RESET_FAKE(bsp_delay);
FFF_RESET_HISTORY();
cli_send_fake.custom_fake = capture_cli_send;
bsp_tick_get_ms_fake.return_val = 0U;
s_captured[0] = '\0';
}
void tearDown(void)
{
}
/* ── Тесты: session_start ──────────────────────────────────────────────── */
void test_session_start_format_zero_uptime(void)
{
bsp_tick_get_ms_fake.return_val = 0U;
protocol_send_session_start();
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"session_start\",\"fw\":\"" FIRMWARE_TEST_VERSION "\","
"\"target\":\"IMXRT1052\",\"uptime_ms\":0}\n",
s_captured);
}
void test_session_start_uptime_nonzero(void)
{
bsp_tick_get_ms_fake.return_val = 1234U;
protocol_send_session_start();
TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"uptime_ms\":1234"));
}
/* ── Тесты: test_begin ─────────────────────────────────────────────────── */
void test_test_begin_critical_true(void)
{
protocol_send_test_begin(&K_MOD_SDRAM);
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"test_begin\",\"id\":\"sdram\","
"\"name\":\"SDRAM 32 MB\",\"critical\":true}\n",
s_captured);
}
void test_test_begin_critical_false(void)
{
protocol_send_test_begin(&K_MOD_OPTO);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"test_begin\",\"id\":\"opto\","
"\"name\":\"Opto-in EXT\",\"critical\":false}\n",
s_captured);
}
/* ── Тесты: test_result ────────────────────────────────────────────────── */
void test_test_result_pass(void)
{
test_result_t result = {
.status = TEST_STATUS_PASS,
.duration_ms = 312U,
.detail = "",
};
protocol_send_test_result(&K_MOD_SDRAM, &result);
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"test_result\",\"id\":\"sdram\","
"\"status\":\"pass\",\"ms\":312,\"detail\":\"\"}\n",
s_captured);
}
void test_test_result_fail_with_detail(void)
{
test_result_t result = {
.status = TEST_STATUS_FAIL,
.duration_ms = 88U,
.detail = "addr=0x80001000 expected=0xA5 got=0x00",
};
protocol_send_test_result(&K_MOD_SDRAM, &result);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"test_result\",\"id\":\"sdram\","
"\"status\":\"fail\",\"ms\":88,"
"\"detail\":\"addr=0x80001000 expected=0xA5 got=0x00\"}\n",
s_captured);
}
void test_test_result_skip(void)
{
test_result_t result = {
.status = TEST_STATUS_SKIP,
.duration_ms = 0U,
.detail = "confirm timeout",
};
protocol_send_test_result(&K_MOD_OPTO, &result);
TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"status\":\"skip\""));
TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"detail\":\"confirm timeout\""));
}
/* ── Тесты: summary ────────────────────────────────────────────────────── */
void test_summary_overall_pass(void)
{
protocol_send_summary(6U, 0U, 1U, true);
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"summary\",\"passed\":6,\"failed\":0,"
"\"skipped\":1,\"overall\":\"pass\"}\n",
s_captured);
}
void test_summary_overall_fail(void)
{
protocol_send_summary(5U, 1U, 0U, false);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"summary\",\"passed\":5,\"failed\":1,"
"\"skipped\":0,\"overall\":\"fail\"}\n",
s_captured);
}
/* ── Тесты: confirm_request ────────────────────────────────────────────── */
void test_confirm_request_custom_timeout(void)
{
confirm_params_t params = {
.id = "usd_insert",
.prompt = "Insert microSD card",
.timeout_ms = 30000U,
};
protocol_send_confirm_request(&params);
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"confirm_request\",\"id\":\"usd_insert\","
"\"prompt\":\"Insert microSD card\",\"timeout_ms\":30000}\n",
s_captured);
}
void test_confirm_request_default_timeout_on_zero(void)
{
confirm_params_t params = {
.id = "display_red",
.prompt = "Red?",
.timeout_ms = 0U,
};
protocol_send_confirm_request(&params);
/* timeout_ms == 0 → подставляется PROTOCOL_CONFIRM_TIMEOUT_MS */
TEST_ASSERT_NOT_NULL(strstr(s_captured, "\"timeout_ms\":15000"));
}
/* ── Тесты: pong ───────────────────────────────────────────────────────── */
void test_pong_exact_string(void)
{
protocol_send_pong();
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"type\":\"pong\"}\n", s_captured);
}
/* ── Тесты: error ──────────────────────────────────────────────────────── */
void test_error_parse_err(void)
{
protocol_send_error("PARSE_ERR");
TEST_ASSERT_EQUAL_INT(1, cli_send_fake.call_count);
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n", s_captured);
}
void test_error_unknown_test(void)
{
protocol_send_error("UNKNOWN_TEST");
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"UNKNOWN_TEST\"}\n", s_captured);
}
/* ── Точка входа ───────────────────────────────────────────────────────── */
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_session_start_format_zero_uptime);
RUN_TEST(test_session_start_uptime_nonzero);
RUN_TEST(test_test_begin_critical_true);
RUN_TEST(test_test_begin_critical_false);
RUN_TEST(test_test_result_pass);
RUN_TEST(test_test_result_fail_with_detail);
RUN_TEST(test_test_result_skip);
RUN_TEST(test_summary_overall_pass);
RUN_TEST(test_summary_overall_fail);
RUN_TEST(test_confirm_request_custom_timeout);
RUN_TEST(test_confirm_request_default_timeout_on_zero);
RUN_TEST(test_pong_exact_string);
RUN_TEST(test_error_parse_err);
RUN_TEST(test_error_unknown_test);
return UNITY_END();
}

View file

@ -0,0 +1,347 @@
/**
* @file test_firmware_runner.c
* @brief Host unit-тесты state machine test_runner.c.
*
* Категория B (fff): мокируются protocol_send_*, bsp_tick_get_ms,
* bsp_usb_cdc_poll, cli_process.
*
* UNIT_TEST seam: реестр тест-модулей предоставляется этим файлом
* через g_unit_test_registry / g_unit_test_registry_size.
* set_registry() меняет состав реестра между тестами.
*
* Паттерн захвата test_result: protocol_send_test_result получает указатель
* на стековую переменную execute_test(). Копируем через custom_fake.
*/
#include "fff.h"
#include "unity.h"
DEFINE_FFF_GLOBALS;
/* ── Типы (нужны для сигнатур фейков) ─────────────────────────────────── */
#include "protocol.h"
#include "test_module.h"
/* ── Фейки зависимостей test_runner.c ─────────────────────────────────── */
FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms);
FAKE_VOID_FUNC(bsp_delay, uint32_t);
FAKE_VOID_FUNC(bsp_usb_cdc_poll);
FAKE_VOID_FUNC(cli_process);
FAKE_VOID_FUNC(protocol_send_test_begin, const test_module_t *);
FAKE_VOID_FUNC(protocol_send_test_result, const test_module_t *, const test_result_t *);
FAKE_VOID_FUNC(protocol_send_summary, uint8_t, uint8_t, uint8_t, bool);
FAKE_VOID_FUNC(protocol_send_confirm_request, const confirm_params_t *);
FAKE_VOID_FUNC(protocol_send_error, const char *);
FAKE_VOID_FUNC(protocol_send_pong);
FAKE_VOID_FUNC(protocol_send_session_start);
/* ── Модуль под тестом ─────────────────────────────────────────────────── */
#include "test_runner.h"
/* ── Инъекция реестра ──────────────────────────────────────────────────── */
#define UNIT_TEST_REGISTRY_MAX 8U
const test_module_t *g_unit_test_registry[UNIT_TEST_REGISTRY_MAX];
size_t g_unit_test_registry_size = 0U;
static void set_registry(const test_module_t **pp_mods, size_t count)
{
for (size_t i = 0U; i < count && i < UNIT_TEST_REGISTRY_MAX; i++)
{
g_unit_test_registry[i] = pp_mods[i];
}
g_unit_test_registry_size = count;
}
/* ── Тест-модули ───────────────────────────────────────────────────────── */
static test_result_t run_pass(void)
{
test_result_t r = { .status = TEST_STATUS_PASS, .duration_ms = 0U };
r.detail[0] = '\0';
return r;
}
static test_result_t run_fail(void)
{
test_result_t r = { .status = TEST_STATUS_FAIL, .duration_ms = 0U };
(void) snprintf(r.detail, TEST_DETAIL_SIZE, "%s", "simulated fail");
return r;
}
static const test_module_t K_MOD_PASS = {
.id = "t_pass",
.name = "Pass Test",
.critical = false,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = NULL,
.run = run_pass,
.deinit = NULL,
};
static const test_module_t K_MOD_CRIT_FAIL = {
.id = "t_fail",
.name = "Critical Fail Test",
.critical = true,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = NULL,
.run = run_fail,
.deinit = NULL,
};
static const test_module_t K_MOD_PRECONFIRM = {
.id = "t_pre",
.name = "Pre-confirm Test",
.critical = false,
.requires_hil = false,
.pre_confirm_prompt = "Insert the device",
.init = NULL,
.run = run_pass,
.deinit = NULL,
};
/* ── Захват test_result (dangling pointer fix) ─────────────────────────── */
static test_result_t s_last_result;
static bool s_result_captured;
static void capture_test_result(const test_module_t *p_mod, const test_result_t *p_result)
{
s_last_result = *p_result; /* копия пока стек жив */
s_result_captured = true;
(void) p_mod;
}
/* ── setUp / tearDown ──────────────────────────────────────────────────── */
void setUp(void)
{
RESET_FAKE(bsp_tick_get_ms);
RESET_FAKE(bsp_delay);
RESET_FAKE(bsp_usb_cdc_poll);
RESET_FAKE(cli_process);
RESET_FAKE(protocol_send_test_begin);
RESET_FAKE(protocol_send_test_result);
RESET_FAKE(protocol_send_summary);
RESET_FAKE(protocol_send_confirm_request);
RESET_FAKE(protocol_send_error);
FFF_RESET_HISTORY();
protocol_send_test_result_fake.custom_fake = capture_test_result;
bsp_tick_get_ms_fake.return_val = 0U;
s_result_captured = false;
g_unit_test_registry_size = 0U;
test_runner_init();
}
void tearDown(void)
{
}
/* ── Тесты: базовые (пустой реестр) ───────────────────────────────────── */
void test_not_busy_initially(void)
{
TEST_ASSERT_FALSE(test_runner_is_busy());
}
void test_run_all_empty_registry_sends_summary(void)
{
test_runner_run_all();
TEST_ASSERT_EQUAL_INT(1, protocol_send_summary_fake.call_count);
TEST_ASSERT_EQUAL_INT(0, protocol_send_summary_fake.arg0_val); /* passed */
TEST_ASSERT_EQUAL_INT(0, protocol_send_summary_fake.arg1_val); /* failed */
TEST_ASSERT_EQUAL_INT(0, protocol_send_summary_fake.arg2_val); /* skipped */
TEST_ASSERT_TRUE(protocol_send_summary_fake.arg3_val); /* overall */
}
void test_run_single_unknown_id_sends_error(void)
{
test_runner_run_single("nonexistent");
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("UNKNOWN_TEST", protocol_send_error_fake.arg0_val);
}
/* ── Тесты: одиночный запуск ───────────────────────────────────────────── */
void test_run_single_pass_sends_begin_and_result(void)
{
const test_module_t *mods[] = { &K_MOD_PASS };
set_registry(mods, 1U);
test_runner_run_single("t_pass");
TEST_ASSERT_EQUAL_INT(1, protocol_send_test_begin_fake.call_count);
TEST_ASSERT_TRUE(s_result_captured);
TEST_ASSERT_EQUAL_INT(TEST_STATUS_PASS, s_last_result.status);
TEST_ASSERT_FALSE(test_runner_is_busy());
}
void test_run_single_fail_sends_fail_result(void)
{
const test_module_t *mods[] = { &K_MOD_CRIT_FAIL };
set_registry(mods, 1U);
test_runner_run_single("t_fail");
TEST_ASSERT_TRUE(s_result_captured);
TEST_ASSERT_EQUAL_INT(TEST_STATUS_FAIL, s_last_result.status);
}
/* ── Тесты: run_all ────────────────────────────────────────────────────── */
void test_run_all_two_pass_sends_correct_summary(void)
{
const test_module_t *mods[] = { &K_MOD_PASS, &K_MOD_PASS };
set_registry(mods, 2U);
test_runner_run_all();
TEST_ASSERT_EQUAL_INT(2, protocol_send_test_begin_fake.call_count);
TEST_ASSERT_EQUAL_INT(2, protocol_send_test_result_fake.call_count);
TEST_ASSERT_EQUAL_INT(1, protocol_send_summary_fake.call_count);
TEST_ASSERT_EQUAL_INT(2, protocol_send_summary_fake.arg0_val); /* passed */
TEST_ASSERT_EQUAL_INT(0, protocol_send_summary_fake.arg1_val); /* failed */
TEST_ASSERT_TRUE(protocol_send_summary_fake.arg3_val); /* overall */
}
void test_run_all_critical_fail_skips_remaining(void)
{
/* [pass, critical_fail, pass] — третий должен получить SKIP */
const test_module_t *mods[] = { &K_MOD_PASS, &K_MOD_CRIT_FAIL, &K_MOD_PASS };
set_registry(mods, 3U);
test_runner_run_all();
/* Все три теста получили test_begin + test_result */
TEST_ASSERT_EQUAL_INT(3, protocol_send_test_begin_fake.call_count);
TEST_ASSERT_EQUAL_INT(3, protocol_send_test_result_fake.call_count);
/* Summary: 1 pass, 1 fail, 1 skip, overall fail */
TEST_ASSERT_EQUAL_INT(1, protocol_send_summary_fake.arg0_val);
TEST_ASSERT_EQUAL_INT(1, protocol_send_summary_fake.arg1_val);
TEST_ASSERT_EQUAL_INT(1, protocol_send_summary_fake.arg2_val);
TEST_ASSERT_FALSE(protocol_send_summary_fake.arg3_val);
}
/* ── Тесты: pre_confirm state machine ─────────────────────────────────── */
void test_pre_confirm_module_enters_busy_state(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
test_runner_run_single("t_pre");
/* confirm_request отправлен, runner в PRE_CONFIRM */
TEST_ASSERT_EQUAL_INT(1, protocol_send_confirm_request_fake.call_count);
TEST_ASSERT_TRUE(test_runner_is_busy());
/* Тест ещё не выполнен */
TEST_ASSERT_EQUAL_INT(0, protocol_send_test_begin_fake.call_count);
}
void test_pre_confirm_busy_rejects_new_run(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
test_runner_run_single("t_pre"); /* входим в PRE_CONFIRM */
test_runner_run_single("t_pre"); /* должен получить BUSY */
TEST_ASSERT_EQUAL_INT(1, protocol_send_error_fake.call_count);
TEST_ASSERT_EQUAL_STRING("BUSY", protocol_send_error_fake.arg0_val);
}
void test_pre_confirm_confirmed_true_executes_test(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
test_runner_run_single("t_pre");
test_runner_on_confirm("t_pre", true);
test_runner_process(); /* confirm принят → тест выполняется */
TEST_ASSERT_EQUAL_INT(1, protocol_send_test_begin_fake.call_count);
TEST_ASSERT_TRUE(s_result_captured);
TEST_ASSERT_EQUAL_INT(TEST_STATUS_PASS, s_last_result.status);
TEST_ASSERT_FALSE(test_runner_is_busy());
}
void test_pre_confirm_confirmed_false_skips_test(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
test_runner_run_single("t_pre");
test_runner_on_confirm("t_pre", false);
test_runner_process();
TEST_ASSERT_TRUE(s_result_captured);
TEST_ASSERT_EQUAL_INT(TEST_STATUS_SKIP, s_last_result.status);
TEST_ASSERT_FALSE(test_runner_is_busy());
}
void test_pre_confirm_timeout_skips_test(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
bsp_tick_get_ms_fake.return_val = 0U;
test_runner_run_single("t_pre");
/* Симулируем истечение таймаута */
bsp_tick_get_ms_fake.return_val = PROTOCOL_CONFIRM_TIMEOUT_MS;
test_runner_process();
TEST_ASSERT_TRUE(s_result_captured);
TEST_ASSERT_EQUAL_INT(TEST_STATUS_SKIP, s_last_result.status);
}
void test_stale_confirm_wrong_id_ignored(void)
{
const test_module_t *mods[] = { &K_MOD_PRECONFIRM };
set_registry(mods, 1U);
test_runner_run_single("t_pre");
test_runner_on_confirm("wrong_id", true); /* stale — не должен совпасть */
test_runner_process();
/* Тест не выполнен — confirm проигнорирован, всё ещё ждём */
TEST_ASSERT_EQUAL_INT(0, protocol_send_test_begin_fake.call_count);
TEST_ASSERT_TRUE(test_runner_is_busy());
}
/* ── main ──────────────────────────────────────────────────────────────── */
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_not_busy_initially);
RUN_TEST(test_run_all_empty_registry_sends_summary);
RUN_TEST(test_run_single_unknown_id_sends_error);
RUN_TEST(test_run_single_pass_sends_begin_and_result);
RUN_TEST(test_run_single_fail_sends_fail_result);
RUN_TEST(test_run_all_two_pass_sends_correct_summary);
RUN_TEST(test_run_all_critical_fail_skips_remaining);
RUN_TEST(test_pre_confirm_module_enters_busy_state);
RUN_TEST(test_pre_confirm_busy_rejects_new_run);
RUN_TEST(test_pre_confirm_confirmed_true_executes_test);
RUN_TEST(test_pre_confirm_confirmed_false_skips_test);
RUN_TEST(test_pre_confirm_timeout_skips_test);
RUN_TEST(test_stale_confirm_wrong_id_ignored);
return UNITY_END();
}

View file

@ -1,145 +0,0 @@
# Рефакторинг `conftest.py` — HIL-тесты MIMXRT1052
## Обзор
В ходе code review были выявлены и устранены 3 проблемы:
утечка ресурсов при исключениях, дублирование кода фикстур UART,
несогласованное чтение конфигурации.
---
## 1. Утечка UART-порта при исключении
**Проблема.** Функция `_open_uart_and_wait_ready` открывала `serial.Serial`,
но при исключении внутри цикла ожидания `READY` (например, `SerialException`)
порт не закрывался — операционная система удерживала дескриптор до завершения
процесса.
**Решение.** Функция заменена на контекстный менеджер `_uart_context`,
реализованный через `@contextmanager`. Блок `finally` гарантирует вызов
`ser.close()` при любом исходе.
```python
# До
def _open_uart_and_wait_ready(request) -> serial.Serial:
ser = serial.Serial(port=port, ...)
# ... если исключение здесь — ser не закрыт
return ser
# После
```python
def _uart_context(port: str, baud: int, ready_timeout: float):
ser = serial.Serial(port=port, baudrate=baud, timeout=2.0, write_timeout=1.0)
try:
# … ожидание READY
yield ser
finally:
ser.close() # выполняется всегда
```
Аналогичная правка применена к фикстуре `usb_cdc_port` — добавлен
`try/finally` вокруг `yield ser`.
---
## 2. Дублирование `uart_*` фикстур
**Проблема.** Пять фикстур (`uart`, `uart_opto`, `uart_can`, `uart_button`,
`uart_hil_usb_cdc`) имели идентичное тело и отличались только зависимостью
`loaded_*`. При добавлении нового теста требовалось вручную копировать
очередную фикстуру.
**Решение.** Введена фабричная функция `_make_uart_fixture` и словарь
`_UART_FIXTURE_MAP`. Все фикстуры генерируются в одну строку через `globals()`.
Зависимость `loaded_*` активируется через `request.getfixturevalue()`
официальный pytest API (доступен с pytest 3.x).
```python
# До — пять одинаковых блоков
def uart_opto(request, loaded_hil_opto):
ser = _open_uart_and_wait_ready(request)
yield ser
ser.close()
def uart_can(request, loaded_hil_can):
ser = _open_uart_and_wait_ready(request)
yield ser\
ser.close()
# и т.д.
# После Фабрика + словарь
_UART_FIXTURE_MAP = {
"uart": "loaded_host_uart",
"uart_opto": "loaded_hil_opto",
"uart_can": "loaded_hil_can",
"uart_button": "loaded_hil_button",
"uart_hil_usb_cdc": "loaded_hil_usb_cdc",
}
def _make_uart_fixture(loaded_name: str):
@pytest.fixture(scope="module")
def _fixture(request: pytest.FixtureRequest) -> Generator[serial.Serial, None, None]:
request.getfixturevalue(loaded_name) # триггерит зависимость явно
port = request.config.getoption("--vcom")
with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser:
yield ser
return _fixture
# Регистрируем все фикстуры в пространстве имён модуля одной строкой
for _name, _dep in _UART_FIXTURE_MAP.items():
globals()[_name] = _make_uart_fixture(_dep)
```
Для добавления поддержки нового теста теперь достаточно одной строки
в `_UART_FIXTURE_MAP`.
---
## 3. Несогласованное чтение конфигурации
**Проблема.** Большинство параметров читались через `env_config` (`cfg.*`),
но фикстура `usb_cdc_port` обращалась к `os.environ.get()` напрямую.
Это создавало три источника истины, рассыпало дефолтные значения по коду
и лишало возможности переопределить параметр через CLI pytest.
```python
# До — нарушает единообразие конфигурации
port = os.environ.get("HIL_USB_CDC_PORT", "")
baud = int(os.environ.get("HIL_USB_CDC_BAUD", "115200"))
timeout = float(os.environ.get("HIL_USB_CDC_TIMEOUT", "5.0"))
# После
@pytest.fixture(scope="module")
def usb_cdc_port(
uart_hil_usb_cdc,
) -> Generator[serial.Serial, None, None]:
"""Открыть USB CDC порт таргета. Ждёт появления порта и DTR ready."""
import time
if not cfg.TARGET_VCOM_PORT:
pytest.skip("HIL_USB_CDC_PORT not set")
# Ждём появления USB CDC порта (enumeration после загрузки ELF).
deadline = time.monotonic() + cfg.TARGET_VCOM_TIMEOUT
ser = None
while time.monotonic() < deadline:
try:
ser = serial.Serial(port=cfg.TARGET_VCOM_PORT, baudrate=cfg.TARGET_VCOM_BAUD, timeout=0.5)
break
except serial.SerialException:
time.sleep(0.3)
if ser is None:
pytest.fail(f"USB CDC port {cfg.TARGET_VCOM_PORT} not available after {cfg.TARGET_VCOM_TIMEOUT}s")
try:
# Установить DTR чтобы firmware увидела DTE presence.
ser.dtr = True
time.sleep(0.3)
# Сбросить входной буфер — могут быть мусорные байты от enumeration.
ser.reset_input_buffer()
yield ser
finally:
ser.close()
```