# 1. Набросок архитектуры firmware_test

This commit is contained in:
Dmitry Akimov 2026-03-13 19:12:35 +03:00
parent 310a8d6bc9
commit 806b41e0a5
4 changed files with 513 additions and 3 deletions

View file

@ -167,5 +167,3 @@ just flash # прошивка через USB ROM
> Подробнее о прошивке — [HOW_TO_FLASH.md](HOW_TO_FLASH.md)
> Подробнее об окружении разработки — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)
>
>

View file

@ -34,7 +34,7 @@ CI использует те же команды что и локальная р
│ ├── docker ← управление devcontainer
│ ├── git ← работа с репозиторием
│ ├── uv + spsdk ← прошивка платы (flash_usb.py, sdphost, blhost)
│ │ venv: tools/host/.venv-host (Linux/macOS)
│ │ venv: tools/host/.venv. (Linux/macOS)
│ │ tools/host/.venv-host-win (Windows)
│ ├── JLinkGDBServer / probe-rs ← сервер отладки (USB → TCP :2331)
│ └── VSCode ← IDE (Dev Containers extension)

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

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

268
firmware/test/arch.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 15 KiB