# firmware_test: can & opto
This commit is contained in:
parent
293b61fff3
commit
20bebf6d5e
15 changed files with 1518 additions and 168 deletions
|
|
@ -48,11 +48,11 @@ HIL_VCOM_BAUD=115200
|
|||
HIL_READY_TIMEOUT=5.0
|
||||
|
||||
# M5Stack StamPLC (промежуточная платформа для HIL)
|
||||
HIL_M5_PORT=/dev/cu.usbmodem211101
|
||||
HIL_M5_PORT=/dev/cu.usbmodem11101
|
||||
HIL_M5_BAUD=115200
|
||||
HIL_M5_TIMEOUT=3.0
|
||||
|
||||
# VCOM на плате таргета
|
||||
HIL_USB_CDC_PORT=/dev/cu.usbmodem21201
|
||||
HIL_USB_CDC_PORT=/dev/cu.usbmodem11301
|
||||
HIL_USB_CDC_BAUD=115200
|
||||
HIL_USB_CDC_TIMEOUT=5.0
|
||||
|
|
@ -133,6 +133,48 @@ sequenceDiagram
|
|||
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
|
||||
```
|
||||
|
||||
### `list_tests` — получить список тестов
|
||||
|
||||
Таргет возвращает реестр тестов с метаданными. TUI строит список
|
||||
динамически на основе этого ответа, не хардкодит тесты.
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"list_tests"}
|
||||
← {"type":"test_list","tests":[
|
||||
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
|
||||
{"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false},
|
||||
{"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
|
||||
{"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false},
|
||||
{"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false},
|
||||
{"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false},
|
||||
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true},
|
||||
{"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}
|
||||
]}
|
||||
```
|
||||
|
||||
### `run_selected` — запуск подмножества тестов
|
||||
|
||||
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,
|
||||
не по порядку в запросе. Таргет не фильтрует по `requires_hil` —
|
||||
ответственность за фильтрацию HIL-тестов лежит на TUI.
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]}
|
||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_begin","id":"qspi",...}
|
||||
← {"type":"test_result","id":"qspi",...}
|
||||
← {"type":"test_begin","id":"display",...}
|
||||
← {"type":"test_result","id":"display",...}
|
||||
← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"}
|
||||
```
|
||||
|
||||
Если хотя бы один ID не найден в реестре — ни один тест не запускается:
|
||||
|
||||
```json
|
||||
← {"ok":false,"error":"UNKNOWN_TEST"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## События таргета → хост
|
||||
|
|
@ -205,10 +247,10 @@ sequenceDiagram
|
|||
### Ошибки протокола
|
||||
|
||||
| Код | Причина |
|
||||
| --------------- | ----------------------------------------------- |
|
||||
| --------------- | -------------------------------------------------------------------------- |
|
||||
| `PARSE_ERR` | Строка не является валидным JSON-lines запросом |
|
||||
| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение |
|
||||
| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре |
|
||||
| `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID |
|
||||
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
|
||||
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
|
||||
|
||||
|
|
@ -358,8 +400,8 @@ const test_module_t k_test_foo = {
|
|||
};
|
||||
```
|
||||
|
||||
3. Добавить `&k_test_foo` в реестр `test_runner.c`.
|
||||
4. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета.
|
||||
1. Добавить `&k_test_foo` в реестр `test_runner.c`.
|
||||
2. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ add_executable(
|
|||
src/cli.c
|
||||
src/protocol.c
|
||||
src/test_runner.c
|
||||
src/tests/test_opto.c
|
||||
src/tests/test_sdram.c
|
||||
src/tests/test_can.c
|
||||
src/tests/test_qspi.c
|
||||
src/tests/test_usd.c
|
||||
src/tests/test_display.c
|
||||
|
|
@ -38,6 +40,7 @@ target_compile_definitions(
|
|||
target_link_libraries(
|
||||
${TARGET_NAME}
|
||||
PRIVATE bsp_board
|
||||
bsp_can
|
||||
bsp_led
|
||||
bsp_button
|
||||
bsp_display
|
||||
|
|
@ -46,6 +49,7 @@ target_link_libraries(
|
|||
bsp_usb_cdc
|
||||
bsp_sdram
|
||||
bsp_qspi_flash
|
||||
bsp_opto
|
||||
bsp_sd
|
||||
firmware_test_fatfs)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
# firmware_test — План разработки
|
||||
|
||||
> Версия: 0.5 | Обновлён после завершения Этапа 5 (display + buttons).
|
||||
> Версия: 0.6 | Обновлён после завершения Этапа 5 (display + buttons) и архитектурных решений по Этапам 6–8.
|
||||
|
||||
---
|
||||
|
||||
## Контекст проекта
|
||||
|
||||
**Цель прошивки:** входной контроль платы MIMXRT1052CVJ5B на производстве.
|
||||
**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации).
|
||||
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
|
||||
|
||||
**Стенд:**
|
||||
|
||||
- Хост подключается через USB CDC ACM — единственный канал firmware_test
|
||||
- Тесты с внешними сигналами управляются через M5StampPLC
|
||||
- Сервер запускает `tools/hil/` (разработка) или `tools/production/` (производство)
|
||||
- HIL-тесты управляются через M5StampPLC (опционально)
|
||||
- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -24,16 +25,38 @@
|
|||
| firmware_test скелет | ✅ | `main.c` + `cli.c` |
|
||||
| Протокол v2 + test_runner | ✅ | JSON-lines event-driven |
|
||||
| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention |
|
||||
| `bsp_qspi_flash` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock |
|
||||
| `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
|
||||
| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
|
||||
| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага |
|
||||
| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified |
|
||||
| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified |
|
||||
| CAN test | ⬜ | Этап 6 (bsp_can ✅) |
|
||||
| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) |
|
||||
| UART ISO test | ⬜ | Этап 6 |
|
||||
| Opto test | ⬜ | Этап 6 (bsp_opto ✅) |
|
||||
| Протокол: `list_tests` | ⬜ | Этап 6а |
|
||||
| Протокол: `run_selected` | ⬜ | Этап 6а |
|
||||
| `test_opto` | ⬜ | Этап 6б (bsp_opto ✅) |
|
||||
| `test_can` | ⬜ | Этап 6в (bsp_can ✅) |
|
||||
| `bsp_mqs` + `test_mqs` | ⬜ | Этап 6г |
|
||||
| HIL pytest firmware_cdc | ⬜ | Этап 6д |
|
||||
| Provisioning | ⬜ | Этап 7 |
|
||||
| TUI сервисного инженера | ⬜ | Этап 8 |
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов — итоговая
|
||||
|
||||
| ID | Название | Critical | HIL | Тип | BSP | Статус |
|
||||
| --------- | ------------------ | -------- | --- | ----------- | ------------------- | ------ |
|
||||
| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ |
|
||||
| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ |
|
||||
| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ |
|
||||
| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ |
|
||||
| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ |
|
||||
| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ |
|
||||
| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ⬜ |
|
||||
| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ⬜ |
|
||||
|
||||
**Убранные тесты (закрытые решения):**
|
||||
|
||||
- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется
|
||||
- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -41,129 +64,210 @@
|
|||
|
||||
> Не пересматривать без явного запроса.
|
||||
|
||||
### Этапы 1–5 (ранее зафиксированные)
|
||||
|
||||
- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
|
||||
- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`.
|
||||
- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
|
||||
- **SDRAM тест:** прогоняется командами через firmware_test, не отдельным HIL ELF.
|
||||
- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION` в CMakeLists.
|
||||
- **QSPI IRQ lock:** `__get_PRIMASK()` + DSB/ISB. Публичные API под полным lock.
|
||||
- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`.
|
||||
- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7).
|
||||
- **Порядок init в main.c:** `bsp_qspi_init()` до `bsp_tick_init()`.
|
||||
- **IR и RTC:** не реализуются.
|
||||
- **Производственный runner:** Вариант D — отдельный `tools/production/` без pytest.
|
||||
- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`.
|
||||
- **Тест дисплея:** 4 цвета + 2 ротации (TFT ≠ TFT4). Таймаут confirm 15 с → FAIL.
|
||||
- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL.
|
||||
- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP.
|
||||
|
||||
---
|
||||
### Этап 6 (новые решения)
|
||||
|
||||
## Этап 4 — bsp_sd + test_usd ✅
|
||||
- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL).
|
||||
TUI фильтрует HIL-тесты если M5StampPLC не подключён.
|
||||
- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста.
|
||||
TUI строит UI динамически, не хардкодит список тестов.
|
||||
- **`run_selected`:** запуск произвольного подмножества тестов по списку ID.
|
||||
Порядок выполнения — как в реестре таргета, не как в запросе.
|
||||
Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI.
|
||||
- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request`
|
||||
от HIL-теста TUI командует M5, получает результат, отправляет confirm.
|
||||
- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты
|
||||
недоступны (серые в UI, не входят в `run_selected`).
|
||||
- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`.
|
||||
- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен
|
||||
один канал. Буфер всегда стерео (L+R идентичны).
|
||||
- **MQS тест:** захардкоженная мелодия 3–5 с, `confirm_request("mqs_tone")`,
|
||||
оператор слышит → OK/FAIL. `critical=false`, `requires_hil=false`.
|
||||
|
||||
### Аппаратный контекст
|
||||
### Этап 8 (TUI решения)
|
||||
|
||||
| Параметр | Значение |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| Интерфейс | USDHC (SDIO) |
|
||||
| Карта | microSD, вставляется оператором перед тестом |
|
||||
| Файловая система | FatFS (SDK middleware) |
|
||||
| Детект карты | `USDHC_GetPresentStatusFlags` |
|
||||
|
||||
### Закрытые решения — Этап 4
|
||||
|
||||
- BSP-слой: `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` (FatFS).
|
||||
`bsp_usd` как отдельный модуль не создавался — тест работает напрямую через `bsp_sd` + `ff.h`.
|
||||
- Card detect: `bsp_sd_is_inserted()` через `USDHC_GetPresentStatusFlags`. GPIO-прерывание не используется.
|
||||
- Карта вставляется оператором по запросу (pre_confirm). Между тестами может извлекаться.
|
||||
- Drive `2:/`. Тестовый файл `2:/FWTEST.TMP`, удаляется в любом исходе (через `deinit()`).
|
||||
- Паттерн: `byte[i] = i & 0xFF`, 4096 байт.
|
||||
- `critical = false`: тест не блокирует HIL-тесты при отсутствии карты.
|
||||
- Confirm timeout: 30 000 мс (`PROTOCOL_CONFIRM_TIMEOUT_MS`).
|
||||
- `SD_HostInit` не вызывается в `bsp_sd_init()` — `sd_disk_initialize` делает полный init. Двойной init даёт `FR_NOT_READY`.
|
||||
- Отдельный `test_usd.h` не создавался — `extern K_TEST_USD` объявлен в `test_runner.c`.
|
||||
- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost).
|
||||
Оператор сам переставляет перемычку BOOT — это ок, документируется.
|
||||
- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130)
|
||||
или CDC firmware_test (session_start) — и показывает соответствующий экран.
|
||||
- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты,
|
||||
работает в SSH-сессии, вписывается в uv-экосистему.
|
||||
- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/`
|
||||
и `tools/production/`.
|
||||
|
||||
---
|
||||
|
||||
## Этап 5 — Display + Buttons ✅
|
||||
## Этап 6 — test_can + test_opto + test_mqs + протокол ← ТЕКУЩИЙ
|
||||
|
||||
### Аппаратный контекст кнопок
|
||||
### 6а — Расширение протокола
|
||||
|
||||
| Кнопка | Пин MCU | GPIO | Схема | Нажатие |
|
||||
| ---------- | ---------- | --------- | ----------------------------- | ------- |
|
||||
| Test_But_1 | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW |
|
||||
| Test_But_2 | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW |
|
||||
**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md`
|
||||
|
||||
### Закрытые решения — test_display
|
||||
#### Новая команда `list_tests`
|
||||
|
||||
- `pre_confirm_prompt = NULL` — нет pre-confirm, `test_begin` отправляется сразу.
|
||||
- 6 шагов confirm: 4 цвета (Red/Green/Blue/White) + 2 ротации (только для TFT ≠ TFT4).
|
||||
- Таймаут каждого confirm: 15 000 мс. Не подтверждён → FAIL с `detail = "<id> not confirmed"`.
|
||||
- Ротация: `ROTATE_0` + `FLIP_HORIZONTAL`. Восстановить `ROTATE_0` в любом исходе.
|
||||
- Фреймбуфер: статический в NonCacheable SDRAM (`AT_NONCACHEABLE_SECTION_ALIGN`, 64-byte align).
|
||||
- Тип дисплея: `DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8` через CMake compile definition.
|
||||
|
||||
### Закрытые решения — test_buttons
|
||||
|
||||
- `pre_confirm_prompt = NULL` — `confirm_request` используется только как UI-подсказка оператору.
|
||||
- Хост **не** отправляет `{"type":"confirm",...}`. Детект нажатия — через `bsp_button_get_event_pressed()`.
|
||||
- Таймаут: 10 000 мс → `TEST_STATUS_SKIP` (не FAIL).
|
||||
- Порядок: But_1 → But_2.
|
||||
- `bsp_button_poll()` вызывается каждые 5 мс через rate-limiting по `bsp_tick_get_ms()`.
|
||||
- На каждом poll дренируются события **обеих** кнопок: предотвращает stale-событие от нецелевой кнопки.
|
||||
- `bsp_button_init()` вызывается в `init()` тест-модуля — сброс debounce-счётчиков перед тестом.
|
||||
- `bsp_tick_delay_ms()` не используется — polling pattern аналогичен `test_runner_wait_confirm()`.
|
||||
|
||||
---
|
||||
|
||||
## Этап 6 — CAN + UART + Opto (HIL, M5StampPLC) ← ТЕКУЩИЙ
|
||||
|
||||
Все три теста `requires_hil = true`. Запускаются только при наличии стенда.
|
||||
BSP для всех трёх уже готов.
|
||||
|
||||
### test_can
|
||||
|
||||
M5StampPLC отправляет CAN-фрейм → плата принимает → сравниваем ID и payload.
|
||||
|
||||
**Шаги:**
|
||||
1. Таргет посылает `confirm_request` → M5 получает команду `can_send`
|
||||
2. M5 → `{"cmd":"can_send","id":0x100,"data":[0xDE,0xAD,0xBE,0xEF]}`
|
||||
3. Таргет: ожидает CAN-фрейм, 500 мс → верификация ID и payload
|
||||
4. Ответный: таргет посылает → M5 `can_recv` → верификация
|
||||
|
||||
### test_uart_ttl
|
||||
|
||||
M5 loopback через UART TTL → echo-верификация.
|
||||
|
||||
### test_uart_iso
|
||||
|
||||
M5 RLY2 → RS_RX оптовход (BSP_OPTO_CH_RS) → детект ACTIVE/INACTIVE.
|
||||
Использует `bsp_opto` с `rs_as_gpio=true`.
|
||||
|
||||
### test_opto
|
||||
|
||||
M5 RLY3/RLY4 → EXT_IN1/IN2 → детект ACTIVE/INACTIVE через `bsp_opto`.
|
||||
|
||||
**Параметры стенда (из HIL_BENCH.md):**
|
||||
|
||||
```
|
||||
RLY2 → RS_RX (BSP_OPTO_CH_RS) GPIO1[23]
|
||||
RLY3 → EXT_IN1 (BSP_OPTO_CH_IN1) GPIO1[22]
|
||||
RLY4 → EXT_IN2 (BSP_OPTO_CH_IN2) GPIO1[21]
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"list_tests"}
|
||||
← {"type":"test_list","tests":[
|
||||
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
|
||||
{"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false},
|
||||
{"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
|
||||
{"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false},
|
||||
{"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false},
|
||||
{"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false},
|
||||
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true},
|
||||
{"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}
|
||||
]}
|
||||
```
|
||||
|
||||
### HIL pytest для Этапа 6
|
||||
#### Новая команда `run_selected`
|
||||
|
||||
Тесты firmware_test через USB CDC — отдельные от существующих HIL ELF тестов:
|
||||
|
||||
```
|
||||
tools/hil/
|
||||
├── conftest.py ← добавить фикстуру firmware_cdc (USB CDC клиент)
|
||||
├── 06_test_firmware_can.py ← M5 + USB CDC
|
||||
├── 06_test_firmware_uart.py
|
||||
└── 06_test_firmware_opto.py
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]}
|
||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_begin","id":"qspi",...}
|
||||
← {"type":"test_result","id":"qspi",...}
|
||||
← {"type":"test_begin","id":"display",...}
|
||||
← {"type":"test_result","id":"display",...}
|
||||
← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"}
|
||||
```
|
||||
|
||||
Фикстура `firmware_cdc` открывает CDC порт firmware_test (прошит в Flash),
|
||||
посылает JSON команды, читает события.
|
||||
Если хотя бы один ID не найден в реестре:
|
||||
|
||||
```json
|
||||
← {"ok":false,"error":"UNKNOWN_TEST"}
|
||||
```
|
||||
|
||||
**Реализация в `test_runner.c`:**
|
||||
|
||||
- Новый режим `RUNNER_MODE_SELECTED`
|
||||
- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc
|
||||
- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция
|
||||
|
||||
### 6б — test_opto.c
|
||||
|
||||
**Файл:** `firmware/test/src/tests/test_opto.c`
|
||||
|
||||
6 шагов, попарно ACTIVE/INACTIVE для трёх каналов:
|
||||
|
||||
| Шаг | confirm_request id | M5 действие | Проверка |
|
||||
| --- | ------------------- | ----------- | -------------------------------- |
|
||||
| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` |
|
||||
| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` |
|
||||
| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` |
|
||||
| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` |
|
||||
| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` |
|
||||
| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` |
|
||||
|
||||
- Init: `bsp_opto_init()` единым вызовом для всех каналов
|
||||
- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`)
|
||||
- FAIL при несоответствии: `detail = "<id> state mismatch: expected ACTIVE got INACTIVE"`
|
||||
- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг
|
||||
|
||||
### 6в — test_can.c
|
||||
|
||||
**Файл:** `firmware/test/src/tests/test_can.c`
|
||||
|
||||
2 шага, оба направления независимо:
|
||||
|
||||
**Шаг 1 — RX (M5 → таргет):**
|
||||
|
||||
```bash
|
||||
confirm_request("can_rx_ready")
|
||||
→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF])
|
||||
→ TUI: confirm(true)
|
||||
→ таргет: bsp_can_receive(&frame, 500 мс)
|
||||
→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF]
|
||||
→ FAIL если timeout или несовпадение
|
||||
```
|
||||
|
||||
**Шаг 2 — TX (таргет → M5):**
|
||||
|
||||
```bash
|
||||
bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс)
|
||||
confirm_request("can_tx_verify")
|
||||
→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data
|
||||
→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет
|
||||
→ FAIL если confirmed=false или timeout
|
||||
```
|
||||
|
||||
- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует
|
||||
- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()`
|
||||
|
||||
### 6г — bsp_mqs + test_mqs.c
|
||||
|
||||
**Файлы:** `bsp/mqs/` (новый BSP-модуль) + `firmware/test/src/tests/test_mqs.c`
|
||||
|
||||
**bsp_mqs:**
|
||||
|
||||
- MQS требует стерео PCM (L+R), на плате один физический канал
|
||||
- Буфер: всегда стерео (L == R, оба канала идентичны)
|
||||
- API минимальный: `bsp_mqs_init()`, `bsp_mqs_play(buf, len)`, `bsp_mqs_stop()`, `bsp_mqs_deinit()`
|
||||
- Реализация — на основе наработок (предоставит разработчик)
|
||||
|
||||
**test_mqs:**
|
||||
- Захардкоженная мелодия, 3–5 секунд
|
||||
- `confirm_request("mqs_tone")` → оператор слышит → OK/FAIL
|
||||
- Таймаут: 15 с (аналогично display)
|
||||
- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`
|
||||
|
||||
### 6д — HIL pytest для firmware_test
|
||||
|
||||
**Файлы:**
|
||||
```
|
||||
tools/hil/conftest.py ← новая фикстура firmware_cdc
|
||||
tools/hil/06_test_firmware_opto.py
|
||||
tools/hil/06_test_firmware_can.py
|
||||
```
|
||||
|
||||
**Фикстура `firmware_cdc`:**
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="module")
|
||||
def firmware_cdc(m5):
|
||||
"""
|
||||
Открывает USB CDC порт firmware_test.
|
||||
firmware_test уже прошит в Flash (не загружается pyOCD).
|
||||
Ждёт session_start, возвращает FirmwareCdcClient.
|
||||
"""
|
||||
```
|
||||
|
||||
**`FirmwareCdcClient`** — тонкий клиент:
|
||||
|
||||
- `send_cmd(cmd_dict)` — отправить JSON команду
|
||||
- `wait_event(type, timeout_s)` — ждать события нужного типа
|
||||
- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}`
|
||||
- `run_test(id)` — запустить тест, вернуть test_result dict
|
||||
|
||||
**Сценарий `06_test_firmware_can.py`:**
|
||||
|
||||
```python
|
||||
def test_can_rx(firmware_cdc, m5):
|
||||
# Запустить тест can через firmware_test
|
||||
# При confirm_request("can_rx_ready") — M5 шлёт фрейм, затем confirm
|
||||
...
|
||||
|
||||
def test_can_tx(firmware_cdc, m5):
|
||||
# При confirm_request("can_tx_verify") — M5 принимает фрейм, верифицирует
|
||||
...
|
||||
```
|
||||
|
||||
**Justfile:**
|
||||
|
||||
```bash
|
||||
hil-firmware-opto → pytest 06_test_firmware_opto.py -v
|
||||
hil-firmware-can → pytest 06_test_firmware_can.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -171,10 +275,10 @@ tools/hil/
|
|||
|
||||
### Что нужно
|
||||
|
||||
1. Читать `OCOTP_UNIQUE_ID` (или `OCOTP_MAC0/1`) через SDK fsl_ocotp.
|
||||
2. Посылать `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary`.
|
||||
3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста.
|
||||
4. Записывать статус в Flash (первый сектор после прошивки, вне XIP).
|
||||
1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp`
|
||||
2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary`
|
||||
3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста
|
||||
4. Записывать статус в Flash (первый сектор после прошивки, вне XIP)
|
||||
|
||||
### BSP (предварительно)
|
||||
|
||||
|
|
@ -190,45 +294,173 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из
|
|||
|
||||
---
|
||||
|
||||
## Матрица тестов — итоговая
|
||||
## Этап 8 — TUI сервисного инженера
|
||||
|
||||
| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус |
|
||||
| ---------- | -------------- | ----------- | -------- | -------- | ------------------ | ------ |
|
||||
| — | PING | cmd | — | ❌ | — | ✅ |
|
||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ |
|
||||
| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash` ✅ | ✅ |
|
||||
| `usd` | uSD (SDIO) | interactive | ❌ | ❌ | `bsp_sd` ✅ | ✅ |
|
||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | `bsp_display` ✅ | ✅ |
|
||||
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` ✅ | ✅ |
|
||||
| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ |
|
||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host` ✅ | ⬜ |
|
||||
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
|
||||
| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
|
||||
### Стек технологий
|
||||
|
||||
| Компонент | Выбор | Обоснование |
|
||||
| ------------- | ------------- | ----------------------------------------------------- |
|
||||
| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема |
|
||||
| Serial | pyserial | Уже в стеке (tools/hil) |
|
||||
| Прошивка | spsdk | sdphost + blhost, уже в tools/host |
|
||||
| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом |
|
||||
|
||||
### Структура приложения
|
||||
|
||||
```bash
|
||||
tools/production/
|
||||
├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv
|
||||
├── uv.lock
|
||||
├── main.py ← точка входа
|
||||
├── app/
|
||||
│ ├── tui.py ← Textual App, экраны, layout
|
||||
│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test
|
||||
│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py)
|
||||
│ ├── flasher.py ← USB SDP обёртка над spsdk
|
||||
│ ├── orchestrator.py ← confirm_request → M5 action → confirm response
|
||||
│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses)
|
||||
└── README.md
|
||||
|
||||
tools/shared/
|
||||
└── m5_agent.py ← общая M5-логика для hil/ и production/
|
||||
```
|
||||
|
||||
### Два режима работы
|
||||
|
||||
**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP)
|
||||
|
||||
```bash
|
||||
┌─ Прошивка платы ─────────────────────────────────┐
|
||||
│ Обнаружен BootROM (SDP режим) │
|
||||
│ │
|
||||
│ Что прошить? │
|
||||
│ ◉ firmware_test (диагностика) │
|
||||
│ ○ Production (bootloader + tft_app) │
|
||||
│ │
|
||||
│ Файл: [/path/to/firmware_test_hab.bin ···] │
|
||||
│ │
|
||||
│ [ Прошить ] │
|
||||
│ │
|
||||
│ ████████████░░░░░░ 64% Запись во Flash... │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Режим B — Диагностика** (триггер: session_start получен по CDC)
|
||||
|
||||
```bash
|
||||
┌─ Диагностика платы fw:0.1.0 ─────────────────────┐
|
||||
│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ Выбор тестов: │ Результаты: │
|
||||
│ ☑ SDRAM 32 MB │ sdram ✓ PASS │
|
||||
│ ☑ QSPI Flash │ qspi ✓ PASS │
|
||||
│ ☑ microSD │ usd ✗ FAIL │
|
||||
│ ☑ TFT Display │ mount failed: 5 │
|
||||
│ ☑ Кнопки │ display ✓ PASS │
|
||||
│ ☑ MQS Audio │ buttons ✓ PASS │
|
||||
│ ☑ CAN loopback [HIL] │ ... │
|
||||
│ ☑ Оптовходы [HIL] │ │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ [ Запустить выбранные ] [ Все тесты ] │
|
||||
│ ████████████████░░░░ 80% Тест: display │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ ⚠ Экран залит красным цветом? │
|
||||
│ [ ✓ Да ] [ ✗ Нет ] │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Поведение confirm_request в TUI
|
||||
|
||||
| Тип теста | Источник confirm | Действие TUI |
|
||||
| -------------------- | ------------------ | --------------------------------------------- |
|
||||
| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown |
|
||||
| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result |
|
||||
| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) |
|
||||
|
||||
HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt.
|
||||
|
||||
### Конфигурация (.env)
|
||||
|
||||
```ini
|
||||
# Существующие переменные (tools/hil/.env):
|
||||
HIL_VCOM_PORT=/dev/ttyACM0
|
||||
HIL_M5_PORT=/dev/ttyACM1
|
||||
|
||||
# Новые переменные для production TUI:
|
||||
SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start
|
||||
SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5
|
||||
FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin
|
||||
PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin
|
||||
PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin
|
||||
```
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
just host::service-tui # запустить TUI сервисного инженера
|
||||
just host::service-flash <bin> # прошить без TUI (для автоматизации)
|
||||
```
|
||||
|
||||
### Процесс работы сервисника
|
||||
|
||||
**Диагностика (firmware_test уже в Flash):**
|
||||
|
||||
```bash
|
||||
1. Плата в нормальном режиме (BOOT_MOD_1 → GND)
|
||||
2. Подключить USB к сервисному ПК
|
||||
3. just host::service-tui → TUI обнаружил session_start → Режим B
|
||||
4. Выбрать тесты → Запустить → Смотреть результаты
|
||||
```
|
||||
|
||||
**Перепрошивка (нужна новая версия firmware_test или production):**
|
||||
```
|
||||
1. Перемычка BOOT_MOD_1 → 3V3
|
||||
2. Reset, подключить USB
|
||||
3. TUI обнаружил 1FC9:0130 → Режим A
|
||||
4. Выбрать бинарь → Прошить
|
||||
5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации
|
||||
|
||||
```
|
||||
✅ Этап 1 протокол v2 + runner
|
||||
✅ Этап 2 bsp_sdram + test_sdram
|
||||
✅ Этап 3 bsp_qspi_flash + test_qspi
|
||||
✅ Этап 4 bsp_sd + test_usd
|
||||
✅ Этап 5 display + buttons
|
||||
|
||||
⬜ Этап 6а протокол: list_tests + run_selected ← СЛЕДУЮЩИЙ ШАГ
|
||||
⬜ Этап 6б test_opto.c + hardware верификация
|
||||
⬜ Этап 6в test_can.c + hardware верификация
|
||||
⬜ Этап 6г bsp_mqs + test_mqs.c (после получения наработок)
|
||||
⬜ Этап 6д HIL pytest: firmware_cdc фикстура
|
||||
⬜ Этап 6е HIL pytest: 06_test_firmware_opto.py
|
||||
⬜ Этап 6ж HIL pytest: 06_test_firmware_can.py
|
||||
|
||||
⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг)
|
||||
|
||||
⬜ Этап 8а tools/production/ скелет + models + clients
|
||||
⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты)
|
||||
⬜ Этап 8в Экран прошивки (flasher + SDP автодетект)
|
||||
⬜ Этап 8г Provisioning в TUI
|
||||
⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики)
|
||||
|
||||
⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Зависимости между этапами
|
||||
|
||||
```bash
|
||||
6а (протокол) → 6б (opto) → 6в (can) → 6г (mqs)
|
||||
↓
|
||||
6д (conftest) → 6е (opto pytest) → 6ж (can pytest)
|
||||
↓
|
||||
7 (provisioning)
|
||||
↓
|
||||
8 (TUI)
|
||||
```
|
||||
✅ Этап 1 (протокол v2 + runner)
|
||||
✅ Этап 2 (bsp_sdram + test_sdram)
|
||||
✅ Этап 3 (bsp_qspi_flash + test_qspi)
|
||||
✅ Этап 4 (bsp_sd + test_usd)
|
||||
✅ Этап 5 (display + buttons)
|
||||
⬜ Этап 6 (CAN + UART + Opto, HIL) ← ТЕКУЩИЙ
|
||||
⬜ Этап 7 (provisioning)
|
||||
⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Хостовое ПО производственного прогона (Этап 8)
|
||||
|
||||
**Решение принято (Вариант D):** отдельное приложение `tools/production/`, без pytest, с TUI (Textual).
|
||||
|
||||
### Открытые вопросы — Этап 8
|
||||
|
||||
- [ ] TUI: Textual или Rich или plain print на первой итерации?
|
||||
- [ ] БД: SQLite локально или REST API?
|
||||
- [ ] Несколько стендов параллельно или всегда один?
|
||||
- [ ] Этикетка: нужна ли автоматическая печать после provisioning?
|
||||
|
|
@ -36,7 +36,7 @@ static const char K_FIELD_TYPE[] = "\"type\"";
|
|||
static const char K_FIELD_CMD[] = "\"cmd\"";
|
||||
static const char K_FIELD_ID[] = "\"id\"";
|
||||
static const char K_FIELD_CONFIRMED[] = "\"confirmed\"";
|
||||
|
||||
static const char K_FIELD_TESTS[] = "\"tests\"";
|
||||
/** @brief Буфер непрочитанного остатка chunk после вызова process_line(). */
|
||||
static uint8_t g_s_chunk_buf[CLI_LINE_BUF_SIZE];
|
||||
static size_t g_s_chunk_len = 0U;
|
||||
|
|
@ -172,6 +172,63 @@ static bool parse_confirmed_field(const char *p_line, bool *p_out)
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Извлечь массив строковых значений из JSON-массива ["a","b","c"].
|
||||
*
|
||||
* @param[in] p_array_start Указатель на символ '[' в строке.
|
||||
* @param[out] pp_out Массив указателей на статические буферы.
|
||||
* @param[out] p_out_bufs Двумерный буфер под строки.
|
||||
* @param[in] max_items Максимальное число элементов.
|
||||
* @param[in] item_max_len Максимальная длина каждого элемента (с NUL).
|
||||
* @return Количество извлечённых элементов, или 0 при ошибке.
|
||||
*/
|
||||
static size_t parse_string_array(const char *p_array_start, char (*p_out_bufs)[TEST_ID_MAX_SIZE],
|
||||
size_t max_items)
|
||||
{
|
||||
const char *p = strchr(p_array_start, '[');
|
||||
if (p == NULL)
|
||||
{
|
||||
return 0U;
|
||||
}
|
||||
p++; /* пропустить '[' */
|
||||
|
||||
size_t count = 0U;
|
||||
while (count < max_items)
|
||||
{
|
||||
const char *open_q = strchr(p, '"');
|
||||
if (open_q == NULL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
open_q++;
|
||||
const char *close_q = strchr(open_q, '"');
|
||||
if (close_q == NULL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
size_t len = (size_t) (close_q - open_q);
|
||||
if (len == 0U || len >= TEST_ID_MAX_SIZE)
|
||||
{
|
||||
return 0U; /* невалидный ID */
|
||||
}
|
||||
|
||||
memcpy(p_out_bufs[count], open_q, len);
|
||||
p_out_bufs[count][len] = '\0';
|
||||
count++;
|
||||
|
||||
p = close_q + 1U;
|
||||
/* Проверить конец массива */
|
||||
const char *bracket = strchr(p, ']');
|
||||
const char *next_q = strchr(p, '"');
|
||||
if (bracket != NULL && (next_q == NULL || bracket < next_q))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ── Обработчики входящих сообщений ────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
|
|
@ -191,6 +248,40 @@ static void handle_cmd_run(const char *p_line)
|
|||
test_runner_run_single(test_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработать команду "run_selected": распарсить массив ids и передать в test_runner.
|
||||
*/
|
||||
static void handle_cmd_run_selected(const char *p_line)
|
||||
{
|
||||
/* Максимальный размер реестра — не более 16 тестов */
|
||||
const size_t MAX_SELECTED = 16U;
|
||||
char id_bufs[16][TEST_ID_MAX_SIZE];
|
||||
const char *id_ptrs[16];
|
||||
|
||||
const char *tests_key = strstr(p_line, K_FIELD_TESTS);
|
||||
if (tests_key == NULL)
|
||||
{
|
||||
protocol_send_error("PARSE_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t count =
|
||||
parse_string_array(tests_key + sizeof(K_FIELD_TESTS) - 1U, id_bufs, MAX_SELECTED);
|
||||
|
||||
if (count == 0U)
|
||||
{
|
||||
protocol_send_error("PARSE_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0U; i < count; i++)
|
||||
{
|
||||
id_ptrs[i] = id_bufs[i];
|
||||
}
|
||||
|
||||
test_runner_run_selected(id_ptrs, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработать сообщение {"type":"cmd",...}.
|
||||
*
|
||||
|
|
@ -225,6 +316,18 @@ static void handle_cmd(const char *p_line)
|
|||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "list_tests") == 0)
|
||||
{
|
||||
test_runner_send_list();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "run_selected") == 0)
|
||||
{
|
||||
handle_cmd_run_selected(p_line);
|
||||
return;
|
||||
}
|
||||
|
||||
protocol_send_error("UNKNOWN_CMD");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── Константы ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
|
|
@ -114,6 +113,28 @@ void protocol_send_confirm_request(const confirm_params_t *p_params)
|
|||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_send_test_list(const test_module_t *const *p_pp_registry, size_t count)
|
||||
{
|
||||
/* Заголовок массива */
|
||||
cli_send("{\"type\":\"test_list\",\"tests\":[");
|
||||
|
||||
for (size_t i = 0U; i < count; i++)
|
||||
{
|
||||
const test_module_t *mod = p_pp_registry[i];
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"id\":\"%s\","
|
||||
"\"name\":\"%s\","
|
||||
"\"critical\":%s,"
|
||||
"\"requires_hil\":%s}%s",
|
||||
mod->id, mod->name, mod->critical ? "true" : "false",
|
||||
mod->requires_hil ? "true" : "false", (i + 1U < count) ? "," : "");
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
cli_send("]}\n");
|
||||
}
|
||||
|
||||
void protocol_send_pong(void)
|
||||
{
|
||||
cli_send("{\"type\":\"pong\"}\n");
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
#include "test_module.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** @brief Строка версии прошивки, вставляемая в session_start. */
|
||||
#define FIRMWARE_TEST_VERSION "0.1.4"
|
||||
|
||||
|
|
@ -74,6 +74,16 @@ void protocol_send_confirm_request(const confirm_params_t *p_params);
|
|||
*/
|
||||
void protocol_send_pong(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить список тестов из реестра (ответ на команду list_tests).
|
||||
*
|
||||
* Формат: {"type":"test_list","tests":[{"id":"...","name":"...","critical":...,"requires_hil":...},...]}
|
||||
*
|
||||
* @param[in] pp_registry Указатель на массив указателей на дескрипторы тестов.
|
||||
* @param[in] count Размер реестра.
|
||||
*/
|
||||
void protocol_send_test_list(const test_module_t *const *p_pp_registry, size_t count);
|
||||
|
||||
/**
|
||||
* @brief Отправить событие error.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -30,10 +30,13 @@ extern const test_module_t K_TEST_QSPI;
|
|||
extern const test_module_t K_TEST_USD;
|
||||
extern const test_module_t K_TEST_DISPLAY;
|
||||
extern const test_module_t K_TEST_BUTTONS;
|
||||
extern const test_module_t K_TEST_OPTO;
|
||||
extern const test_module_t K_TEST_CAN;
|
||||
|
||||
static const test_module_t *const k_registry[] = {
|
||||
|
||||
&K_TEST_SDRAM, &K_TEST_QSPI, &K_TEST_USD, &K_TEST_DISPLAY, &K_TEST_BUTTONS,
|
||||
&K_TEST_SDRAM, &K_TEST_QSPI, &K_TEST_USD, &K_TEST_DISPLAY,
|
||||
&K_TEST_BUTTONS, &K_TEST_OPTO, &K_TEST_CAN,
|
||||
};
|
||||
|
||||
#define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0]))
|
||||
|
|
@ -67,6 +70,7 @@ typedef enum runner_mode_e
|
|||
{
|
||||
RUNNER_MODE_SINGLE,
|
||||
RUNNER_MODE_ALL,
|
||||
RUNNER_MODE_SELECTED,
|
||||
} runner_mode_t;
|
||||
|
||||
/* ── Статическое состояние ─────────────────────────────────────────────── */
|
||||
|
|
@ -86,6 +90,9 @@ static uint8_t g_s_failed;
|
|||
static uint8_t g_s_skipped;
|
||||
static bool g_s_critical_failed;
|
||||
|
||||
/* Маска выбранных тестов для RUNNER_MODE_SELECTED */
|
||||
static bool g_s_selected[REGISTRY_SIZE];
|
||||
|
||||
/* ── Forward declaration ───────────────────────────────────────────────── */
|
||||
|
||||
static void start_test_at(size_t idx);
|
||||
|
|
@ -199,6 +206,10 @@ static void skip_from(size_t from_idx)
|
|||
{
|
||||
for (size_t i = from_idx; i < REGISTRY_SIZE; i++)
|
||||
{
|
||||
if (g_s_mode == RUNNER_MODE_SELECTED && !g_s_selected[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const test_module_t *mod = k_registry[i];
|
||||
test_result_t result = make_skip_result("critical test failed");
|
||||
protocol_send_test_begin(mod);
|
||||
|
|
@ -222,7 +233,18 @@ static void advance_after_current(void)
|
|||
return;
|
||||
}
|
||||
|
||||
g_s_current_idx++;
|
||||
/* Найти следующий активный индекс */
|
||||
size_t next_idx = g_s_current_idx + 1U;
|
||||
|
||||
if (g_s_mode == RUNNER_MODE_SELECTED)
|
||||
{
|
||||
while (next_idx < REGISTRY_SIZE && !g_s_selected[next_idx])
|
||||
{
|
||||
next_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
g_s_current_idx = next_idx;
|
||||
|
||||
if (g_s_current_idx >= REGISTRY_SIZE)
|
||||
{
|
||||
|
|
@ -414,3 +436,62 @@ bool test_runner_wait_confirm(const confirm_params_t *p_params)
|
|||
|
||||
return g_s_confirm_value;
|
||||
}
|
||||
|
||||
void test_runner_send_list(void)
|
||||
{
|
||||
protocol_send_test_list(k_registry, REGISTRY_SIZE);
|
||||
}
|
||||
|
||||
void test_runner_run_selected(const char *const *p_pp_ids, size_t count)
|
||||
{
|
||||
if (g_s_state != RUNNER_STATE_IDLE)
|
||||
{
|
||||
protocol_send_error("BUSY");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Проверить все ID до начала выполнения */
|
||||
for (size_t i = 0U; i < count; i++)
|
||||
{
|
||||
if (find_test_by_id(p_pp_ids[i]) >= REGISTRY_SIZE)
|
||||
{
|
||||
protocol_send_error("UNKNOWN_TEST");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Сбросить маску выбора */
|
||||
for (size_t i = 0U; i < REGISTRY_SIZE; i++)
|
||||
{
|
||||
g_s_selected[i] = false;
|
||||
}
|
||||
|
||||
/* Заполнить маску */
|
||||
for (size_t i = 0U; i < count; i++)
|
||||
{
|
||||
size_t idx = find_test_by_id(p_pp_ids[i]);
|
||||
g_s_selected[idx] = true;
|
||||
}
|
||||
|
||||
g_s_passed = 0U;
|
||||
g_s_failed = 0U;
|
||||
g_s_skipped = 0U;
|
||||
g_s_critical_failed = false;
|
||||
g_s_mode = RUNNER_MODE_SELECTED;
|
||||
|
||||
/* Найти первый выбранный тест */
|
||||
size_t first_idx = 0U;
|
||||
while (first_idx < REGISTRY_SIZE && !g_s_selected[first_idx])
|
||||
{
|
||||
first_idx++;
|
||||
}
|
||||
|
||||
if (first_idx >= REGISTRY_SIZE)
|
||||
{
|
||||
/* Пустой список — summary с нулями */
|
||||
protocol_send_summary(0U, 0U, 0U, true);
|
||||
return;
|
||||
}
|
||||
|
||||
start_test_at(first_idx);
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
#include "test_module.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* @brief Инициализировать runner. Сбрасывает состояние в IDLE и обнуляет счётчики.
|
||||
|
|
@ -86,4 +87,25 @@ bool test_runner_is_busy(void);
|
|||
*/
|
||||
bool test_runner_wait_confirm(const confirm_params_t *p_params);
|
||||
|
||||
/**
|
||||
* @brief Запустить выбранное подмножество тестов по списку ID.
|
||||
*
|
||||
* Порядок выполнения — по реестру таргета, не по порядку в pp_ids.
|
||||
* Если хотя бы один ID не найден в реестре — отправляет UNKNOWN_TEST и не запускает ничего.
|
||||
* Если runner занят — отправляет BUSY.
|
||||
* После завершения всех выбранных тестов отправляет summary.
|
||||
*
|
||||
* @param[in] pp_ids Массив C-строк с идентификаторами тестов.
|
||||
* @param[in] count Количество элементов в pp_ids.
|
||||
*/
|
||||
void test_runner_run_selected(const char *const *p_pp_ids, size_t count);
|
||||
|
||||
/**
|
||||
* @brief Отправить список всех зарегистрированных тестов хосту.
|
||||
*
|
||||
* Вызывается из cli.c при получении команды list_tests.
|
||||
* Проксирует реестр в protocol_send_test_list().
|
||||
*/
|
||||
void test_runner_send_list(void);
|
||||
|
||||
#endif /* TEST_RUNNER_H_ */
|
||||
209
firmware/test/src/tests/test_can.c
Normal file
209
firmware/test/src/tests/test_can.c
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* test_can — HIL-тест CAN-интерфейса (FlexCAN2, трансивер SN65HVD230D)
|
||||
*
|
||||
* 2 шага, оба направления независимо:
|
||||
*
|
||||
* Шаг 1 — RX (M5 → таргет):
|
||||
* confirm_request("can_rx_ready")
|
||||
* TUI командует M5: can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF])
|
||||
* TUI отправляет confirm(true)
|
||||
* таргет: bsp_can_receive(&frame, 500 мс)
|
||||
* верификация: frame.id == 0x100, frame.data == [0xDE,0xAD,0xBE,0xEF]
|
||||
*
|
||||
* Шаг 2 — TX (таргет → M5):
|
||||
* таргет: bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс)
|
||||
* confirm_request("can_tx_verify")
|
||||
* TUI: M5.can_recv(timeout=500 мс) → проверяет id+data
|
||||
* TUI: confirm(true) если корректно, confirm(false) если нет
|
||||
*
|
||||
* disableSelfReception=true в bsp_can — таргет не слышит свой TX,
|
||||
* верификацию TX делает только M5.
|
||||
*/
|
||||
|
||||
#include "bsp/can.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "protocol.h"
|
||||
#include "test_module.h"
|
||||
#include "test_runner.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
/* ── Константы ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Битрейт CAN, совпадает с конфигурацией агента M5. */
|
||||
#define CAN_BITRATE 125000U
|
||||
|
||||
/** Таймаут TX в bsp_can_send(), мс. */
|
||||
#define CAN_TX_TIMEOUT_MS 100U
|
||||
|
||||
/**
|
||||
* Таймаут bsp_can_receive() после confirm от M5, мс.
|
||||
* M5 уже отправил фрейм до confirmed:true — 500 мс с большим запасом.
|
||||
*/
|
||||
#define CAN_RX_TIMEOUT_MS 500U
|
||||
|
||||
/** ID фрейма M5 → таргет (шаг 1). */
|
||||
#define CAN_RX_EXPECTED_ID 0x100U
|
||||
|
||||
/** ID фрейма таргет → M5 (шаг 2). */
|
||||
#define CAN_TX_ID 0x200U
|
||||
|
||||
/** Данные фрейма M5 → таргет (шаг 1). */
|
||||
static const uint8_t K_RX_EXPECTED_DATA[] = { 0xDEU, 0xADU, 0xBEU, 0xEFU };
|
||||
|
||||
/** Данные фрейма таргет → M5 (шаг 2). */
|
||||
static const uint8_t K_TX_DATA[] = { 0xCAU, 0xFEU, 0xBAU, 0xBEU };
|
||||
|
||||
#define CAN_FRAME_DLC 4U
|
||||
|
||||
/* ── Реализация тест-модуля ──────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Инициализация: 125 kbit/s, принимать все фреймы.
|
||||
*/
|
||||
static void can_init(void)
|
||||
{
|
||||
const bsp_can_config_t k_cfg = { .bitrate = CAN_BITRATE };
|
||||
(void) bsp_can_init(&k_cfg);
|
||||
(void) bsp_can_accept_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Выполнение теста: шаг 1 (RX) + шаг 2 (TX).
|
||||
*
|
||||
* @return test_result_t
|
||||
*/
|
||||
static test_result_t can_run(void)
|
||||
{
|
||||
test_result_t result = {
|
||||
.status = TEST_STATUS_PASS,
|
||||
.duration_ms = 0U,
|
||||
.detail = "",
|
||||
};
|
||||
|
||||
/* ── Шаг 1: RX (M5 → таргет) ─────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Запросить у TUI: пусть M5 отправит тестовый фрейм.
|
||||
* M5 отправляет фрейм ДО того как подтвердить confirm(true).
|
||||
* После confirm таргет вызывает bsp_can_receive().
|
||||
*/
|
||||
const confirm_params_t k_rx_params = {
|
||||
.id = "can_rx_ready",
|
||||
.prompt = "M5: can_send id=0x100 data=[DE AD BE EF]",
|
||||
.timeout_ms = 0U,
|
||||
};
|
||||
|
||||
bool confirmed = test_runner_wait_confirm(&k_rx_params);
|
||||
|
||||
if (!confirmed)
|
||||
{
|
||||
result.status = TEST_STATUS_SKIP;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "confirm timeout on can_rx_ready");
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Принять фрейм — M5 уже отправил его до confirmed:true */
|
||||
bsp_can_frame_t rx_frame;
|
||||
bsp_status_t rx_status = bsp_can_receive(&rx_frame, CAN_RX_TIMEOUT_MS);
|
||||
|
||||
if (rx_status != BSP_OK)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "can_rx_ready: no frame received");
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Верификация ID */
|
||||
if (rx_frame.id != CAN_RX_EXPECTED_ID)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE,
|
||||
"rx id mismatch: expected 0x%03lX got 0x%03lX",
|
||||
(unsigned long) CAN_RX_EXPECTED_ID, (unsigned long) rx_frame.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Верификация DLC */
|
||||
if (rx_frame.dlc != CAN_FRAME_DLC)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "rx dlc mismatch: expected %u got %u",
|
||||
(unsigned) CAN_FRAME_DLC, (unsigned) rx_frame.dlc);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Верификация данных */
|
||||
if (memcmp(rx_frame.data, K_RX_EXPECTED_DATA, CAN_FRAME_DLC) != 0)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE,
|
||||
"rx data mismatch: got %02X %02X %02X %02X", (unsigned) rx_frame.data[0],
|
||||
(unsigned) rx_frame.data[1], (unsigned) rx_frame.data[2],
|
||||
(unsigned) rx_frame.data[3]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Шаг 2: TX (таргет → M5) ─────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Сначала отправляем фрейм — до confirm_request.
|
||||
* TUI увидит confirm_request, скажет M5 принять фрейм,
|
||||
* проверит id+data и пришлёт confirm(true/false).
|
||||
*/
|
||||
bsp_can_frame_t tx_frame;
|
||||
(void) memset(&tx_frame, 0, sizeof(tx_frame));
|
||||
tx_frame.id = CAN_TX_ID;
|
||||
tx_frame.dlc = CAN_FRAME_DLC;
|
||||
tx_frame.is_extended = false;
|
||||
tx_frame.is_remote = false;
|
||||
(void) memcpy(tx_frame.data, K_TX_DATA, CAN_FRAME_DLC);
|
||||
|
||||
bsp_status_t tx_status = bsp_can_send(&tx_frame, CAN_TX_TIMEOUT_MS);
|
||||
|
||||
if (tx_status != BSP_OK)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "tx failed: bsp_can_send returned %d",
|
||||
(int) tx_status);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Запросить верификацию у TUI: M5 должен был принять наш фрейм.
|
||||
* TUI проверяет id+data и шлёт confirmed(true) или confirmed(false).
|
||||
*/
|
||||
const confirm_params_t k_tx_params = {
|
||||
.id = "can_tx_verify",
|
||||
.prompt = "M5: verify can_recv id=0x200 data=[CA FE BA BE]",
|
||||
.timeout_ms = 0U,
|
||||
};
|
||||
|
||||
confirmed = test_runner_wait_confirm(&k_tx_params);
|
||||
|
||||
if (!confirmed)
|
||||
{
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE,
|
||||
"can_tx_verify: M5 did not confirm tx frame");
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Дескриптор тест-модуля ──────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Дескриптор для регистрации в test_runner. */
|
||||
const test_module_t K_TEST_CAN = {
|
||||
.id = "can",
|
||||
.name = "CAN loopback",
|
||||
.critical = false,
|
||||
.requires_hil = true,
|
||||
.pre_confirm_prompt = NULL,
|
||||
.init = can_init,
|
||||
.run = can_run,
|
||||
.deinit = NULL,
|
||||
};
|
||||
193
firmware/test/src/tests/test_opto.c
Normal file
193
firmware/test/src/tests/test_opto.c
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* test_opto — HIL-тест оптоизолированных входов (PS2801-4)
|
||||
*
|
||||
* Три канала: EXT_IN1 (RLY3), EXT_IN2 (RLY4), RS_RX (RLY2).
|
||||
* Все три — MODE_LEVEL, rs_as_gpio = true.
|
||||
*
|
||||
* 6 шагов: confirm_request → M5 переключает реле → settle → bsp_opto_read().
|
||||
* Верификация синхронная: M5 переключил реле до отправки confirmed:true.
|
||||
*/
|
||||
#include "bsp/opto.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "protocol.h"
|
||||
#include "test_module.h"
|
||||
#include "test_runner.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
/* ── Константы ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Период дебаунса оптовходов, мс. */
|
||||
#define OPTO_DEBOUNCE_MS 10U
|
||||
|
||||
/**
|
||||
* Пауза после confirm перед bsp_opto_read(), мс.
|
||||
* Должна перекрывать OPTO_DEBOUNCE_MS с запасом на jitter реле.
|
||||
* M5 уже переключил реле до отправки confirmed:true.
|
||||
*/
|
||||
#define OPTO_SETTLE_MS 30U
|
||||
|
||||
/* ── Описание одного шага ────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *p_confirm_id; /**< id для confirm_request / confirm. */
|
||||
const char *p_prompt; /**< Инструкция для TUI / M5. */
|
||||
bsp_opto_ch_t channel; /**< Канал для верификации. */
|
||||
bsp_opto_state_t expected_state; /**< Ожидаемое состояние. */
|
||||
} opto_step_t;
|
||||
|
||||
/* ── Таблица шагов (PLAN.md §6б) ─────────────────────────────────────────── */
|
||||
|
||||
static const opto_step_t K_STEPS[] = {
|
||||
{
|
||||
.p_confirm_id = "opto_in1_active",
|
||||
.p_prompt = "M5: RLY3 ON -> IN1 ACTIVE",
|
||||
.channel = BSP_OPTO_CH_IN1,
|
||||
.expected_state = BSP_OPTO_STATE_ACTIVE,
|
||||
},
|
||||
{
|
||||
.p_confirm_id = "opto_in1_inactive",
|
||||
.p_prompt = "M5: RLY3 OFF -> IN1 INACTIVE",
|
||||
.channel = BSP_OPTO_CH_IN1,
|
||||
.expected_state = BSP_OPTO_STATE_INACTIVE,
|
||||
},
|
||||
{
|
||||
.p_confirm_id = "opto_in2_active",
|
||||
.p_prompt = "M5: RLY4 ON -> IN2 ACTIVE",
|
||||
.channel = BSP_OPTO_CH_IN2,
|
||||
.expected_state = BSP_OPTO_STATE_ACTIVE,
|
||||
},
|
||||
{
|
||||
.p_confirm_id = "opto_in2_inactive",
|
||||
.p_prompt = "M5: RLY4 OFF -> IN2 INACTIVE",
|
||||
.channel = BSP_OPTO_CH_IN2,
|
||||
.expected_state = BSP_OPTO_STATE_INACTIVE,
|
||||
},
|
||||
{
|
||||
.p_confirm_id = "opto_rs_active",
|
||||
.p_prompt = "M5: RLY2 ON -> RS ACTIVE",
|
||||
.channel = BSP_OPTO_CH_RS,
|
||||
.expected_state = BSP_OPTO_STATE_ACTIVE,
|
||||
},
|
||||
{
|
||||
.p_confirm_id = "opto_rs_inactive",
|
||||
.p_prompt = "M5: RLY2 OFF -> RS INACTIVE",
|
||||
.channel = BSP_OPTO_CH_RS,
|
||||
.expected_state = BSP_OPTO_STATE_INACTIVE,
|
||||
},
|
||||
};
|
||||
|
||||
#define OPTO_STEP_COUNT ((size_t) (sizeof(K_STEPS) / sizeof(K_STEPS[0])))
|
||||
|
||||
/* ── Реализация тест-модуля ──────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Инициализация: все три канала MODE_LEVEL, rs_as_gpio = true.
|
||||
*
|
||||
* Коллбэки не нужны — состояние читается через bsp_opto_read() синхронно
|
||||
* после confirm.
|
||||
*/
|
||||
static void opto_init(void)
|
||||
{
|
||||
static const bsp_opto_config_t K_CFG = {
|
||||
.callbacks = { NULL, NULL, NULL },
|
||||
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL },
|
||||
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||
.rs_as_gpio = true,
|
||||
.debounce_ms = OPTO_DEBOUNCE_MS,
|
||||
};
|
||||
|
||||
(void) bsp_opto_init(&K_CFG);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Выполнение теста: 6 шагов confirm_request → settle → verify.
|
||||
*
|
||||
* Для каждого шага:
|
||||
* 1. Отправить confirm_request — TUI командует M5 переключить реле.
|
||||
* 2. Ждать confirm через test_runner_wait_confirm().
|
||||
* Возвращает false при таймауте или отказе оператора.
|
||||
* 3. После confirm дать дебаунсу отработать (OPTO_SETTLE_MS),
|
||||
* гоняя bsp_opto_process() + bsp_usb_cdc_poll().
|
||||
* 4. Прочитать bsp_opto_read() и сравнить с expected.
|
||||
*
|
||||
* @return test_result_t с полями status и detail.
|
||||
*/
|
||||
static test_result_t opto_run(void)
|
||||
{
|
||||
test_result_t result = {
|
||||
.status = TEST_STATUS_PASS,
|
||||
.duration_ms = 0U,
|
||||
.detail = "",
|
||||
};
|
||||
|
||||
for (size_t i = 0U; i < OPTO_STEP_COUNT; i++)
|
||||
{
|
||||
const opto_step_t *p_step = &K_STEPS[i];
|
||||
|
||||
/* 1–2. Запросить действие M5 и ждать confirm */
|
||||
const confirm_params_t params = {
|
||||
.id = p_step->p_confirm_id,
|
||||
.prompt = p_step->p_prompt,
|
||||
.timeout_ms = 0U, /* использовать PROTOCOL_CONFIRM_TIMEOUT_MS */
|
||||
};
|
||||
|
||||
bool confirmed = test_runner_wait_confirm(¶ms);
|
||||
|
||||
if (!confirmed)
|
||||
{
|
||||
/*
|
||||
* Таймаут или явный отказ — тест пропускается целиком.
|
||||
* Одна из двух причин: "confirm timeout" или "operator declined".
|
||||
* test_runner_wait_confirm() не разделяет их — используем "confirm timeout"
|
||||
* как универсальный повод для SKIP.
|
||||
*/
|
||||
result.status = TEST_STATUS_SKIP;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "confirm timeout on %s",
|
||||
p_step->p_confirm_id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 3. Settle: дать дебаунсу отработать после переключения реле */
|
||||
uint32_t settle_until = bsp_tick_get_ms() + OPTO_SETTLE_MS;
|
||||
while (bsp_tick_get_ms() < settle_until)
|
||||
{
|
||||
bsp_opto_process();
|
||||
bsp_usb_cdc_poll();
|
||||
}
|
||||
|
||||
/* 4. Верификация */
|
||||
bsp_opto_state_t got = bsp_opto_read(p_step->channel);
|
||||
if (got != p_step->expected_state)
|
||||
{
|
||||
const char *p_exp_str =
|
||||
(p_step->expected_state == BSP_OPTO_STATE_ACTIVE) ? "ACTIVE" : "INACTIVE";
|
||||
const char *p_got_str = (got == BSP_OPTO_STATE_ACTIVE) ? "ACTIVE" : "INACTIVE";
|
||||
|
||||
result.status = TEST_STATUS_FAIL;
|
||||
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s mismatch: expected %s got %s",
|
||||
p_step->p_confirm_id, p_exp_str, p_got_str);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Дескриптор тест-модуля ──────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Дескриптор для регистрации в test_runner. */
|
||||
const test_module_t K_TEST_OPTO = {
|
||||
.id = "opto",
|
||||
.name = "Opto Inputs",
|
||||
.critical = false,
|
||||
.requires_hil = true,
|
||||
.pre_confirm_prompt = NULL,
|
||||
.init = opto_init,
|
||||
.run = opto_run,
|
||||
.deinit = NULL,
|
||||
};
|
||||
|
|
@ -370,6 +370,21 @@ hil-usb-cdc:
|
|||
HIL_BUILD_DIR={{ _hil_build }} \
|
||||
uv run --directory {{ HIL_DIR }} pytest 05_test_usb_cdc.py -v
|
||||
|
||||
[doc('Запустить HIL-тест оптовходов через firmware_test CDC (протокол v2)')]
|
||||
[group('hil')]
|
||||
hil-firmware-opto:
|
||||
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py -v
|
||||
|
||||
[doc('Запустить HIL-тест CAN через firmware_test CDC (протокол v2)')]
|
||||
[group('hil')]
|
||||
hil-firmware-can:
|
||||
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_can.py -v
|
||||
|
||||
[doc('Запустить все firmware_test CDC HIL-тесты (opto + can)')]
|
||||
[group('hil')]
|
||||
hil-firmware:
|
||||
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py 06_test_firmware_can.py -v
|
||||
|
||||
# =============================================================================
|
||||
# ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer)
|
||||
#
|
||||
|
|
@ -476,6 +491,7 @@ m5-deploy:
|
|||
echo " 📤 Копируем агент на M5Stack..."
|
||||
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" \
|
||||
cp "{{ _m5_agent }}" :main.py
|
||||
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" reset
|
||||
echo " ✅ m5_agent.py → /main.py"
|
||||
echo " ✅ M5Stack перезапущен"
|
||||
|
||||
|
|
|
|||
128
tools/hil/06_test_firmware_can.py
Normal file
128
tools/hil/06_test_firmware_can.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""
|
||||
06_test_firmware_can.py — HIL тест test_can через firmware_test CDC протокол v2.
|
||||
|
||||
Проверяет test_can в составе firmware_test:
|
||||
Шаг 1 (RX): при confirm_request "can_rx_ready" M5 отправляет CAN фрейм,
|
||||
затем оркестратор подтверждает confirmed:true.
|
||||
Таргет принимает фрейм и верифицирует id+data.
|
||||
Шаг 2 (TX): таргет отправляет фрейм, затем confirm_request "can_tx_verify".
|
||||
Оркестратор принимает фрейм на M5, проверяет id+data,
|
||||
отправляет confirmed:true/false.
|
||||
|
||||
Запуск:
|
||||
just host::hil-firmware-can
|
||||
uv run --directory tools/hil pytest 06_test_firmware_can.py -v
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from conftest import FirmwareCdc, M5Agent
|
||||
|
||||
# CAN параметры — должны совпадать с test_can.c
|
||||
CAN_RX_ID = 0x100
|
||||
CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||
CAN_TX_ID = 0x200
|
||||
CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE]
|
||||
|
||||
# Таймаут для M5 can_recv — с запасом относительно CAN_RX_TIMEOUT_MS (500 мс) в прошивке
|
||||
CAN_RECV_TIMEOUT_MS = 1000
|
||||
|
||||
@pytest.mark.usb_vcom
|
||||
class TestFirmwareCan:
|
||||
"""Тест CAN-интерфейса через firmware_test CDC (протокол v2)."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, firmware_cdc: FirmwareCdc, m5: M5Agent) -> None:
|
||||
self.cdc = firmware_cdc
|
||||
self.m5 = m5
|
||||
|
||||
def _on_confirm(self, confirm_id: str) -> bool:
|
||||
"""
|
||||
Оркестратор CAN-теста.
|
||||
|
||||
can_rx_ready:
|
||||
M5 отправляет фрейм на шину ДО confirmed:true.
|
||||
Таргет вызовет bsp_can_receive() после confirm.
|
||||
|
||||
can_tx_verify:
|
||||
Таргет уже отправил фрейм ДО confirm_request.
|
||||
M5 принимает фрейм, проверяет id+data, возвращает результат.
|
||||
"""
|
||||
import logging
|
||||
logging.getLogger(__name__).info("on_confirm called: %r", confirm_id)
|
||||
|
||||
if confirm_id == "can_rx_ready":
|
||||
self.m5.can_send(CAN_RX_ID, CAN_RX_DATA)
|
||||
# Небольшая пауза чтобы фрейм успел уйти на шину
|
||||
time.sleep(0.05)
|
||||
return True
|
||||
|
||||
if confirm_id == "can_tx_verify":
|
||||
try:
|
||||
frame = self.m5.can_recv(timeout_ms=CAN_RECV_TIMEOUT_MS)
|
||||
import logging
|
||||
logging.getLogger(__name__).info(
|
||||
"M5 received frame: id=0x%X data=%s",
|
||||
frame.get("id", -1), frame.get("data", [])
|
||||
)
|
||||
except (TimeoutError, RuntimeError) as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("M5 can_recv failed: %s", e)
|
||||
return False
|
||||
|
||||
# Верифицировать id и data
|
||||
if frame.get("id") != CAN_TX_ID:
|
||||
return False
|
||||
if frame.get("data") != CAN_TX_DATA:
|
||||
return False
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def test_ping(self) -> None:
|
||||
"""Базовая проверка CDC-канала."""
|
||||
self.cdc.ping()
|
||||
|
||||
def test_can_pass(self) -> None:
|
||||
"""
|
||||
Полный прогон test_can: RX + TX через M5.
|
||||
Ожидаем status=pass.
|
||||
"""
|
||||
result = self.cdc.run_hil_test(
|
||||
test_id="can",
|
||||
on_confirm=self._on_confirm,
|
||||
timeout_s=60.0,
|
||||
)
|
||||
|
||||
assert result.get("status") == "pass", (
|
||||
f"test_can вернул {result.get('status')!r}: "
|
||||
f"{result.get('detail', '')}"
|
||||
)
|
||||
|
||||
def test_can_rx_fail_no_frame(self) -> None:
|
||||
"""
|
||||
Негативный: при can_rx_ready не отправляем фрейм, но подтверждаем.
|
||||
Таргет вызовет bsp_can_receive() и получит timeout → status=fail.
|
||||
"""
|
||||
|
||||
def bad_confirm(confirm_id: str) -> bool:
|
||||
# Подтверждаем без отправки фрейма
|
||||
if confirm_id == "can_rx_ready":
|
||||
return True
|
||||
# can_tx_verify не достигается — тест упадёт раньше
|
||||
return False
|
||||
|
||||
result = self.cdc.run_hil_test(
|
||||
test_id="can",
|
||||
on_confirm=bad_confirm,
|
||||
timeout_s=30.0,
|
||||
)
|
||||
|
||||
assert result.get("status") == "fail", (
|
||||
f"Ожидали fail, получили {result.get('status')!r}"
|
||||
)
|
||||
detail = result.get("detail", "")
|
||||
assert "no frame" in detail, (
|
||||
f"detail должен содержать 'no frame': {detail!r}"
|
||||
)
|
||||
108
tools/hil/06_test_firmware_opto.py
Normal file
108
tools/hil/06_test_firmware_opto.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""
|
||||
06_test_firmware_opto.py — HIL тест test_opto через firmware_test CDC протокол v2.
|
||||
|
||||
Проверяет test_opto в составе firmware_test:
|
||||
- 6 шагов: IN1 active/inactive, IN2 active/inactive, RS active/inactive
|
||||
- Оркестратор: при confirm_request с известным id командует M5 переключить реле,
|
||||
затем отправляет confirmed:true
|
||||
|
||||
Маппинг confirm_id → действие M5:
|
||||
opto_in1_active → opto_set(1, True) (RLY3 ON)
|
||||
opto_in1_inactive → opto_set(1, False) (RLY3 OFF)
|
||||
opto_in2_active → opto_set(2, True) (RLY4 ON)
|
||||
opto_in2_inactive → opto_set(2, False) (RLY4 OFF)
|
||||
opto_rs_active → opto_set(3, True) (RLY2 ON)
|
||||
opto_rs_inactive → opto_set(3, False) (RLY2 OFF)
|
||||
|
||||
Запуск:
|
||||
just host::hil-firmware-opto
|
||||
uv run --directory tools/hil pytest 06_test_firmware_opto.py -v
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from conftest import FirmwareCdc, M5Agent
|
||||
|
||||
|
||||
RELAY_ON_S = 0.15 # реле замыкается быстро
|
||||
RELAY_OFF_S = 0.5 # размыкание + дебаунс прошивки с запасом
|
||||
|
||||
|
||||
# Маппинг confirm_id → (opto_ch, state)
|
||||
_OPTO_ACTIONS: dict[str, tuple[int, bool]] = {
|
||||
"opto_in1_active": (1, True),
|
||||
"opto_in1_inactive": (1, False),
|
||||
"opto_in2_active": (2, True),
|
||||
"opto_in2_inactive": (2, False),
|
||||
"opto_rs_active": (3, True),
|
||||
"opto_rs_inactive": (3, False),
|
||||
}
|
||||
|
||||
@pytest.mark.usb_vcom
|
||||
class TestFirmwareOpto:
|
||||
"""Тест оптовходов через firmware_test CDC (протокол v2)."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, firmware_cdc: FirmwareCdc, m5: M5Agent) -> None:
|
||||
self.cdc = firmware_cdc
|
||||
self.m5 = m5
|
||||
# Сбросить все реле перед тестом
|
||||
self.m5.opto_all_off()
|
||||
time.sleep(RELAY_ON_S)
|
||||
|
||||
def _on_confirm(self, confirm_id: str) -> bool:
|
||||
action = _OPTO_ACTIONS.get(confirm_id)
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
ch, state = action
|
||||
self.m5.opto_set(ch, state)
|
||||
time.sleep(RELAY_ON_S if state else RELAY_OFF_S)
|
||||
return True
|
||||
|
||||
def test_ping(self) -> None:
|
||||
"""Базовая проверка CDC-канала."""
|
||||
self.cdc.ping()
|
||||
|
||||
def test_opto_pass(self) -> None:
|
||||
"""
|
||||
Запустить test_opto через firmware_test.
|
||||
Оркестратор автоматически управляет M5 при каждом confirm_request.
|
||||
Ожидаем status=pass.
|
||||
"""
|
||||
result = self.cdc.run_hil_test(
|
||||
test_id="opto",
|
||||
on_confirm=self._on_confirm,
|
||||
timeout_s=60.0,
|
||||
)
|
||||
|
||||
assert result.get("status") == "pass", (
|
||||
f"test_opto вернул {result.get('status')!r}: "
|
||||
f"{result.get('detail', '')}"
|
||||
)
|
||||
|
||||
def test_opto_in1_fail_on_inactive(self) -> None:
|
||||
"""
|
||||
Негативный тест: при opto_in1_active НЕ включаем реле.
|
||||
Ожидаем status=fail с detail содержащим 'in1' и 'mismatch'.
|
||||
"""
|
||||
|
||||
def bad_confirm(confirm_id: str) -> bool:
|
||||
# Не переключаем реле — канал остаётся INACTIVE
|
||||
# Но подтверждаем чтобы тест продолжился
|
||||
return True
|
||||
|
||||
result = self.cdc.run_hil_test(
|
||||
test_id="opto",
|
||||
on_confirm=bad_confirm,
|
||||
timeout_s=60.0,
|
||||
)
|
||||
|
||||
assert result.get("status") == "fail", (
|
||||
f"Ожидали fail, получили {result.get('status')!r}"
|
||||
)
|
||||
detail = result.get("detail", "")
|
||||
assert "mismatch" in detail, (
|
||||
f"detail должен содержать 'mismatch': {detail!r}"
|
||||
)
|
||||
|
|
@ -348,3 +348,184 @@ def uart_cmd(ser: serial.Serial, cmd: str) -> str:
|
|||
if not resp:
|
||||
raise TimeoutError(f"Нет ответа на команду '{cmd}'")
|
||||
return resp.decode("ascii", errors="replace").strip()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# firmware_test CDC — фикстура для тестирования firmware_test через протокол v2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FirmwareCdc:
|
||||
"""
|
||||
Драйвер firmware_test протокола v2 (JSON-lines через USB CDC).
|
||||
|
||||
Открывает CDC-порт, ждёт session_start, предоставляет методы:
|
||||
- run_test(test_id) → запустить тест, дождаться test_result
|
||||
- send_confirm(confirm_id, confirmed) → ответить на confirm_request
|
||||
- wait_event(type, timeout_s) → ждать событие нужного типа
|
||||
"""
|
||||
|
||||
def __init__(self, ser: serial.Serial) -> None:
|
||||
self._ser = ser
|
||||
|
||||
def _readline(self, timeout_s: float = 5.0) -> dict:
|
||||
"""Читать строки до получения валидного JSON. TimeoutError если истёк таймаут."""
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
raw = self._ser.readline()
|
||||
if not raw:
|
||||
continue
|
||||
text = raw.decode("ascii", errors="replace").strip()
|
||||
if not text:
|
||||
continue
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
log.debug("firmware_cdc: не JSON: %r", text)
|
||||
continue
|
||||
raise TimeoutError(f"firmware_cdc: нет ответа за {timeout_s} с")
|
||||
|
||||
def wait_event(self, event_type: str, timeout_s: float = 5.0) -> dict:
|
||||
"""
|
||||
Ждать событие с заданным полем type.
|
||||
Пропускает промежуточные события (progress и т.п.).
|
||||
"""
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
remaining = deadline - time.monotonic()
|
||||
msg = self._readline(timeout_s=max(remaining, 0.1))
|
||||
if msg.get("type") == event_type:
|
||||
return msg
|
||||
log.debug("firmware_cdc: пропускаем %r (ждём %r)", msg.get("type"), event_type)
|
||||
raise TimeoutError(
|
||||
f"firmware_cdc: событие {event_type!r} не получено за {timeout_s} с"
|
||||
)
|
||||
|
||||
def send(self, payload: dict) -> None:
|
||||
"""Отправить JSON-команду на таргет."""
|
||||
line = json.dumps(payload, separators=(",", ":")) + "\n"
|
||||
self._ser.write(line.encode("ascii"))
|
||||
self._ser.flush()
|
||||
|
||||
def ping(self) -> None:
|
||||
"""Проверка связи: ping → pong."""
|
||||
self.send({"type": "cmd", "cmd": "ping"})
|
||||
self.wait_event("pong")
|
||||
|
||||
def send_confirm(self, confirm_id: str, confirmed: bool) -> None:
|
||||
"""Ответить на confirm_request."""
|
||||
self.send({"type": "confirm", "id": confirm_id, "confirmed": confirmed})
|
||||
|
||||
def run_test(self, test_id: str, timeout_s: float = 60.0) -> dict:
|
||||
"""
|
||||
Запустить тест по id, вернуть dict test_result.
|
||||
|
||||
Промежуточные confirm_request игнорируются — их нужно обрабатывать
|
||||
отдельно через wait_event("confirm_request") до вызова run_test,
|
||||
или через on_confirm callback в специализированных тестах.
|
||||
|
||||
Для HIL-тестов (opto, can) используй run_hil_test().
|
||||
"""
|
||||
self.send({"type": "cmd", "cmd": "run", "id": test_id})
|
||||
self.wait_event("test_begin", timeout_s=5.0)
|
||||
return self.wait_event("test_result", timeout_s=timeout_s)
|
||||
|
||||
def run_hil_test(
|
||||
self,
|
||||
test_id: str,
|
||||
on_confirm,
|
||||
timeout_s: float = 30.0,
|
||||
) -> dict:
|
||||
"""
|
||||
Запустить HIL-тест с обработкой confirm_request.
|
||||
|
||||
on_confirm(confirm_id: str) → bool вызывается для каждого confirm_request.
|
||||
Возвращает True → отправляет confirmed:true, False → confirmed:false.
|
||||
|
||||
Цикл завершается при получении test_result.
|
||||
|
||||
Args:
|
||||
test_id: идентификатор теста в реестре таргета
|
||||
on_confirm: callable(confirm_id: str) → bool
|
||||
timeout_s: общий таймаут на весь тест
|
||||
"""
|
||||
self.send({"type": "cmd", "cmd": "run", "id": test_id})
|
||||
self.wait_event("test_begin", timeout_s=5.0)
|
||||
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
remaining = deadline - time.monotonic()
|
||||
msg = self._readline(timeout_s=max(remaining, 0.1))
|
||||
msg_type = msg.get("type")
|
||||
|
||||
if msg_type == "test_result":
|
||||
return msg
|
||||
|
||||
if msg_type == "confirm_request":
|
||||
confirm_id = msg.get("id", "")
|
||||
log.info("firmware_cdc: confirm_request %r", confirm_id)
|
||||
confirmed = on_confirm(confirm_id)
|
||||
self.send_confirm(confirm_id, confirmed)
|
||||
continue
|
||||
|
||||
log.debug("firmware_cdc: промежуточное событие %r", msg_type)
|
||||
|
||||
raise TimeoutError(
|
||||
f"firmware_cdc: test_result для {test_id!r} не получен за {timeout_s} с"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def firmware_cdc(m5: M5Agent) -> Generator[FirmwareCdc, None, None]:
|
||||
"""
|
||||
Открыть USB CDC порт firmware_test, дождаться session_start.
|
||||
|
||||
Зависит от m5 — питание таргета уже включено.
|
||||
firmware_test должна быть прошита в Flash (via SDP) и запущена.
|
||||
|
||||
Порт берётся из TARGET_VCOM_PORT (HIL_USB_CDC_PORT в .env).
|
||||
"""
|
||||
if not cfg.TARGET_VCOM_PORT:
|
||||
pytest.skip("HIL_USB_CDC_PORT не задан — пропускаем firmware_cdc тесты")
|
||||
|
||||
# Ждём появления CDC порта после включения питания
|
||||
deadline = time.monotonic() + cfg.TARGET_VCOM_TIMEOUT
|
||||
ser = None
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
ser = serial.Serial(
|
||||
port=cfg.TARGET_VCOM_PORT,
|
||||
baudrate=cfg.TARGET_VCOM_BAUD,
|
||||
timeout=1.0,
|
||||
write_timeout=1.0,
|
||||
)
|
||||
break
|
||||
except serial.SerialException:
|
||||
time.sleep(0.2)
|
||||
|
||||
if ser is None:
|
||||
pytest.fail(
|
||||
f"CDC порт {cfg.TARGET_VCOM_PORT} недоступен "
|
||||
f"за {cfg.TARGET_VCOM_TIMEOUT} с"
|
||||
)
|
||||
|
||||
try:
|
||||
ser.dtr = True
|
||||
time.sleep(0.3)
|
||||
ser.reset_input_buffer()
|
||||
|
||||
cdc = FirmwareCdc(ser)
|
||||
|
||||
# session_start — одноразовое событие при старте, может быть уже пропущено.
|
||||
# Проверяем живость через ping → pong.
|
||||
try:
|
||||
cdc.ping()
|
||||
log.info("firmware_test: ping OK, прошивка активна")
|
||||
except (TimeoutError, Exception) as exc:
|
||||
pytest.fail(
|
||||
f"firmware_test не отвечает на ping — "
|
||||
f"прошивка запущена? Порт верный? ({exc})"
|
||||
)
|
||||
|
||||
yield cdc
|
||||
finally:
|
||||
ser.close()
|
||||
Loading…
Reference in a new issue