# Simple usb-cdc cli + host tests

This commit is contained in:
Dmitry Akimov 2026-04-07 11:50:38 +03:00
parent 6f0e082849
commit 5f2876e89f
13 changed files with 1472 additions and 555 deletions

16
.vscode/tasks.json vendored
View file

@ -31,7 +31,7 @@
// HOST-ТЕСТЫ // HOST-ТЕСТЫ
// ============================================================= // =============================================================
{ {
"label": "🧪 Host Tests (Debug)", "label": "Host Tests",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true
@ -45,14 +45,14 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"statusbar": { "statusbar": {
"color": "#c800ff", "color": "#c800ff",
"label": "$(test-view-icon) Host Tests", "label": "$(test-view-icon) Host Tests (Debug)",
"detail": "Run host unit tests" "detail": "Run host unit tests"
} }
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "🧪 Host Tests (Release)", "label": "🔨Host Tests (Release)",
"group": "test", "group": "test",
"type": "shell", "type": "shell",
"command": "just", "command": "just",
@ -79,7 +79,7 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"statusbar": { "statusbar": {
"color": "#00e5ff", "color": "#00e5ff",
"label": "$(circuit-board) HIL Build", "label": "$(circuit-board) HIL Tests (Debug)",
"detail": "Build HIL target firmware" "detail": "Build HIL target firmware"
} }
}, },
@ -91,7 +91,7 @@
// HAB-ОБРАЗЫ // HAB-ОБРАЗЫ
// ============================================================= // =============================================================
{ {
"label": "📦 HAB Image", "label": "HAB Image",
"group": "build", "group": "build",
"type": "shell", "type": "shell",
"command": "just", "command": "just",
@ -102,14 +102,14 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"statusbar": { "statusbar": {
"color": "#03b3ff", "color": "#03b3ff",
"label": "$(package) HAB", "label": "$(package) HAB Image Build",
"detail": "Build HAB image" "detail": "Build HAB image"
} }
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "📦 HAB All (Debug)", "label": "📦 HAB Build All (Debug)",
"group": "build", "group": "build",
"type": "shell", "type": "shell",
"command": "just", "command": "just",
@ -122,7 +122,7 @@
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "📦 HAB All (Release)", "label": "📦 HAB Build All (Release)",
"group": "build", "group": "build",
"type": "shell", "type": "shell",
"command": "just", "command": "just",

View file

@ -146,6 +146,7 @@
"test_bsp_can", "test_bsp_can",
"uart_host_mock_example", "uart_host_mock_example",
"test_ring_buffer", "test_ring_buffer",
"test_cli",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"
] ]
@ -160,6 +161,7 @@
"test_bsp_opto", "test_bsp_opto",
"test_bsp_button", "test_bsp_button",
"test_bsp_can", "test_bsp_can",
"test_cli",
"uart_host_mock_example", "uart_host_mock_example",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",

View file

