From 5f2876e89fa450a4fc0a4622658744f0b0abc1d9 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Tue, 7 Apr 2026 11:50:38 +0300 Subject: [PATCH] # Simple usb-cdc cli + host tests --- .vscode/tasks.json | 16 +- CMakePresets.json | 2 + firmware/test/CMakeLists.txt | 19 +- firmware/test/PLAN.md | 559 +++++++++++++++++++++++++++++++++++ firmware/test/README.md | 350 +++++++++------------- firmware/test/TODO.md | 244 +++++++++++++++ firmware/test/arch.svg | 268 ----------------- firmware/test/main.c | 51 ---- firmware/test/src/cli.c | 191 ++++++++++++ firmware/test/src/cli.h | 50 ++++ firmware/test/src/main.c | 61 ++++ tests/host/CMakeLists.txt | 15 + tests/host/cli/test_cli.c | 201 +++++++++++++ 13 files changed, 1472 insertions(+), 555 deletions(-) create mode 100644 firmware/test/PLAN.md create mode 100644 firmware/test/TODO.md delete mode 100644 firmware/test/arch.svg delete mode 100644 firmware/test/main.c create mode 100644 firmware/test/src/cli.c create mode 100644 firmware/test/src/cli.h create mode 100644 firmware/test/src/main.c create mode 100644 tests/host/cli/test_cli.c diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 88db8aa..a65e1c7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -31,7 +31,7 @@ // HOST-ТЕСТЫ // ============================================================= { - "label": "🧪 Host Tests (Debug)", + "label": "Host Tests", "group": { "kind": "test", "isDefault": true @@ -45,14 +45,14 @@ "cwd": "${workspaceFolder}", "statusbar": { "color": "#c800ff", - "label": "$(test-view-icon) Host Tests", + "label": "$(test-view-icon) Host Tests (Debug)", "detail": "Run host unit tests" } }, "problemMatcher": [] }, { - "label": "🧪 Host Tests (Release)", + "label": "🔨Host Tests (Release)", "group": "test", "type": "shell", "command": "just", @@ -79,7 +79,7 @@ "cwd": "${workspaceFolder}", "statusbar": { "color": "#00e5ff", - "label": "$(circuit-board) HIL Build", + "label": "$(circuit-board) HIL Tests (Debug)", "detail": "Build HIL target firmware" } }, @@ -91,7 +91,7 @@ // HAB-ОБРАЗЫ // ============================================================= { - "label": "📦 HAB Image", + "label": "HAB Image", "group": "build", "type": "shell", "command": "just", @@ -102,14 +102,14 @@ "cwd": "${workspaceFolder}", "statusbar": { "color": "#03b3ff", - "label": "$(package) HAB", + "label": "$(package) HAB Image Build", "detail": "Build HAB image" } }, "problemMatcher": [] }, { - "label": "📦 HAB All (Debug)", + "label": "📦 HAB Build All (Debug)", "group": "build", "type": "shell", "command": "just", @@ -122,7 +122,7 @@ "problemMatcher": [] }, { - "label": "📦 HAB All (Release)", + "label": "📦 HAB Build All (Release)", "group": "build", "type": "shell", "command": "just", diff --git a/CMakePresets.json b/CMakePresets.json index 8ef03f7..7409241 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -146,6 +146,7 @@ "test_bsp_can", "uart_host_mock_example", "test_ring_buffer", + "test_cli", "test_timeout_pattern", "uart_host_mock_example" ] @@ -160,6 +161,7 @@ "test_bsp_opto", "test_bsp_button", "test_bsp_can", + "test_cli", "uart_host_mock_example", "test_ring_buffer", "test_timeout_pattern", diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 8c4b399..dc5512d 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -3,8 +3,11 @@ set(TARGET_NAME firmware_test) -add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c - ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) +add_executable( + ${TARGET_NAME} src/main.c src/cli.c ${BSP_GENERATED}/clock_config.c + ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) + +target_include_directories(firmware_test PRIVATE src/) target_compile_definitions(${TARGET_NAME} 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_* дефайны # ----------------------------------------------------------------------------- target_link_libraries( - ${TARGET_NAME} - PRIVATE bsp_board - bsp_led - bsp_tick - bsp_uart_host - bsp_boot_xip - bsp_usb_cdc - port_log_uart - lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED -) + ${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick bsp_boot_xip bsp_status + bsp_usb_cdc) # ----------------------------------------------------------------------------- # Linker script diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md new file mode 100644 index 0000000..0640a64 --- /dev/null +++ b/firmware/test/PLAN.md @@ -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_` (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 позже? diff --git a/firmware/test/README.md b/firmware/test/README.md index b63957b..da32d80 100644 --- a/firmware/test/README.md +++ b/firmware/test/README.md @@ -1,244 +1,162 @@ -# firmware_test — Architecture +# firmware_test -> Target: NXP IMXRT1052CVJ5B -> Версия документа: 0.1 -> Статус: draft +Bare-metal прошивка входного контроля платы **MIMXRT1052CVJ5B**. +Загружается через USB ROM (SDP) в Flash. Запускается автономно при включении. --- -## 1. Назначение +## Канал связи -`firmware_test` — входная тестовая прошивка для проверки работоспособности платы при производстве и во время разработки. Запускается напрямую через BootROM (USB Serial Download), без предварительной прошивки загрузчика. После успешного прохождения всех тестов инициирует фазу провижининга (привязка Chip UID к версиям ПО). - ---- - -## 2. Workflow прошивки платы +Единственный транспорт: **USB CDC ACM** (USB1 / EHCI0, разъём J2). +UART (MCU-Link VCOM) не используется — только в HIL ELF прошивках. ```bash -BootROM (USB Serial Download, встроен в IMXRT1052) - ↓ -firmware_test (залит напрямую) - ↓ [тесты прошли, provisioning выполнен] -Флашим: Bootloader + App - ↓ -Ждём heartbeat Bootloader → App - ↓ -Плата принята +Хост (pytest / minicom / pyserial) + └── USB CDC ACM (J2) + └── firmware_test + └── cli.c → dispatch → обработчик команды ``` -Вариант с предварительной заливкой загрузчика не используется — 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 (~30–60 с) | + +Коды ошибок: + +| Код | Причина | +|---|---| +| `UNKNOWN_CMD` | Команда не найдена в таблице | +| `PARSE_ERR` | Не найдено поле `"cmd"` в JSON | +| `LINE_TOO_LONG` | Строка превысила `CLI_LINE_BUF_SIZE` | +| `SDRAM_FAIL` | Тест SDRAM выявил ошибку чтения/записи | + +--- + +## Последовательность старта ```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 │ -└─────────────────────────────────────────────────────┘ +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() ``` --- -## 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 на стенде */ -}; +firmware/test/ +├── CMakeLists.txt +├── README.md ← этот файл +└── src/ + ├── main.c # точка входа, init, main loop + ├── cli.h # публичный API CLI + ├── cli.c # буферизация RX, парсинг "cmd", dispatch + └── 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 ``` --- -## 8. Статусы тестов +## Добавление новой команды -| Статус | Значение | -|--------|-------------------------------------------------------| -| `PASS` | Тест прошёл успешно | -| `FAIL` | Тест провален, в `detail` диагностическая информация | -| `SKIP` | Тест пропущен (нет карты, нет Firefly, таймаут оператора) | +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 --- -## 9. Открытые вопросы +## Сборка и прошивка -- [ ] Формат `detail` при FAIL для каждого теста (договориться между разработчиками) -- [ ] Handshake-протокол между firmware_test и Firefly (как плата узнаёт о готовности стенда) -- [ ] Полная схема интерфейсной платы для Firefly (оптовходы, IR LED, уровни +24V) -- [ ] GUI на сервере: формат отображения `summary` и хранение истории плат \ No newline at end of file +```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 +``` + +--- + +## Тестирование + +```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`. diff --git a/firmware/test/TODO.md b/firmware/test/TODO.md new file mode 100644 index 0000000..b63957b --- /dev/null +++ b/firmware/test/TODO.md @@ -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` и хранение истории плат \ No newline at end of file diff --git a/firmware/test/arch.svg b/firmware/test/arch.svg deleted file mode 100644 index da527f6..0000000 --- a/firmware/test/arch.svg +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - - -firmware_test · high-level architecture -NXP IMXRT1052CVJ5B - - -firmware_test - - - - - USB CDC - virtual serial port - -↔ Host PC -(терминал / GUI) - - - - - - - Protocol - JSON-lines · cJSON - - - - - - Test runner - sequencer + registry - - - - - - Local UI - LEDs + display - - - - - - - - - - -self-tests - - - SDRAM - 32 MB - - QSPI - W25Q128 - - uSD - SDIO - - RTC - BM8563 - - Display - operator ✓ - - IR - idle GPIO - - - -HIL tests · Firefly AIO-3588Q - - - CAN - frame exchange - - UART TTL - echo pattern - - UART ISO - +24V RX only - - Opto-in - +24V all ch. - - - - - - HAL / BSP - - - - -firmware_test · test runner flow - - - - - Boot - clock, USB CDC, HAL init - - - - - - - Wait for host - USB connect or timeout - - - - - -self-tests - - - - SDRAM - critical - write/read pattern - - - - QSPI Flash - critical - JEDEC ID + R/W - - - - uSD - critical - mount + R/W - - - - RTC - I2C presence - set / get - - - - Display - R/G/B/W fill - btn confirm - - - - - - - - - - - - Check critical failures - SDRAM || QSPI || uSD FAIL → abort HIL, report immediately - - - - - - -HIL tests · requires Firefly - - - - CAN - full-duplex - - - - UART TTL - echo - - - - UART ISO - RX only - - - - Opto-in - all channels - - - - IR burst - 38 kHz RX - - - - - - - - Summary report - JSON · pass/fail/skip per test · overall - - - - - - - - Provisioning - OCOTP chip_uid → host DB · only on pass - - -only if overall == pass - - - - -critical test - -HIL test - -operator - - diff --git a/firmware/test/main.c b/firmware/test/main.c deleted file mode 100644 index 5b4568c..0000000 --- a/firmware/test/main.c +++ /dev/null @@ -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 -#include -#include -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); - } -} diff --git a/firmware/test/src/cli.c b/firmware/test/src/cli.c new file mode 100644 index 0000000..5d79f45 --- /dev/null +++ b/firmware/test/src/cli.c @@ -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 +#include +#include + +/* ── 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":""\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"); +} \ No newline at end of file diff --git a/firmware/test/src/cli.h b/firmware/test/src/cli.h new file mode 100644 index 0000000..25ceaa1 --- /dev/null +++ b/firmware/test/src/cli.h @@ -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 + +/** @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_ */ \ No newline at end of file diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c new file mode 100644 index 0000000..fc4982e --- /dev/null +++ b/firmware/test/src/main.c @@ -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 +#include + +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(); + } +} diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 800c755..d92e7af 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -158,3 +158,18 @@ add_host_test( ${CMAKE_SOURCE_DIR}/bsp/common/include MOCKS ${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}) diff --git a/tests/host/cli/test_cli.c b/tests/host/cli/test_cli.c new file mode 100644 index 0000000..c6893fa --- /dev/null +++ b/tests/host/cli/test_cli.c @@ -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 +#include + +/* ── 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(); +} \ No newline at end of file