# Added: cli, protocol, runner modules for firmware_test + host tests
This commit is contained in:
parent
74cef512c7
commit
bf324b2aa2
36 changed files with 26965 additions and 1001 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -69,6 +69,7 @@ compile_commands.json
|
|||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.bak
|
||||
.env
|
||||
.claude
|
||||
tools/host/.venv-host/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
0
bsp/sdram/СMakeLists.txt
Normal file
0
bsp/sdram/СMakeLists.txt
Normal file
BIN
docs/hardware/board/W25Q128_datasheet.pdf
Normal file
BIN
docs/hardware/board/W25Q128_datasheet.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/board/W25Q256_datasheet.pdf
Normal file
BIN
docs/hardware/board/W25Q256_datasheet.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/board/schematic_TFT8.pdf
Normal file
BIN
docs/hardware/board/schematic_TFT8.pdf
Normal file
Binary file not shown.
23785
docs/hardware/tft_displays/10.1'' TFT BF10150-01 datasheet.pdf
Normal file
23785
docs/hardware/tft_displays/10.1'' TFT BF10150-01 datasheet.pdf
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/hardware/tft_displays/4.3'' TFT BF4340-03 datasheet.pdf
Normal file
BIN
docs/hardware/tft_displays/4.3'' TFT BF4340-03 datasheet.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/tft_displays/7.0" TFT BF7050-03 datasheet.pdf
Normal file
BIN
docs/hardware/tft_displays/7.0" TFT BF7050-03 datasheet.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/tft_displays/8.0'' TFT BF8050-01E datasheet.pdf
Normal file
BIN
docs/hardware/tft_displays/8.0'' TFT BF8050-01E datasheet.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/tft_displays/driver_ics/HX8282-A11.pdf
Normal file
BIN
docs/hardware/tft_displays/driver_ics/HX8282-A11.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
docs/hardware/tft_displays/driver_ics/Himax-HX8264-D02.pdf
Normal file
BIN
docs/hardware/tft_displays/driver_ics/Himax-HX8264-D02.pdf
Normal file
Binary file not shown.
BIN
docs/hardware/tft_displays/driver_ics/NV3047E.pdf
Normal file
BIN
docs/hardware/tft_displays/driver_ics/NV3047E.pdf
Normal file
Binary file not shown.
520
docs/testing/PROTOCOL.md
Normal file
520
docs/testing/PROTOCOL.md
Normal 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"` и предупреждать оператора при несовпадении
|
||||
ожидаемой версии.
|
||||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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 (~30–60 с) |
|
||||
|
||||
Коды ошибок:
|
||||
|
||||
| Код | Причина |
|
||||
|---|---|
|
||||
| `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 рекламационной диагностики |
|
||||
| Тесты атомарны | Инженер сам решает что проверять |
|
||||
| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики |
|
||||
|
|
|
|||
|
|
@ -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` и хранение истории плат
|
||||
|
|
@ -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 |
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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().
|
||||
* Неблокирующий: если данных нет — возвращается немедленно.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
firmware/test/src/protocol.c
Normal file
127
firmware/test/src/protocol.c
Normal 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);
|
||||
}
|
||||
84
firmware/test/src/protocol.h
Normal file
84
firmware/test/src/protocol.h
Normal 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_ */
|
||||
89
firmware/test/src/test_module.h
Normal file
89
firmware/test/src/test_module.h
Normal 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_ */
|
||||
426
firmware/test/src/test_runner.c
Normal file
426
firmware/test/src/test_runner.c
Normal 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(¶ms);
|
||||
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;
|
||||
}
|
||||
89
firmware/test/src/test_runner.h
Normal file
89
firmware/test/src/test_runner.h
Normal 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_ */
|
||||
|
|
@ -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, драйверы нужны только если
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/** @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;
|
||||
if (s_inject_len == 0U)
|
||||
{
|
||||
return 0U;
|
||||
}
|
||||
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();
|
||||
}
|
||||
13
tests/host/mocks/bsp/usb_cdc.h
Normal file
13
tests/host/mocks/bsp/usb_cdc.h
Normal 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);
|
||||
281
tests/host/protocol/test_protocol.c
Normal file
281
tests/host/protocol/test_protocol.c
Normal 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(¶ms);
|
||||
|
||||
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(¶ms);
|
||||
|
||||
/* 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();
|
||||
}
|
||||
347
tests/host/runner/test_firmware_runner.c
Normal file
347
tests/host/runner/test_firmware_runner.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
```
|
||||
Loading…
Reference in a new issue