@ -3,9 +3,12 @@
set(TARGET_NAME firmware_test) set(TARGET_NAME firmware_test)
add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c add_executable(
${TARGET_NAME} src/main.c src/cli.c ${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
target_include_directories(firmware_test PRIVATE src/)
target_compile_definitions(${TARGET_NAME} target_compile_definitions(${TARGET_NAME}
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512) PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)
# #
@ -14,16 +17,8 @@ target_compile_definitions(${TARGET_NAME}
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны # даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
target_link_libraries( target_link_libraries(
${TARGET_NAME} ${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick bsp_boot_xip bsp_status
PRIVATE bsp_board bsp_usb_cdc)
bsp_led
bsp_tick
bsp_uart_host
bsp_boot_xip
bsp_usb_cdc
port_log_uart
lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Linker script # Linker script

559
firmware/test/PLAN.md Normal file
View file

@ -0,0 +1,559 @@
# firmware_test — Plan of Development
> Документ для нового треда. Содержит все принятые решения, текущий статус и
> пошаговый план дальнейшей разработки.
> Версия: 0.2 | Обновлён после реализации скелета + bsp_usb_cdc.
---
## Контекст проекта
**Цель прошивки:** входной контроль платы MIMXRT1052CVJ5B на производстве.
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
**Стенд:**
- Хост подключается через USB CDC ACM (J2) — единственный канал
- Тесты с внешними сигналами управляются через M5StampPLC (реле, оптовходы)
- Отдельный компьютер-сервер запускает pytest
---
## Статус на момент создания документа
| Компонент | Статус | Примечание |
|---|---|---|
| `bsp_usb_cdc` | ✅ Готов | HIL тест пройден (`05_test_usb_cdc.py`) |
| `firmware_test` скелет | ✅ Готов | `main.c` + `cli.c` + PING работает |
| Host-тест CLI | ✅ Готов | `test_cli.c`, 8 тестов, зелёные |
| `bsp_sdram` | ⬜ Не начат | |
| `bsp_qspi` | ⬜ Не начат | |
| `bsp_usd` | ⬜ Не начат | |
| Протокол v2 | ⬜ Не начат | Эволюция от cmd/ok к event-driven |
| Test runner | ⬜ Не начат | |
| Provisioning | ⬜ Не начат | |
### Закрытые архитектурные решения
> Не пересматривать без явного запроса.
- **[DECISION] Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
- **[DECISION] Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"cmd"` / `"type"`.
- **[DECISION] SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
- **[DECISION] SDRAM тест — не HIL ELF:** тест прогоняется командами через firmware_test, не отдельным ELF.
- **[DECISION] IR и RTC:** не реализуются.
---
## Протокол v2 — решение, требующее принятия в новом треде
Текущий скелет использует упрощённый протокол:
```json
{"cmd":"PING"} → {"ok":true,"result":"PONG"}
```
TODO.md описывает расширенный протокол с типами событий:
```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}
{"type":"summary","passed":7,"failed":0,"overall":"pass"}
```
**Вопросы для обсуждения в треде:**
1. Переходить ли на v2 сразу или итерационно (сначала SDRAM с cmd/ok, потом рефакторинг)?
2. Как обрабатывать `confirm_request` (display test) в cli.c — отдельный тип входящего сообщения?
3. `session_start` — посылать ли при каждом `READY` или только по команде `START_SESSION`?
**Рекомендация:** реализовать v2 до добавления первого теста, иначе рефакторинг CLI затронет уже написанные тест-модули.
---
## Структура файлов — целевое состояние
```
firmware/test/
├── CMakeLists.txt
├── README.md
└── src/
├── main.c # ✅ готов
├── cli.h / cli.c # ✅ готов (v1), требует эволюции до v2
├── protocol.h / protocol.c # ⬜ новый: сериализация ответов
├── test_runner.h / .c # ⬜ новый: реестр + sequencer
├── provisioning.h / .c # ⬜ новый: chip UID + provision_ack
└── tests/
├── test_sdram.h / .c # ⬜
├── test_qspi.h / .c # ⬜
├── test_usd.h / .c # ⬜
├── test_display.h / .c # ⬜ интерактивный (кнопки)
├── test_can.h / .c # ⬜ HIL (M5StampPLC)
├── test_uart.h / .c # ⬜ HIL TTL + ISO
└── test_opto.h / .c # ⬜ HIL (M5StampPLC)
bsp/
├── sdram/ # ⬜ CMakeLists.txt + sdram.c (структура есть)
├── qspi/ # ⬜ новый модуль
└── usd/ # ⬜ новый модуль
```
---
## Матрица тестов
| ID | Название | Тип | Critical | HIL (M5) | HIL ELF | BSP | Статус |
|---|---|---|---|---|---|---|---|
| — | PING | cmd | — | — | — | — | ✅ |
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | ❌ | `bsp_sdram` | ⬜ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | ❌ | `bsp_qspi` | ⬜ |
| `usd` | uSD (SDIO) | self | ✅ | ❌ | ❌ | `bsp_usd` | ⬜ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ❌ | существующий BSP | ⬜ |
| `can` | CAN | HIL | ❌ | ✅ | ❌ | `bsp_can` ✅ | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | `bsp_uart_host` ✅ | ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | ❌ | уточнить | ⬜ |
| `opto` | Opto-in +24V | HIL | ❌ | ✅ | ❌ | `bsp_opto` ✅ | ⬜ |
**Колонка "HIL ELF":** отдельная RAM-прошивка через pyOCD. Для firmware_test тестов — не нужна, тесты идут через USB CDC.
**Колонка "HIL (M5)":** нужна ли M5StampPLC для управления внешними сигналами.
---
## Пошаговый план
### Этап 0 — Вопросы, требующие ответа до кода
Обсудить в начале треда:
- Протокол v2: переходить сейчас или после SDRAM?
- `uart_iso`: есть отдельный BSP модуль или это тот же `bsp_uart_host` с другими параметрами?
- `usd`: карта вставлена постоянно на плате или оператор вставляет перед тестом?
- QSPI: какой конкретно чип (W25Q128?), есть ли уже sdk_flexspi таргет в CMake?
---
### Этап 1 — Протокол v2 + Test runner скелет
**Цель:** эволюция cli.c → protocol.c + test_runner.c. После этого этапа добавление каждого теста — одна строка в реестре.
**Файлы:**
```
firmware/test/src/
├── cli.c ← упрощается: только IO (read/write/buffer)
├── protocol.h/.c ← новый: сериализация всех типов сообщений
├── test_module.h ← новый: интерфейс тест-модуля
└── test_runner.h/.c ← новый: реестр + sequencer
```
**Интерфейс тест-модуля (`test_module.h`):**
```c
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;
const char *name;
bool critical;
bool requires_hil;
void (*init)(void);
test_result_t (*run)(void);
void (*deinit)(void);
} test_module_t;
```
**Сериализация (protocol.h):** функции `protocol_send_session_start()`,
`protocol_send_test_begin()`, `protocol_send_test_result()`,
`protocol_send_summary()`, `protocol_send_confirm_request()` — все через `cli_send()`.
**Команды хоста v2:**
| Входящий тип | Поле | Действие |
|---|---|---|
| `cmd` | `"run_all"` | Запустить все тесты по реестру |
| `cmd` | `"run"` + `"id"` | Запустить один тест |
| `cmd` | `"ping"` | `{"type":"pong"}` |
| `confirm` | `"id"` + `"confirmed"` | Ответ оператора на display тест |
**Host-тесты:** расширить `test_cli.c` + добавить `test_protocol.c` (Категория A).
---
### Этап 2 — `bsp_sdram`
API уже описан в `firmware_test_plan.md` (закрытое решение):
```c
bsp_sdram_status_t bsp_sdram_init(void);
bsp_sdram_status_t bsp_sdram_test_fast(bsp_sdram_result_t *result);
bsp_sdram_status_t bsp_sdram_test_full(bsp_sdram_result_t *result);
```
**Файлы:**
```
bsp/sdram/
├── CMakeLists.txt ← создать
├── include/bsp/sdram.h ← по spec из firmware_test_plan.md
└── src/sdram.c
```
**Тест модуль firmware_test:**
```
firmware/test/src/tests/test_sdram.c
```
Команды через реестр: `run` + `id: "sdram"`. Отдельной HIL ELF нет.
**HIL pytest (`tools/hil/test_sdram.py`):** подключается к firmware_test через USB CDC.
Фикстура `cdc_firmware_test` — firmware_test прошит в Flash, pytest открывает CDC порт.
**Just рецепты:** `hil-sdram` (без `--slow`), `hil-sdram-full` (с `@pytest.mark.slow`).
---
### Этап 3 — `bsp_qspi` + QSPI тест
**Аппаратный контекст:** W25Q64FVSSIG (из схемы, 8MB SPI NOR Flash), интерфейс FlexSPI.
**API:**
```c
bsp_qspi_status_t bsp_qspi_init(void);
bsp_qspi_status_t bsp_qspi_read_jedec_id(uint8_t *manufacturer, uint16_t *device_id);
bsp_qspi_status_t bsp_qspi_test(bsp_qspi_result_t *result); /* erase sector + write + verify */
```
**Что проверяет тест:**
1. JEDEC ID совпадает с ожидаемым для W25Q64 (`0xEF`, `0x4017`)
2. Erase тестового сектора (последний сектор, чтобы не трогать прошивку)
3. Write + Read + Compare 256 байт
**Нет HIL ELF, нет M5.** Тест полностью самостоятельный.
**Вопрос перед началом:** проверить есть ли `sdk_flexspi` таргет в `sdk/CMakeLists.txt`.
---
### Этап 4 — `bsp_usd` + uSD тест
**Аппаратный контекст:** SDMMC (uSD слот), интерфейс USDHC. SDK таргет `sdk_usdhc` есть в матрице.
**Стратегия:** использовать FatFS из SDK middleware (уже vendored в `middleware/fatfs/`).
**API:**
```c
bsp_usd_status_t bsp_usd_init(void); /* USDHC init + mount FAT */
bsp_usd_status_t bsp_usd_test(bsp_usd_result_t *result); /* write + read + verify */
bsp_usd_status_t bsp_usd_deinit(void); /* unmount */
```
**Поведение при отсутствии карты:** `TEST_STATUS_SKIP` (критически важно для
производственного прогона — карта может быть не вставлена).
**Вопрос:** карта вставлена постоянно или оператор вставляет? Если оператор — нужен `confirm_request` перед тестом.
---
### Этап 5 — Display тест (интерактивный)
**Что проверяем:** RGB888 интерфейс + подсветка + реакция оператора через кнопки.
**Последовательность:**
```
firmware → LCD: залить R (красный)
firmware → хост: {"type":"confirm_request","id":"display_red","timeout_ms":15000}
оператор: нажать кнопку PASS (Test_But_1) или FAIL (Test_But_2)
firmware → хост: {"type":"confirm_ack","id":"display_red","confirmed":true/false}
повторить для G, B, W
итоговый результат = AND всех подтверждений
```
**Особенности:**
- Одновременно тестируются кнопки (`bsp_button` уже есть с HIL тестами)
- Таймаут 15 с`TEST_STATUS_SKIP`
- Маркер `@pytest.mark.interactive` в pytest — не входит в `hil-run`
**HIL pytest:** `tools/hil/test_display.py` с `_operator_prompt()` через `/dev/tty`.
---
### Этап 6 — HIL тесты: CAN, UART, Opto в firmware_test
Эти BSP модули уже реализованы и имеют отдельные HIL ELF тесты.
Задача этапа — **интегрировать их в firmware_test** как тест-модули,
запускаемые через протокол v2.
#### 6.1 CAN (`bsp_can` ✅)
- Стенд: M5StampPLC подключён к CAN шине платы через интерфейсную плату
- Тест: M5 посылает CAN фрейм → плата принимает → сравниваем
#### 6.2 UART TTL (`bsp_uart_host` ✅)
- Стенд: M5 UART ↔ UART TTL платы (loopback или echo)
- Уточнить: какой UART порт на плате (LPUART1 занят MCU-Link, какой свободен?)
#### 6.3 UART ISO
- Уточнить наличие отдельного BSP модуля или это конфигурация `bsp_uart_host`
- +24V уровни через интерфейсную плату
#### 6.4 Opto (`bsp_opto` ✅)
- Стенд: M5 реле → оптовходы EXT_IN1, EXT_IN2, RS_RX
- Логика уже отработана в `02_test_opto.py` (HIL ELF)
- Переиспользовать: тот же M5 агент, другой транспорт (USB CDC вместо UART)
**Важно для всех HIL тестов этапа 6:** pytest для firmware_test использует
`cdc_firmware_test` фикстуру (USB CDC), а не `uart_<n>` (UART + pyOCD).
M5StampPLC управляет сигналами так же, как в существующих HIL ELF тестах.
---
### Этап 7 — Provisioning
**Источник UID:** OCOTP регистры через `OCOTP_GetFuseData()` (NXP HAL).
```c
void provisioning_run(provision_info_t *out);
```
**Поток:**
```
firmware → хост: {"type":"provision_ready","chip_uid":"A3F2...","fw":"0.1.0"}
хост → БД: uid ↔ fw_version (логика на хосте)
хост → firmware: {"type":"cmd","cmd":"provision_ack","fw":"1.0.0","bootloader":"1.0.0"}
firmware → хост: {"type":"provision_done","recorded":true}
```
**Запускается только при `overall == pass`.** В pytest отдельная фикстура
`provision_firmware_test`.
---
## Зависимости между этапами
```
Этап 1 (протокол v2 + runner)
├── Этап 2 (bsp_sdram)
│ └── HIL: test_sdram.py
├── Этап 3 (bsp_qspi)
│ └── HIL: test_qspi.py
├── Этап 4 (bsp_usd)
│ └── HIL: test_usd.py
├── Этап 5 (display, interactive)
│ └── HIL: test_display.py (@pytest.mark.interactive)
└── Этап 6 (CAN + UART + Opto)
└── HIL: test_can.py, test_uart.py, test_opto.py
Этап 7 (provisioning) → зависит от всех предыдущих
```
---
## BSP модули — итог
| BSP | Статус | Нужен HIL ELF | Нужен в firmware_test |
|---|---|---|---|
| `bsp_usb_cdc` | ✅ | ✅ (есть) | ✅ (есть) |
| `bsp_led` | ✅ | ❌ | ✅ |
| `bsp_tick` | ✅ | ❌ | ✅ |
| `bsp_button` | ✅ | ✅ (есть) | ✅ (display тест) |
| `bsp_opto` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_can` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_uart_host` | ✅ | ✅ (есть) | ✅ этап 6 |
| `bsp_sdram` | ⬜ | ❌ | ✅ этап 2 |
| `bsp_qspi` | ⬜ | ❌ | ✅ этап 3 |
| `bsp_usd` | ⬜ | ❌ | ✅ этап 4 |
---
## Контекст для нового треда — что передать
```
Системный промпт: тот же (роль + правила).
Приложить файлы:
- firmware_test_plan.md (обновлённый)
- usb_cdc.md
- firmware/test/src/main.c (текущий)
- firmware/test/src/cli.h/.c (текущий)
- firmware/test/CMakeLists.txt (текущий)
- tests/host/CMakeLists.txt
- tests/host/cli/test_cli.c
- этот план (PLAN.md)
Первый вопрос нового треда:
"Начинаем Этап 1 — протокол v2 и test_runner.
Ответы на открытые вопросы: [...]"
```
---
## Открытый вопрос: ПО на стороне хоста для производственного прогона
### Контекст и разделение ответственности
Принципиально важно разделить два окружения:
| Окружение | Кто запускает | Инструмент | Что тестирует |
|---|---|---|---|
| **Разработка** | Разработчик, локальный ПК | pytest (`just host::hil-*`) | Отдельные BSP модули через HIL ELF + UART |
| **Производство / Сервис** | Оператор, сервер | ??? | Плата целиком через firmware_test + USB CDC |
Это два **принципиально разных** use case с разными требованиями к UX,
надёжности и изоляции. Смешивать их в одном pytest-прогоне нельзя.
---
### Почему "просто pytest" недостаточно для производства
Производственный прогон отличается от HIL тестов разработчика по нескольким осям:
**Оператор — не разработчик.** Он не читает pytest output в терминале.
Ему нужно видеть: какой тест сейчас идёт, прошёл или нет, что делать дальше
(вставить карту, посмотреть на дисплей, нажать кнопку).
**Последовательность фиксирована.** Прогон всегда идёт по реестру:
flash → READY → run_all → summary → provisioning. Никакого выбора тестов.
**Результат — не лог, а запись в БД.** `chip_uid` + результат + версия прошивки
должны сохраняться. pytest ничего не знает о БД.
**Интерактивные тесты** (display) требуют управляемого диалога с оператором,
а не `input()` в терминале с флагом `-s`.
---
### Варианты хостового ПО — анализ
#### Вариант A: pytest + плагины + HTML отчёт
Обернуть весь прогон в pytest, добавить `pytest-html` для отчётов,
интерактивные тесты через `@pytest.mark.interactive` с `-s`.
**Плюсы:** минимум нового кода, знакомый инструмент.
**Минусы:** оператор смотрит в терминал; интерактивность через `/dev/tty` хрупкая;
provisioning (запись в БД) — костыль в фикстуре; нет живого статуса
"тест N из M идёт X секунд". Для производства неприемлемо.
#### Вариант B: TUI — Python + Textual / Rich
Самостоятельное Python-приложение с текстовым интерфейсом.
Управляет всем: flash через spsdk, M5StampPLC, USB CDC, provisioning, БД.
pytest не используется как runner — только как библиотека для assert-логики
(или вообще не используется).
**Плюсы:** полный контроль над UX; живой прогресс; чёткие диалоги оператора;
нативная запись в БД; изолировано от HIL тестов разработчика полностью.
**Минусы:** значительный объём разработки хостового ПО;
нужен отдельный репозиторий или директория `tools/production/`.
#### Вариант C: pytest как backend, TUI/GUI как frontend
pytest запускается программно через `pytest.main()` или subprocess,
результаты передаются через JSON reporter (`pytest-json-report`) в
отдельное GUI/TUI приложение которое их отображает.
**Плюсы:** переиспользуем pytest инфраструктуру (фикстуры, параллелизм).
**Минусы:** архитектурно сложно; интерактивные тесты всё равно требуют
кастомного канала; два процесса вместо одного.
#### Вариант D: pytest только для разработки, отдельный runner для производства
**Разработка:** pytest (`just host::hil-*`) — тесты отдельных BSP модулей
через HIL ELF. Остаётся как есть.
**Производство:** отдельное Python-приложение (`tools/production/run.py`)
без pytest. Использует те же низкоуровневые библиотеки
(`pyserial`, `spsdk`, M5 агент) но со своим runner-ом и TUI.
Тест-логика на стороне прошивки (в `test_runner.c`) — единственная
точка истины. Хостовое ПО только отправляет команды и интерпретирует события.
**Это рекомендуемый вариант** — наиболее чистое разделение.
---
### Рекомендуемая архитектура (Вариант D, детально)
```
tools/
├── hil/ # СУЩЕСТВУЮЩИЙ — разработчик, локальный ПК
│ ├── conftest.py # pytest фикстуры
│ ├── 01_test_uart.py # HIL тесты отдельных BSP модулей
│ ├── 02_test_opto.py
│ └── ...
└── production/ # НОВЫЙ — производство / сервис
├── pyproject.toml # отдельное окружение uv
├── run.py # точка входа: python run.py
├── runner/
│ ├── flasher.py # spsdk: flash firmware_test.elf
│ ├── cdc_client.py # USB CDC: send cmd, recv events
│ ├── m5_client.py # переиспользовать tools/hil/m5/
│ ├── test_sequence.py # порядок: flash→READY→run_all→provision
│ └── db.py # запись chip_uid + результатов
└── ui/
├── tui.py # Textual TUI: живой прогресс + диалоги
└── report.py # HTML / JSON отчёт после прогона
```
**Поток производственного прогона:**
```
Оператор запускает: python tools/production/run.py
├── flasher.py: spsdk → flash firmware_test.elf → Reset
├── cdc_client: ждёт {"type":"session_start",...}
├── TUI: показывает "Подключение... ОК"
├── cdc_client: → {"type":"cmd","cmd":"run_all"}
├── [цикл событий]
│ ├── {"type":"test_begin","id":"sdram"} → TUI: "SDRAM... ⏳"
│ ├── {"type":"test_result","id":"sdram","status":"pass"} → TUI: "SDRAM ✅ 47ms"
│ ├── {"type":"confirm_request","id":"display_red"} →
│ │ TUI: диалог оператора "Экран красный? [PASS/FAIL]"
│ │ cdc_client: → {"type":"confirm","id":"display_red","confirmed":true}
│ └── ...
├── {"type":"summary","overall":"pass"} → TUI: финальный результат
├── {"type":"provision_ready","chip_uid":"..."} →
│ db.py: записать в БД
│ cdc_client: → {"type":"cmd","cmd":"provision_ack",...}
└── TUI: "Плата принята ✅ | UID: A3F2..." | печать этикетки?
```
---
### Изоляция от HIL тестов разработчика
| Аспект | HIL тесты (разработка) | Production runner |
|---|---|---|
| Инструмент | pytest | Самостоятельный Python runner |
| Директория | `tools/hil/` | `tools/production/` |
| Окружение uv | `tools/hil/pyproject.toml` | `tools/production/pyproject.toml` |
| Just рецепты | `just host::hil-*` | `just host::production-run` |
| Прошивка | HIL ELF в RAM (pyOCD) | firmware_test в Flash (spsdk) |
| Транспорт | UART CLI (pyserial) | USB CDC JSON-lines |
| UX | Терминал / pytest output | TUI (Textual) |
| БД | Нет | Да |
| M5StampPLC | Да (реле для сигналов) | Да (те же реле) |
| CI | Да (автоматический) | Нет |
Общий код (M5 агент, низкоуровневый spsdk wrapper) можно вынести в
`tools/shared/` и подключать как локальный пакет в обоих `pyproject.toml`.
---
### Открытые вопросы для обсуждения
- [ ] **TUI библиотека:** Textual (современный, богатый) или Rich (проще, достаточно)?
Или вообще минималистичный вывод без TUI фреймворка на первой итерации?
- [ ] **БД:** SQLite локально на сервере или REST API на внешний сервис?
- [ ] **Этикетка:** нужна ли автоматическая печать после provisioning?
- [ ] **Несколько стендов:** один сервер управляет несколькими платами параллельно
или всегда одна плата?
- [ ] **Первая итерация:** допустимо ли начать с простого `run.py` без TUI
(plain print + input) и добавить TUI позже?

View file

@ -1,244 +1,162 @@
# firmware_test — Architecture # firmware_test
> Target: NXP IMXRT1052CVJ5B Bare-metal прошивка входного контроля платы **MIMXRT1052CVJ5B**.
> Версия документа: 0.1 Загружается через USB ROM (SDP) в Flash. Запускается автономно при включении.
> Статус: draft
--- ---
## 1. Назначение ## Канал связи
`firmware_test` — входная тестовая прошивка для проверки работоспособности платы при производстве и во время разработки. Запускается напрямую через BootROM (USB Serial Download), без предварительной прошивки загрузчика. После успешного прохождения всех тестов инициирует фазу провижининга (привязка Chip UID к версиям ПО). Единственный транспорт: **USB CDC ACM** (USB1 / EHCI0, разъём J2).
UART (MCU-Link VCOM) не используется — только в HIL ELF прошивках.
---
## 2. Workflow прошивки платы
```bash ```bash
BootROM (USB Serial Download, встроен в IMXRT1052) Хост (pytest / minicom / pyserial)
└── USB CDC ACM (J2)
firmware_test (залит напрямую) └── firmware_test
↓ [тесты прошли, provisioning выполнен] └── cli.c → dispatch → обработчик команды
Флашим: Bootloader + App
Ждём heartbeat Bootloader → App
Плата принята
``` ```
Вариант с предварительной заливкой загрузчика не используется — BootROM является надёжным и всегда доступным recovery-path, не зависящим от состояния Flash.
--- ---
## 3. Высокоуровневая архитектура ## Протокол
**JSON-lines**: каждая строка завершается `\n`.
| Направление | Формат |
|---|---|
| Запрос | `{"cmd":"NAME"}\n` |
| Успех | `{"ok":true,...}\n` |
| Ошибка | `{"ok":false,"error":"CODE"}\n` |
Ограничения:
- Максимальная длина строки: **128 байт** включая `\n` (`CLI_LINE_BUF_SIZE`)
- Терминатор: `\n` (LF, `0x0A`). `\r\n` (CR+LF) тоже принимается
---
## Команды
| Команда | Запрос | Успешный ответ | Описание |
|---|---|---|---|
| `PING` | `{"cmd":"PING"}` | `{"ok":true,"result":"PONG"}` | Проверка связи |
| `SDRAM_TEST` | `{"cmd":"SDRAM_TEST"}` | `{"ok":true,"time_ms":N}` | Быстрый тест SDRAM (≤100 мс) |
| `SDRAM_TEST_FULL` | `{"cmd":"SDRAM_TEST_FULL"}` | `{"ok":true,"time_ms":N}` | Полный тест SDRAM (~3060 с) |
Коды ошибок:
| Код | Причина |
|---|---|
| `UNKNOWN_CMD` | Команда не найдена в таблице |
| `PARSE_ERR` | Не найдено поле `"cmd"` в JSON |
| `LINE_TOO_LONG` | Строка превысила `CLI_LINE_BUF_SIZE` |
| `SDRAM_FAIL` | Тест SDRAM выявил ошибку чтения/записи |
---
## Последовательность старта
```bash ```bash
┌─────────────────────────────────────────────────────┐ board_hw_init() тактирование, MPU, кэш, пины
│ firmware_test │ bsp_tick_init() SysTick 1 мс
│ │ bsp_led_init() оба LED выключены
│ USB CDC ──► Protocol ──► Test runner ──► Local UI │ bsp_usb_cdc_init() PHY + USB стек + NVIC
│ (JSON-lines) (sequencer) (LED+disp) │
│ │ [ожидание хоста] LED_HEARTBEAT мигает 200 мс
│ ┌─────────────────────┐ ┌──────────────────────┐ │ bsp_usb_cdc_is_ready() == true
│ │ self-tests │ │ HIL tests │ │
│ │ SDRAM QSPI uSD │ │ CAN UART Opto-in │ │ LED_APP on
│ │ RTC Display IR │ │ UART ISO IR burst │ │ → {"ok":true,"result":"READY"} сигнал готовности хосту
│ └─────────────────────┘ └──────────────────────┘ │
│ │ [главный цикл]
│ HAL / BSP │ bsp_usb_cdc_poll()
└─────────────────────────────────────────────────────┘ cli_process()
``` ```
--- ---
## 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 ```bash
плата посылает provision_ready + chip_uid firmware/test/
├── CMakeLists.txt
хост записывает в БД: uid ↔ fw_version ↔ bootloader_version ├── README.md ← этот файл
└── src/
хост посылает provision_ack ├── main.c # точка входа, init, main loop
├── cli.h # публичный API CLI
плата устанавливает provisioned = true ├── cli.c # буферизация RX, парсинг "cmd", dispatch
``` └── tests/
├── test_sdram.c # SDRAM_TEST / SDRAM_TEST_FULL [TODO]
Вся логика на стороне хоста (запись в БД, генерация сертификата, привязка партии). Прошивка только читает UID и ждёт подтверждения. ├── test_sdram.h
├── test_led.c # LED_ON / LED_OFF [TODO]
--- ├── test_led.h
├── test_button.c # BUTTON_READ [TODO]
## 7. Реестр тестов └── test_button.h
```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. Статусы тестов ## Добавление новой команды
| Статус | Значение | 1. В `cli.c` объявить обработчик: `static void cmd_foo(void);`
|--------|-------------------------------------------------------| 2. Добавить строку в таблицу `k_cmds[]`: `{ "FOO", cmd_foo }`
| `PASS` | Тест прошёл успешно | 3. Реализовать обработчик в секции `Command handlers`
| `FAIL` | Тест провален, в `detail` диагностическая информация | 4. Добавить тест в `tests/host/cli/test_cli.c`
| `SKIP` | Тест пропущен (нет карты, нет Firefly, таймаут оператора) | 5. Обновить таблицу команд в этом README
--- ---
## 9. Открытые вопросы ## Сборка и прошивка
- [ ] Формат `detail` при FAIL для каждого теста (договориться между разработчиками) ```bash
- [ ] Handshake-протокол между firmware_test и Firefly (как плата узнаёт о готовности стенда) # devcontainer
- [ ] Полная схема интерфейсной платы для Firefly (оптовходы, IR LED, уровни +24V) just build::build-firmware-test-debug
- [ ] GUI на сервере: формат отображения `summary` и хранение истории плат just build::hab-firmware-test-debug
# хост (плата в SDP-режиме: BOOT_MOD_1 → 3V3 → Reset)
just host::flash-test-debug
# вернуть в нормальный режим
# BOOT_MOD_1 → GND → Reset
```
---
## Тестирование
```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 с)
```
---
## Переменные окружения
```bash
HIL_USB_CDC_PORT= # порт USB CDC таргета, например /dev/ttyACM1
HIL_USB_CDC_BAUD= # 115200
HIL_USB_CDC_TIMEOUT= # 5.0
```
Шаблон: `.env.example`. Актуальные значения: `.env` (не коммитится).
---
## Известные ограничения
- **Один пакет за цикл**: `cli_process()` читает один USB bulk-пакет за вызов
главного цикла. При потоке команд без задержки буфер может не успеть
обработаться — добавить задержку на стороне хоста между командами (≥10 мс).
- **TX неблокирующий**: если TX занят — ответ теряется. Хост должен ждать
предыдущий ответ перед отправкой следующей команды.
- **Нет персистентного состояния**: при сбросе питания все результаты теряются.
pytest должен повторно подключаться и дожидаться `READY`.

244
firmware/test/TODO.md Normal file
View file

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

View file

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

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,51 +0,0 @@
#include "board.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/uart_host.h"
#include "bsp/usb_cdc.h"
#include "log/log.h"
#include "port/log_uart.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
int main(void)
{
const uint16_t DELAY_MS = 100;
const uint32_t UART_BAUDRATE = 115200;
board_hw_init();
bsp_led_init();
bsp_tick_init();
bsp_uart_host_init(UART_BAUDRATE);
log_uart_init();
LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms());
if (bsp_usb_cdc_init() == BSP_OK)
{
LOG_I("BOOT", "USB CDC ACM initialized");
}
bsp_led_on(LED_APP);
bool is_connection_established = false;
while (1)
{
if (bsp_usb_cdc_is_ready() == true && is_connection_established == false)
{
is_connection_established = true;
LOG_I("BOOT", "USB CDC ACM ready");
}
if (is_connection_established == true)
{
const char *msg = "Hello from TFT Board\r\n";
bsp_usb_cdc_write((const uint8_t *) msg, strlen(msg));
}
bsp_usb_cdc_poll();
bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(DELAY_MS);
}
}

191
firmware/test/src/cli.c Normal file
View file

@ -0,0 +1,191 @@
/**
* @file cli.c
* @brief Реализация CLI для firmware_test.
*
* Транспорт: USB CDC ACM через bsp_usb_cdc единственный канал.
* Парсинг JSON минималистичный: strstr по полю "cmd".
* Полноценный JSON-парсер (cJSON) не используется намеренно
* схема фиксирована, единственное входящее поле "cmd".
*
* Добавление новой команды:
* 1. Объявить static void cmd_foo(void); выше таблицы.
* 2. Добавить { "FOO", cmd_foo } в k_cmds[].
* 3. Реализовать обработчик ниже раздела "Command handlers".
*/
#include "cli.h"
#include "bsp/usb_cdc.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
/* ── Forward declarations ──────────────────────────────────────────────── */
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]))
/* ── 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-строки.
*
* Ищет паттерн ` "cmd":"<ASCII без кавычек и обратных слэшей>"\n`. Без рекурсии и динамической памяти.
*
* @param[in] line NULL-terminated входная строка.
* @param[out] out Буфер для записи значения.
* @param[in] out_size Размер out (включая место под '\0').
* @return true если поле найдено и значение помещается в out.
*/
static bool parse_cmd_field(const char *p_line, 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, ':');
if (colon == NULL)
{
return false;
}
const char *open_q = strchr(colon + 1U, '"');
if (open_q == NULL)
{
return false;
}
open_q++; /* skip the opening quote */
const char *close_q = strchr(open_q, '"');
if (close_q == NULL)
{
return false;
}
size_t len = (size_t) (close_q - open_q);
if (len == 0U || len >= out_size)
{
return false;
}
memcpy(p_out, open_q, len);
p_out[len] = '\0';
return true;
}
/**
* @brief Найти и вызвать обработчик команды.
*
* Если команда не найдена отправить UNKNOWN_CMD.
*/
static void dispatch(const char *p_cmd_name)
{
for (size_t i = 0U; i < CLI_CMD_COUNT; i++)
{
if (strcmp(k_cmds[i].name, p_cmd_name) == 0)
{
k_cmds[i].handler();
return;
}
}
cli_send("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n");
}
/**
* @brief Обработать одну накопленную строку (без завершающего '\n').
*/
static void process_line(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");
return;
}
dispatch(cmd_name);
}
/* ── Public API ────────────────────────────────────────────────────────── */
void cli_init(void)
{
g_s_line_len = 0U;
}
void cli_send(const char *p_resp)
{
bsp_usb_cdc_write((const uint8_t *) p_resp, strlen(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++)
{
uint8_t byte = chunk[byte_idx];
if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U))
{
g_s_line_len = 0U;
cli_send("{\"ok\":false,\"error\":\"LINE_TOO_LONG\"}\n");
continue;
}
if (byte == (uint8_t) '\n')
{
/* Отбросить '\r' если терминал шлёт CR+LF */
if (g_s_line_len > 0U && g_s_line_buf[g_s_line_len - 1U] == (uint8_t) '\r')
{
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
{
g_s_line_buf[g_s_line_len] = byte;
g_s_line_len++;
}
}
}
/* ── Command handlers ──────────────────────────────────────────────────── */
static void cmd_ping(void)
{
cli_send("{\"ok\":true,\"result\":\"PONG\"}\n");
}

50
firmware/test/src/cli.h Normal file
View file

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

61
firmware/test/src/main.c Normal file
View file

@ -0,0 +1,61 @@
/**
* @file main.c
* @brief firmware_test точка входа.
*
* Bare-metal входной контроль платы.
* Единственный канал хосттаргет: USB CDC ACM (J2).
* Протокол: JSON-lines через cli.c.
*
* Последовательность старта:
* 1. board_hw_init() тактирование, MPU, кэш, пины
* 2. bsp_tick_init() SysTick 1 мс
* 3. bsp_led_init() оба LED выключены
* 4. bsp_usb_cdc_init() PHY + стек + NVIC
* 5. Ожидание CDC ready LED_HEARTBEAT мигает
* 6. cli_init() сброс буфера
* 7. READY хост JSON сигнал готовности
* 8. Главный цикл poll + cli_process
*/
#include "board.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
#include <stdbool.h>
#include <stdint.h>
int main(void)
{
const uint32_t CONNECT_BLINK_MS = 200U;
const uint32_t ERROR_BLINK_MS = 50;
board_hw_init();
bsp_led_init();
bsp_tick_init();
if (bsp_usb_cdc_init() != BSP_OK)
{
bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(ERROR_BLINK_MS);
}
/* Ожидать подключения хоста. LED_HEARTBEAT мигает — прошивка жива. */
while (!bsp_usb_cdc_is_ready())
{
bsp_usb_cdc_poll();
bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(CONNECT_BLINK_MS);
}
bsp_led_on(LED_APP);
cli_init();
cli_send("{\"ok\":true,\"result\":\"READY\"}\n");
while (1)
{
bsp_usb_cdc_poll();
cli_process();
}
}

View file

@ -158,3 +158,18 @@ add_host_test(
${CMAKE_SOURCE_DIR}/bsp/common/include ${CMAKE_SOURCE_DIR}/bsp/common/include
MOCKS MOCKS
${BSP_MOCKS_DIR}) ${BSP_MOCKS_DIR})
# -----------------------------------------------------------------------------
# Тест логики cli
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_cli
SOURCES
cli/test_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})

201
tests/host/cli/test_cli.c Normal file
View file

@ -0,0 +1,201 @@
/**
* @file test_cli.c
* @brief Host unit-тест для firmware_test/src/cli.c
*
* Тестирует: parse_cmd_field (через cli_process), dispatch, обработку
* граничных случаев буфера. Без железа, без fff stub-ы для
* bsp_usb_cdc_read/write определены прямо здесь.
*
* Добавление теста для новой команды:
* 1. stub_rx_feed() подать строку в RX буфер.
* 2. cli_process() запустить.
* 3. TEST_ASSERT_EQUAL_STRING() проверить TX буфер.
*/
#include "bsp/status.h"
#include "bsp/usb_cdc.h"
#include "unity.h"
#include <stdint.h>
#include <string.h>
/* ── 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().
*
* */
static uint8_t s_rx_buf[256];
static size_t s_rx_len = 0U;
static size_t s_rx_pos = 0U;
static char s_tx_buf[512];
static size_t s_tx_len = 0U;
/** @brief Заполнить RX буфер данными для теста. */
static void stub_rx_feed(const char *data)
{
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;
return to_copy;
}
bsp_status_t bsp_usb_cdc_write(const uint8_t *data, size_t len)
{
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;
}
/* Остальные символы 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)
{
}
/* ── Включаем тестируемый модуль ПОСЛЕ stub-ов ─────────────────────── */
#include "cli.c" /* NOLINT(bugprone-suspicious-include) */
/* ── Фикстуры ──────────────────────────────────────────────────────── */
void setUp(void)
{
stub_reset();
cli_init();
}
void tearDown(void)
{
}
/* ── Тесты ─────────────────────────────────────────────────────────── */
void test_ping_returns_pong(void)
{
stub_rx_feed("{\"cmd\":\"PING\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
}
void test_unknown_command(void)
{
stub_rx_feed("{\"cmd\":\"FOOBAR\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n", stub_tx_get());
}
void test_missing_cmd_field(void)
{
stub_rx_feed("{\"foo\":\"bar\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n", stub_tx_get());
}
void test_empty_line_no_response(void)
{
/* Пустая строка '\n' — буфер пуст, dispatch не вызывается */
stub_rx_feed("\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
}
void test_line_too_long(void)
{
/* 127 байт + '\n' = ровно граница CLI_LINE_BUF_SIZE */
char long_line[CLI_LINE_BUF_SIZE + 2U];
memset(long_line, 'A', 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());
}
void test_two_commands_in_sequence(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());
}
void test_cmd_field_with_spaces(void)
{
/* Пробелы вокруг ':' — реальные клиенты могут так форматировать */
stub_rx_feed("{\"cmd\" : \"PING\"}\n");
cli_process();
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
}
void test_partial_input_no_response_until_newline(void)
{
/* Подать строку без '\n' — ответа быть не должно */
stub_rx_feed("{\"cmd\":\"PING\"}");
cli_process();
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
}
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);
return UNITY_END();
}