# firmware_test: TUI app docs
This commit is contained in:
parent
0132d8056d
commit
2a6256e745
12 changed files with 755 additions and 604 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(
|
project(
|
||||||
firmware_test
|
firmware_test
|
||||||
VERSION 0.0.1
|
VERSION 0.1.1
|
||||||
LANGUAGES C ASM)
|
LANGUAGES C ASM)
|
||||||
|
|
||||||
set(TARGET_NAME firmware_test)
|
set(TARGET_NAME firmware_test)
|
||||||
|
|
|
||||||
|
|
@ -3,412 +3,124 @@
|
||||||
TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе.
|
TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе.
|
||||||
Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows.
|
Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows.
|
||||||
|
|
||||||
|
> Внутреннее устройство, диаграммы архитектуры и заметки для разработчиков —
|
||||||
|
> в [DEV_ARCH.md](DEV_ARCH.md). Этот документ — только про то, как приложением
|
||||||
|
> пользоваться.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Структура проекта
|
## Экраны приложения
|
||||||
|
|
||||||
|
### Ожидание подключения (WaitingScreen)
|
||||||
|
|
||||||
|
Стартовый экран. Автодетект USB — как только плата подключена, TUI сам
|
||||||
|
определяет режим (прошивка или диагностика) и переключается.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/production/
|
┌────────────────────────────────────────────────────┐
|
||||||
├── main.py ← точка входа (10 строк)
|
│ service_tool v0.3.0 │
|
||||||
├── pyproject.toml ← зависимости uv
|
│ │
|
||||||
├── uv.lock
|
│ [LOGO_ART] │
|
||||||
└── app/
|
│ │
|
||||||
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
│ Подключите плату индикатора к USB... ⠋ │
|
||||||
├── app.tcss ← единый файл стилей для всех экранов
|
└────────────────────────────────────────────────────┘
|
||||||
├── models.py ← все типы данных (dataclass/Enum)
|
|
||||||
├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8)
|
|
||||||
├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8)
|
|
||||||
├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py
|
|
||||||
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
|
|
||||||
├── widgets/
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
|
||||||
└── screens/
|
|
||||||
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
|
||||||
├── waiting.py ← WaitingScreen — ожидание USB, баннер причины возврата
|
|
||||||
├── flash.py ← FlashScreen — прошивка / chip erase
|
|
||||||
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
|
||||||
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
|
||||||
└── diag/
|
|
||||||
├── __init__.py ← DiagScreen — координатор диагностики
|
|
||||||
├── test_list.py ← TestListPanel — чекбоксы тестов, Выбрать/Снять все
|
|
||||||
├── results.py ← ResultsPanel — DataTable результатов
|
|
||||||
└── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
При потере соединения на любом другом экране сессия разрывается полностью —
|
||||||
|
TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над
|
||||||
|
подсказкой на 4 секунды появляется строка `⚠ <причина>` (например,
|
||||||
|
«⚠ Соединение с платой потеряно»), затем скрывается сама — обычный
|
||||||
|
автодетект продолжается без вмешательства оператора.
|
||||||
|
|
||||||
## Концепция
|
### Прошивка платы (FlashScreen)
|
||||||
|
|
||||||
```mermaid
|
Триггер: обнаружена плата в режиме BootROM SDP.
|
||||||
graph LR
|
|
||||||
subgraph PC["Сервисный ПК"]
|
|
||||||
TUI["service-tui\n(Textual App)"]
|
|
||||||
subgraph app["app/"]
|
|
||||||
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
|
||||||
M5["m5_client.py\nSerial JSON-lines, UTF-8"]
|
|
||||||
FL["flasher.py\nsubprocess + pyusb detect"]
|
|
||||||
OR["orchestrator.py\nconfirm/progress/timeout router"]
|
|
||||||
end
|
|
||||||
TUI --> FC & M5 & FL & OR
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Board["Плата TFT (MIMXRT1052)"]
|
|
||||||
FW["firmware_test\n(USB CDC)"]
|
|
||||||
ROM["BootROM SDP\n(1FC9:0130)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph HIL["HIL стенд (опционально)"]
|
|
||||||
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Host["tools/host/"]
|
|
||||||
FU["flash_usb.py\nsdphost + blhost"]
|
|
||||||
end
|
|
||||||
|
|
||||||
FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
|
|
||||||
FL -->|"subprocess uv run"| FU
|
|
||||||
FU -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM
|
|
||||||
M5 <-->|"JSON-lines\nSerial"| M5HW
|
|
||||||
M5HW -->|"RLY1–4"| Board
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Состояния приложения
|
|
||||||
|
|
||||||
Состояние определяется автодетектом USB и меняется динамически без перезапуска TUI. При потере соединения сессия разрывается полностью — TUI не пытается восстановить прежнее состояние, а стартует заново с `WaitingScreen`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
stateDiagram-v2
|
|
||||||
[*] --> WAITING : запуск TUI
|
|
||||||
|
|
||||||
WAITING --> FLASHING : VID:PID 1FC9:0130\n(BootROM SDP)
|
|
||||||
WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC
|
|
||||||
|
|
||||||
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
|
||||||
FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое
|
|
||||||
|
|
||||||
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
|
||||||
|
|
||||||
DIAGNOSING --> WAITING : DiagDone / ESC /\nпотеря USB в простое
|
|
||||||
DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка BOOT_MOD)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Режим A — Прошивка (FlashScreen)
|
|
||||||
|
|
||||||
Триггер: BootROM SDP `1FC9:0130` обнаружен через `pyusb`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
┌─ Прошивка платы ─────────────────────────────────┐
|
┌────────────────────────────────────────────────────┐
|
||||||
│ ⚡ BootROM SDP обнаружен │
|
│ ⚡ Загрузка прошивки на плату индикатора │
|
||||||
|
│ (режим BootROM) │
|
||||||
│ │
|
│ │
|
||||||
│ Что прошить? │
|
│ Выбор загружаемой прошивки │
|
||||||
│ ◉ firmware_test (диагностическая прошивка) │
|
│ ◉ Диагностическая прошивка (firmware_test) │
|
||||||
│ ○ Production (bootloader + tft_app) │
|
│ ○ Серийная прошивка (bootloader + tft_app) │
|
||||||
│ ○ Кастомный бинарь... │
|
│ ○ Другое │
|
||||||
|
│ [ Имя бинарного файла в custom_binaries/ (.bin) ]│ ← только если «Другое»
|
||||||
│ │
|
│ │
|
||||||
│ [ ▶ Прошить ] [ ⚠ Chip Erase ] [ ✕ Выйти ] │
|
│ [ ▶ Загрузить ] [ ⚠ Очистить память ] [ ✕ Выйти из приложения ] │
|
||||||
│ │
|
│ │
|
||||||
│ ████████████░░░░░░ ← без числового % │
|
│ ████████████░░░░░░ ← без числового % │
|
||||||
│ ┌────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────┐ │
|
||||||
│ │ ▶ Прошивка: firmware_test │ │
|
│ │ ▶ Прошивка: firmware_test │ │
|
||||||
│ │ $ blhost -u 0x15A2,0x0073 -- write-memory… │ │
|
│ │ ... │ │
|
||||||
│ └────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────┘ │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
ProgressBar виден только во время активной операции (скрыт в простое), без числового `%` — только полоса и построчный лог в реальном времени.
|
Лог виден постоянно (не только во время прошивки), прогресс-бар — только
|
||||||
|
во время активной операции (скрыт в простое), без числового `%` — только
|
||||||
|
полоса и построчный лог в реальном времени.
|
||||||
|
|
||||||
### Промежуточный экран — PostFlashScreen
|
### Переход в рабочий режим (PostFlashScreen)
|
||||||
|
|
||||||
Показывается **только** после успешной прошивки `firmware_test` (не для Production/Custom — им этот шаг не нужен).
|
Показывается **только** после успешной прошивки `firmware_test` (для
|
||||||
|
Production/Custom этот шаг не нужен).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
┌─ Прошивка завершена ──────────────────────────────┐
|
┌────────────────────────────────────────────────────┐
|
||||||
│ ✅ firmware_test успешно записан │
|
│ ✅ firmware_test успешно записан │
|
||||||
│ │
|
│ │
|
||||||
│ Переведите плату в нормальный режим: │
|
│ Переведите плату в нормальный режим: │
|
||||||
│ BOOT_MOD_1 → GND → Reset │
|
│ BOOT_MOD_1 → GND → Reset │
|
||||||
│ │
|
│ │
|
||||||
│ [ ✓ Готово, перешёл ] [ ✕ Выйти ] │
|
│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │
|
||||||
│ Автопереход через: 40с │
|
│ Автопереход через: 40с │
|
||||||
└────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Режим B — Диагностика (DiagScreen)
|
### Диагностика (DiagScreen)
|
||||||
|
|
||||||
Триггер: CDC-порт `1996:00AD` виден + `ping→pong` прошёл.
|
Триггер: плата видна как CDC-устройство и отвечает на связь.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
┌─ Диагностика fw:0.2.0 UID:A1B2C3D4E5F60011 M5: ✓ подключён ─┐
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
│ Тесты │ Результаты (DataTable) │
|
│ fw: 0.2.0 MCU ID: A1B2C3D4E5F60011 M5 Bench: ✓ подключён │
|
||||||
│ [Выбрать все][Снять все] │ Тест HIL Статус Время │
|
├───────────────────────────┬──────────────────────────────────────┤
|
||||||
│ ☐ SDRAM 32 MB │ microSD ✗ FAIL 0.1с │
|
│ Доступные тесты │ Тест M5 Bench Статус Время │
|
||||||
│ ☐ QSPI Flash │ no card detected (без обрезки) │
|
│ [Выбрать все][Снять все] │ microSD [-] ✗ FAIL 0.1с │
|
||||||
│ ☐ microSD │ SDRAM 32 MB ✓ PASS 1.8с │
|
│ ☐ SDRAM 32 MB │ no card detected (без обрезки) │
|
||||||
│ ☐ TFT Display │ QSPI Flash ✓ PASS 0.6с │
|
│ ☐ QSPI Flash │ SDRAM 32 MB [-] ✓ PASS 1.8с │
|
||||||
│ ☐ CAN loopback [HIL] │ TFT Display … running │
|
│ ☐ microSD │ QSPI Flash [-] ✓ PASS 0.6с │
|
||||||
│ ☐ Оптовходы [HIL] │ Кнопки pending │
|
│ ☐ TFT Display │ TFT Display [-] … │
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
│ ☐ CAN loopback [HIL] │ Кнопки [-] │
|
||||||
│ [▶ Запустить выбранные] [▶▶ Все тесты] [✕ Выйти] │
|
│ ☐ Оптовходы [HIL] │ │
|
||||||
│ ████████░░░░ Тест: usd — mount: ok │
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
│ ████████░░░░ Тест: usd — mount: ok ← только во время прогона │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [▶ Запустить выбранные тесты] [▶▶ Запустить все тесты] [✕ Выйти из приложения] │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
│ ⚠ Экран залит красным цветом? 28с │
|
│ ⚠ Экран залит красным цветом? 28с │
|
||||||
│ [ ✓ Да ] [ ✗ Нет ] │
|
│ [ ✓ Да ] [ ✗ Нет ] ← только во время confirm │
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключевые отличия от ранних версий TUI:
|
Что важно знать:
|
||||||
- **Тесты изначально не выбраны** — сервисник выбирает явно, либо кнопками "Выбрать все"/"Снять все"
|
- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать
|
||||||
- **Результаты — `DataTable`**, не текстовые строки: сортировка FAIL-наверх (стабильная внутри группы по порядку реестра), FAIL-строка подсвечена красным фоном целиком, длинные `detail`-сообщения переносятся на несколько строк без обрезания (явная ширина колонок, см. раздел «Известные грабли Textual»)
|
все"/"Снять все".
|
||||||
- **`progress`-события** теста USD (`card_detect`, `mount`, `write`, `read_compare`) отображаются в прогресс-строке как текущая фаза
|
- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена
|
||||||
- HIL-тесты без M5StampPLC — серые, недоступны для выбора (постоянное состояние, не путается с временной блокировкой во время прогона)
|
целиком, длинные сообщения переносятся на несколько строк без обрезания.
|
||||||
|
Колонка `M5 Bench` отмечает `[*]`/`[-]` — требует ли тест HIL-стенд.
|
||||||
---
|
- **HIL-тесты** (CAN, оптовходы) без подключённого M5StampPLC — серые,
|
||||||
|
недоступны для выбора.
|
||||||
## Обработка confirm_request
|
- **Интерактивные проверки во время прогона теста** бывают трёх видов:
|
||||||
|
- HIL-тесты (CAN, оптовходы) при наличии M5 — отвечает стенд автоматически,
|
||||||
Маршрутизация реализована в `Orchestrator._handle_confirm()` по значению `confirm_request.id`. Помимо confirm, протокол v2 определяет `progress` — внутришаговые информационные события долгих тестов (сейчас только `usd`), не требующие ответа.
|
без участия оператора;
|
||||||
|
- тесты кнопок — экран показывает подсказку `⌨ <инструкция>`, ответ не
|
||||||
```mermaid
|
требуется, нужно физически нажать кнопку на плате;
|
||||||
flowchart TD
|
- остальное (дисплей, аудио) — форма "Да/Нет" с таймером `⚠ <вопрос>` на
|
||||||
EV["Событие от firmware_test"]
|
экране.
|
||||||
EV --> T{type}
|
- До завершения загрузки списка тестов правая колонка показывает подсказку
|
||||||
|
«Выберите тесты и нажмите «Запустить выбранные»» вместо таблицы.
|
||||||
T -->|"confirm_request"| R{confirm_request.id}
|
|
||||||
T -->|"progress"| PROG["TEST_PROGRESS\nотобразить фазу в прогресс-строке"]
|
|
||||||
T -->|"test_begin / test_result / summary"| STD["стандартная обработка"]
|
|
||||||
T -->|"_timeout (синтетическое,\nот FirmwareClient)"| TO["синтезировать FAIL\nдля зависшего теста\n+ гарантированный SUMMARY"]
|
|
||||||
T -->|"неизвестный тип"| LOG["logger.debug — НЕ ошибка,\nне показывается оператору"]
|
|
||||||
|
|
||||||
R -->|"opto_*"| HIL_OPTO["M5: relay_set → settle → send_confirm"]
|
|
||||||
R -->|"can_rx_ready"| HIL_CAN_RX["M5: can_send → send_confirm"]
|
|
||||||
R -->|"can_tx_verify"| HIL_CAN_TX["M5: can_recv → verify → send_confirm"]
|
|
||||||
R -->|"btn*"| BTN["show_buttons_hint, БЕЗ JSON-confirm"]
|
|
||||||
R -->|"остальное"| OP["show_operator + countdown\nждать resolve_operator_confirm()"]
|
|
||||||
```
|
|
||||||
|
|
||||||
| `confirm_request.id` | Кто отвечает | Реле M5 |
|
|
||||||
| ------------------------------- | ---------------------------- | ------- |
|
|
||||||
| `opto_in1_active` / `_inactive` | M5 авто | RLY3 |
|
|
||||||
| `opto_in2_active` / `_inactive` | M5 авто | RLY4 |
|
|
||||||
| `opto_rs_active` / `_inactive` | M5 авто | RLY2 |
|
|
||||||
| `can_rx_ready` | M5 авто (CAN TX) | — |
|
|
||||||
| `can_tx_verify` | M5 авто (CAN RX) | — |
|
|
||||||
| `btn*` | физика, без JSON-ответа | — |
|
|
||||||
| всё остальное | оператор, prompt + countdown | — |
|
|
||||||
|
|
||||||
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно одним событием `SUMMARY` — настоящим от firmware или синтетическим (`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда (исторический баг, см. CHANGELOG).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Мониторинг соединения и разрыв сессии
|
|
||||||
|
|
||||||
`ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к `FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
|
||||||
|
|
||||||
- **На `FlashScreen`** — проверка приостановлена во время активной прошивки/erase (обрыв обнаружит сам `flash_usb.py` subprocess).
|
|
||||||
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов (обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не просто исчезновение устройства из списка).
|
|
||||||
- При срабатывании — `ConnectionLost` message → экран постит `FlashDone(success=False, target=None)` / `DiagDone(reason=...)` → `ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает на `WaitingScreen(disconnect_reason=...)`.
|
|
||||||
- `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем продолжает обычный автодетект.
|
|
||||||
|
|
||||||
Архитектурное решение: **сессия никогда не восстанавливается** — после разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует диагностику с нуля.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AppFrame — общий каркас экранов
|
|
||||||
|
|
||||||
`widgets/app_frame.py` — единый контейнер, который оборачивает содержимое всех трёх основных экранов:
|
|
||||||
|
|
||||||
```css
|
|
||||||
AppFrame {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 160;
|
|
||||||
max-height: 50;
|
|
||||||
border: heavy $primary;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Решает две задачи:
|
|
||||||
1. **Визуальная консистентность** — одна и та же рамка на всех экранах.
|
|
||||||
2. **Устраняет краш Textual 8.x** при mouse drag (`assert isinstance(content_widget.parent, Widget)`) — раньше `Screen` мог выступать `content_widget` напрямую; с `AppFrame` между `Screen` и контентом всегда есть валидный промежуточный `Widget`.
|
|
||||||
|
|
||||||
Адаптивный размер (`100%` с потолком `160×50`) — гарантирует, что элементы управления (кнопки, таблицы) никогда не обрезаются на маленьком терминале и не расползаются на огромном мониторе.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Архитектура экранов
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph ServiceApp["ServiceApp (app.py)"]
|
|
||||||
direction LR
|
|
||||||
WS["WaitingScreen"]
|
|
||||||
FS["FlashScreen"]
|
|
||||||
PF["PostFlashScreen"]
|
|
||||||
DS["DiagScreen"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph DiagInternals["DiagScreen (screens/diag/)"]
|
|
||||||
TL["TestListPanel\ntest_list.py"]
|
|
||||||
RP["ResultsPanel (DataTable)\nresults.py"]
|
|
||||||
CP["ConfirmPanel\nconfirm_panel.py"]
|
|
||||||
OR["Orchestrator\norchestrator.py"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Clients["Клиенты"]
|
|
||||||
FC["FirmwareClient"]
|
|
||||||
M5["M5Client"]
|
|
||||||
FL["Flasher"]
|
|
||||||
end
|
|
||||||
|
|
||||||
WS -->|"DeviceDetected(FLASHING)"| FS
|
|
||||||
WS -->|"DeviceDetected(DIAGNOSING)"| DS
|
|
||||||
FS -->|"FlashDone(success=True, target=FIRMWARE_TEST)"| PF
|
|
||||||
FS -->|"FlashDone(остальное)"| WS
|
|
||||||
PF -->|"Done"| WS
|
|
||||||
DS -->|"DiagDone(reason)"| WS
|
|
||||||
|
|
||||||
FS --> FL
|
|
||||||
DS --> OR
|
|
||||||
DS --> TL
|
|
||||||
DS --> RP
|
|
||||||
DS --> CP
|
|
||||||
CP -->|"Confirmed"| DS
|
|
||||||
OR --> FC
|
|
||||||
OR --> M5
|
|
||||||
WS --> FL
|
|
||||||
|
|
||||||
FS -.->|"ConnectionWatcherMixin"| FL
|
|
||||||
DS -.->|"ConnectionWatcherMixin"| FL
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Жизненный цикл диагностической сессии
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant OP as Оператор
|
|
||||||
participant TUI as ServiceApp
|
|
||||||
participant WS as WaitingScreen
|
|
||||||
participant DS as DiagScreen
|
|
||||||
participant FW as firmware_test
|
|
||||||
participant M5 as M5StampPLC
|
|
||||||
|
|
||||||
OP->>TUI: запустить service_tui
|
|
||||||
TUI->>WS: push_screen()
|
|
||||||
WS->>WS: pyusb poll каждые 1.5 с
|
|
||||||
|
|
||||||
OP->>FW: подключить плату USB
|
|
||||||
WS->>TUI: DeviceDetected(DIAGNOSING)
|
|
||||||
TUI->>FW: auto_connect() → ping→pong
|
|
||||||
TUI->>FW: get_version()
|
|
||||||
TUI->>M5: auto_connect() (опционально)
|
|
||||||
TUI->>DS: switch_screen(fw_version=...)
|
|
||||||
|
|
||||||
DS->>FW: list_tests() → TestInfo×N
|
|
||||||
DS->>FW: get_uid()
|
|
||||||
DS->>DS: populate (тесты НЕ выбраны по умолчанию)
|
|
||||||
DS->>DS: ConnectionWatcherMixin: старт мониторинга
|
|
||||||
|
|
||||||
OP->>DS: выбрать тесты / "Выбрать все" → Запустить
|
|
||||||
DS->>FW: run_selected([...])
|
|
||||||
|
|
||||||
loop Для каждого теста
|
|
||||||
FW-->>DS: test_begin
|
|
||||||
DS->>DS: ResultsPanel.set_running()
|
|
||||||
|
|
||||||
opt progress (напр. usd)
|
|
||||||
FW-->>DS: progress {step, status}
|
|
||||||
DS->>DS: обновить прогресс-строку
|
|
||||||
end
|
|
||||||
|
|
||||||
alt HIL confirm (opto / can)
|
|
||||||
FW-->>DS: confirm_request
|
|
||||||
DS->>M5: relay_set() / can_send() / can_recv()
|
|
||||||
DS->>FW: send_confirm(true/false)
|
|
||||||
else Оператор (display / mqs)
|
|
||||||
FW-->>DS: confirm_request
|
|
||||||
DS->>DS: ConfirmPanel.show_operator()
|
|
||||||
OP->>DS: OK / Нет
|
|
||||||
DS->>FW: send_confirm(true/false)
|
|
||||||
else Кнопки
|
|
||||||
FW-->>DS: confirm_request
|
|
||||||
DS->>DS: ConfirmPanel.show_buttons_hint()
|
|
||||||
OP->>FW: физическое нажатие
|
|
||||||
end
|
|
||||||
|
|
||||||
FW-->>DS: test_result
|
|
||||||
DS->>DS: ResultsPanel.set_result() + сортировка FAIL-наверх
|
|
||||||
end
|
|
||||||
|
|
||||||
FW-->>DS: summary
|
|
||||||
DS->>DS: показать итог PASS / FAIL
|
|
||||||
|
|
||||||
alt Нормальное завершение
|
|
||||||
OP->>DS: ESC / Выйти → DiagDone()
|
|
||||||
TUI->>WS: switch_screen()
|
|
||||||
else Потеря USB
|
|
||||||
DS->>DS: ConnectionLost
|
|
||||||
TUI->>FW: disconnect()
|
|
||||||
TUI->>WS: switch_screen(disconnect_reason=...)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Конфигурация (`.env`)
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# USB VID:PID — BootROM SDP (константы NXP, не менять)
|
|
||||||
BOOTROM_VID=1fc9
|
|
||||||
BOOTROM_PID=0130
|
|
||||||
|
|
||||||
# USB VID:PID — Flashloader (константы NXP, не менять)
|
|
||||||
FLASHLOADER_VID=15a2
|
|
||||||
FLASHLOADER_PID=0073
|
|
||||||
|
|
||||||
# USB VID:PID — firmware_test CDC (наше устройство)
|
|
||||||
SERVICE_CDC_VID=1996
|
|
||||||
SERVICE_CDC_PID=00ad
|
|
||||||
|
|
||||||
# Тип сборки firmware_test для прошивки (Debug | Release).
|
|
||||||
# Release временно нестабилен — по умолчанию Debug.
|
|
||||||
FIRMWARE_BUILD_TYPE=Debug
|
|
||||||
|
|
||||||
# Опционально: путь к директории лога TUI
|
|
||||||
# SERVICE_LOG_DIR=/tmp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Версионирование firmware
|
|
||||||
|
|
||||||
`firmware_test` версионируется через CMake (`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через `configure_file`. Команда протокола `get_version` (по аналогии с `get_uid`) запрашивается один раз при подключении в `ServiceApp._connect_and_diagnose()` и передаётся в `DiagScreen` параметром конструктора — версия не запрашивается повторно внутри самого экрана.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
### Из монорепозитория (разработчик)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
just host::service-setup # установить зависимости tools/production/
|
|
||||||
just host::service-tui # запустить TUI
|
|
||||||
```
|
|
||||||
|
|
||||||
### Standalone-бинарь (сервисник)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
just host::service-build
|
|
||||||
# → tools/production/dist/service_tui
|
|
||||||
```
|
|
||||||
|
|
||||||
> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен инициализированный `tools/host/` (`just host::setup-tools`), либо абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -428,7 +140,7 @@ just host::service-build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
1. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
1. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen
|
||||||
2. Выбрать firmware_test → Прошить
|
2. Выбрать «Диагностическая прошивка (firmware_test)» → Загрузить
|
||||||
3. PostFlashScreen: BOOT_MOD_1 → GND, сбросить плату
|
3. PostFlashScreen: BOOT_MOD_1 → GND, сбросить плату
|
||||||
4. Нажать "Готово" (или дождаться авто-перехода через 40с)
|
4. Нажать "Готово" (или дождаться авто-перехода через 40с)
|
||||||
5. TUI автоматически попадает в DiagScreen при следующем подключении
|
5. TUI автоматически попадает в DiagScreen при следующем подключении
|
||||||
|
|
@ -438,12 +150,63 @@ just host::service-build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3)
|
1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3)
|
||||||
2. FlashScreen → Chip Erase (~30 с)
|
2. FlashScreen → Очистить память (~30 с)
|
||||||
3. После erase BootROM не загрузит прошивку — требуется перепрошить
|
3. После erase BootROM не загрузит прошивку — требуется перепрошить
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Конфигурация (`.env`)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# USB VID:PID — BootROM SDP (константы NXP, не менять)
|
||||||
|
BOOTROM_VID=1fc9
|
||||||
|
BOOTROM_PID=0130
|
||||||
|
|
||||||
|
# USB VID:PID — Flashloader (константы NXP, не менять)
|
||||||
|
FLASHLOADER_VID=15a2
|
||||||
|
FLASHLOADER_PID=0073
|
||||||
|
|
||||||
|
# USB VID:PID — firmware_test CDC (наше устройство)
|
||||||
|
SERVICE_CDC_VID=1996
|
||||||
|
SERVICE_CDC_PID=00ad
|
||||||
|
|
||||||
|
# USB VID:PID — M5StampPLC (детект HIL-стенда)
|
||||||
|
SERVICE_M5_VID=303a
|
||||||
|
SERVICE_M5_PID=4001
|
||||||
|
|
||||||
|
# Тип сборки firmware_test для прошивки (Debug | Release).
|
||||||
|
# Release временно нестабилен — по умолчанию Debug.
|
||||||
|
FIRMWARE_BUILD_TYPE=Debug
|
||||||
|
|
||||||
|
# Опционально: путь к директории лога TUI
|
||||||
|
# SERVICE_LOG_DIR=/tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
### Из монорепозитория (разработчик)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::service-setup # установить зависимости tools/production/
|
||||||
|
just host::service-tui # запустить TUI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standalone-бинарь (сервисник)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::service-build
|
||||||
|
# → tools/production/dist/service_tui
|
||||||
|
```
|
||||||
|
|
||||||
|
> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен
|
||||||
|
> инициализированный `tools/host/` (`just host::setup-tools`), либо
|
||||||
|
> абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
|
|
||||||
| Пакет | Версия | Назначение |
|
| Пакет | Версия | Назначение |
|
||||||
|
|
@ -454,21 +217,9 @@ just host::service-build
|
||||||
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
| `python-dotenv` | ≥ 1.0 | загрузка `.env` |
|
||||||
| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря |
|
| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря |
|
||||||
|
|
||||||
**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает `tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть инициализирован (`just host::setup-tools`).
|
**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает
|
||||||
|
`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть
|
||||||
---
|
инициализирован (`just host::setup-tools`).
|
||||||
|
|
||||||
## Известные грабли Textual 8.x (для тех, кто продолжит разработку)
|
|
||||||
|
|
||||||
Зафиксировано на практике — экономит время при будущих доработках:
|
|
||||||
|
|
||||||
- **`Screen.Message` не существует.** Вложенные сообщения экранов наследуются от `textual.message.Message` напрямую, не от несуществующего атрибута `Screen.Message`.
|
|
||||||
- **`self._running` — зарезервированное имя.** `MessagePump` (предок `Screen`) использует это поле для своего внутреннего message loop. Случайное совпадение имени тихо ломает логику без исключения — в `DiagScreen` переименовано в `_tests_running`.
|
|
||||||
- **`row.mount(child)` сразу после `self.mount(row)` бросает `MountError`** — `row` ещё не прикреплён к DOM. Решение: передавать детей в конструктор контейнера (`Horizontal(cb, label, classes=...)`) и монтировать одним `mount_all()`.
|
|
||||||
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня проекта — постоянно расходится при рефакторинге структуры. Решение: один `CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`.
|
|
||||||
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает ширину по умолчанию равную длине заголовка — длинный контент обрезается независимо от `height` строки. Нужно использовать `add_column(label, width=N)` по одной колонке.
|
|
||||||
- **`DataTable.sort(*columns, key=fn)` передаёт в `key()` кортеж значений ячеек** (для указанных `columns`), не `row_key` и не `(row_key, row_data)`. Сортировка по `test_id` напрямую невозможна без парсинга содержимого ячеек, которые сами полностью контролируем.
|
|
||||||
- **Нет публичного API для изменения высоты уже добавленной строки.** `update_cell()` меняет только содержимое. Если нужно изменить `height` (например, под более длинный текст) — единственный надёжный путь: `remove_row()` + `add_row(..., height=N)`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -479,4 +230,5 @@ tools/production/service_tui.log ← по умолчанию
|
||||||
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env
|
$SERVICE_LOG_DIR/service_tui.log ← если задан в .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не пишет в stdout — Textual захватывает терминал.
|
Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не
|
||||||
|
пишет в stdout — Textual захватывает терминал.
|
||||||
|
|
|
||||||
453
tools/production/docs/DEV_ARCH.md
Normal file
453
tools/production/docs/DEV_ARCH.md
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
# service-tui — техническая архитектура
|
||||||
|
|
||||||
|
> Компонент: `tools/production/` — TUI сервисного инженера (диагностика и
|
||||||
|
> прошивка платы MIMXRT1052CVJ5B).
|
||||||
|
> Документ описывает внутреннее устройство: структуру модулей, протокол
|
||||||
|
> взаимодействия с firmware/M5, экранную архитектуру Textual, известные
|
||||||
|
> особенности фреймворка.
|
||||||
|
> Пользовательская документация (экраны, запуск, конфигурация,
|
||||||
|
> рабочие процессы сервисника) — в [README.md](README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Структура проекта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tools/production/
|
||||||
|
├── main.py ← точка входа (10 строк)
|
||||||
|
├── pyproject.toml ← зависимости uv
|
||||||
|
├── uv.lock
|
||||||
|
└── app/
|
||||||
|
├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов
|
||||||
|
├── app.tcss ← единый файл стилей для всех экранов
|
||||||
|
├── models.py ← все типы данных (dataclass/Enum)
|
||||||
|
├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen
|
||||||
|
├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8)
|
||||||
|
├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8)
|
||||||
|
├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py
|
||||||
|
├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты
|
||||||
|
├── widgets/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов
|
||||||
|
└── screens/
|
||||||
|
├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, PostFlashScreen, DiagScreen
|
||||||
|
├── waiting.py ← WaitingScreen — ожидание USB, лого, версия, баннер причины возврата
|
||||||
|
├── flash.py ← FlashScreen — прошивка / chip erase
|
||||||
|
├── post_flash.py ← PostFlashScreen — промпт смены BootMode после прошивки
|
||||||
|
├── connection_watcher.py ← ConnectionWatcherMixin — мониторинг обрыва USB
|
||||||
|
└── diag/
|
||||||
|
├── __init__.py ← DiagScreen — координатор диагностики
|
||||||
|
├── test_list.py ← TestListPanel — чекбоксы тестов, Выбрать/Снять все
|
||||||
|
├── results.py ← ResultsPanel — DataTable результатов
|
||||||
|
└── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Концепция
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph PC["Сервисный ПК"]
|
||||||
|
TUI["service-tui\n(Textual App)"]
|
||||||
|
subgraph app["app/"]
|
||||||
|
FC["firmware_client.py\nUSB CDC ACM, UTF-8"]
|
||||||
|
M5["m5_client.py\nSerial JSON-lines, UTF-8"]
|
||||||
|
FL["flasher.py\nsubprocess + pyusb detect"]
|
||||||
|
OR["orchestrator.py\nconfirm/progress/timeout router"]
|
||||||
|
end
|
||||||
|
TUI --> FC & M5 & FL & OR
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Board["Плата TFT (MIMXRT1052)"]
|
||||||
|
FW["firmware_test\n(USB CDC)"]
|
||||||
|
ROM["BootROM SDP\n(1FC9:0130)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph HIL["HIL стенд (опционально)"]
|
||||||
|
M5HW["M5StampPLC\nRLY1–4 + CAN"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Host["tools/host/"]
|
||||||
|
FU["flash_usb.py\nsdphost + blhost"]
|
||||||
|
end
|
||||||
|
|
||||||
|
FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW
|
||||||
|
FL -->|"subprocess uv run"| FU
|
||||||
|
FU -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM
|
||||||
|
M5 <-->|"JSON-lines\nSerial"| M5HW
|
||||||
|
M5HW -->|"RLY1–4"| Board
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как
|
||||||
|
> основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через
|
||||||
|
> `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC.
|
||||||
|
> M5StampPLC детектируется отдельно в `m5_client.py` тем же способом
|
||||||
|
> (`pyusb`, VID/PID из `.env` — см. раздел 5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Диаграмма состояний приложения
|
||||||
|
|
||||||
|
Состояние определяется автодетектом USB и меняется динамически без
|
||||||
|
перезапуска TUI. При потере соединения сессия разрывается полностью — TUI не
|
||||||
|
пытается восстановить прежнее состояние, а стартует заново с `WaitingScreen`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> WAITING : запуск TUI
|
||||||
|
|
||||||
|
WAITING --> FLASHING : VID:PID 1FC9:0130\n(BootROM SDP)
|
||||||
|
WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC
|
||||||
|
|
||||||
|
FLASHING --> POST_FLASH : firmware_test прошит успешно
|
||||||
|
FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое
|
||||||
|
|
||||||
|
POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с
|
||||||
|
|
||||||
|
DIAGNOSING --> WAITING : DiagDone / ESC /\nпотеря USB в простое
|
||||||
|
DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка BOOT_MOD)
|
||||||
|
```
|
||||||
|
|
||||||
|
Состояния соответствуют `AppMode` в `models.py`; переключение экранов —
|
||||||
|
`ServiceApp.push_screen()`/`switch_screen()` в `app.py`, реагирующий на
|
||||||
|
сообщения `DeviceDetected`/`FlashDone`/`DiagDone`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Обработка confirm_request
|
||||||
|
|
||||||
|
Маршрутизация реализована в `Orchestrator._handle_confirm()` по значению
|
||||||
|
`confirm_request.id`. Помимо confirm, протокол v2 определяет `progress` —
|
||||||
|
внутришаговые информационные события долгих тестов (сейчас только `usd`), не
|
||||||
|
требующие ответа.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
EV["Событие от firmware_test"]
|
||||||
|
EV --> T{type}
|
||||||
|
|
||||||
|
T -->|"confirm_request"| R{confirm_request.id}
|
||||||
|
T -->|"progress"| PROG["TEST_PROGRESS\nотобразить фазу в прогресс-строке"]
|
||||||
|
T -->|"test_begin / test_result / summary"| STD["стандартная обработка"]
|
||||||
|
T -->|"_timeout (синтетическое,\nот FirmwareClient)"| TO["синтезировать FAIL\nдля зависшего теста\n+ гарантированный SUMMARY"]
|
||||||
|
T -->|"неизвестный тип"| LOG["logger.debug — НЕ ошибка,\nне показывается оператору"]
|
||||||
|
|
||||||
|
R -->|"opto_*"| HIL_OPTO["M5: relay_set → settle → send_confirm"]
|
||||||
|
R -->|"can_rx_ready"| HIL_CAN_RX["M5: can_send → send_confirm"]
|
||||||
|
R -->|"can_tx_verify"| HIL_CAN_TX["M5: can_recv → verify → send_confirm"]
|
||||||
|
R -->|"btn*"| BTN["show_buttons_hint, БЕЗ JSON-confirm"]
|
||||||
|
R -->|"остальное"| OP["show_operator + countdown\nждать resolve_operator_confirm()"]
|
||||||
|
```
|
||||||
|
|
||||||
|
| `confirm_request.id` | Кто отвечает | Реле M5 |
|
||||||
|
| ------------------------------- | ---------------------------- | ------- |
|
||||||
|
| `opto_in1_active` / `_inactive` | M5 авто | RLY3 |
|
||||||
|
| `opto_in2_active` / `_inactive` | M5 авто | RLY4 |
|
||||||
|
| `opto_rs_active` / `_inactive` | M5 авто | RLY2 |
|
||||||
|
| `can_rx_ready` | M5 авто (CAN TX) | — |
|
||||||
|
| `can_tx_verify` | M5 авто (CAN RX) | — |
|
||||||
|
| `btn*` | физика, без JSON-ответа | — |
|
||||||
|
| всё остальное | оператор, prompt + countdown | — |
|
||||||
|
|
||||||
|
**Гарантия таймаута:** `Orchestrator.run_tests()` всегда завершается ровно
|
||||||
|
одним событием `SUMMARY` — настоящим от firmware или синтетическим
|
||||||
|
(`aborted: true`), если чтение порта оборвалось по таймауту. Без этой гарантии
|
||||||
|
зависший тест блокировал бы кнопки "Выйти" и повторного запуска навсегда
|
||||||
|
(исторический баг, см. `CHANGELOG.md`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Протокол M5 agent (JSON-lines)
|
||||||
|
|
||||||
|
`m5_client.py` общается с `tools/hil/m5/agent.py` напрямую (не через
|
||||||
|
`tools/hil/` pytest-окружение — у HIL pytest свой собственный путь: порт берёт
|
||||||
|
из `.env` (`HIL_M5_PORT`) и не использует `M5Client`).
|
||||||
|
|
||||||
|
Актуальный формат ответа агента:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ok": true, "id": 1, "data": [...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Значимые детали, зафиксированные по факту сверки с `agent.py` (`grep` по
|
||||||
|
обработчикам команд):
|
||||||
|
|
||||||
|
- Поле успеха — **`ok`** (bool), не `status`. Относится ко всем командам:
|
||||||
|
`ping`, `relay_set`, `relay_get`, `can_send`, `can_recv`.
|
||||||
|
- Ключ канала реле в `relay_set`/`relay_get` — **`ch`**, не `relay`.
|
||||||
|
- VID/PID детекта M5StampPLC настраиваются через `.env`:
|
||||||
|
`SERVICE_M5_VID`/`SERVICE_M5_PID` (см. README, раздел «Конфигурация»).
|
||||||
|
Рантайм-режим агента (MicroPython) отличается от ROM-режима ESP32-S3 по
|
||||||
|
PID — при детекте ориентироваться на `just host::m5-scan`, а не на
|
||||||
|
документацию, если она когда-либо разойдётся с кодом.
|
||||||
|
|
||||||
|
**Важно на будущее:** документация (`HIL_BENCH.md`/`HIL_HOW_TO.md`) местами не
|
||||||
|
успевает за изменениями `agent.py`. При любых будущих изменениях протокола
|
||||||
|
агента (новые команды, смена формата ответа) — сверяться напрямую через
|
||||||
|
`grep` по `tools/hil/m5/agent.py`, а не полагаться только на документацию.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Мониторинг соединения и разрыв сессии
|
||||||
|
|
||||||
|
`ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к
|
||||||
|
`FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине.
|
||||||
|
|
||||||
|
- **На `FlashScreen`** — проверка приостановлена во время активной
|
||||||
|
прошивки/erase (обрыв обнаружит сам `flash_usb.py` subprocess).
|
||||||
|
- **На `DiagScreen`** — проверка приостановлена во время прогона тестов
|
||||||
|
(обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не
|
||||||
|
просто исчезновение устройства из списка).
|
||||||
|
- При срабатывании — `ConnectionLost` message → экран постит
|
||||||
|
`FlashDone(success=False, target=None)` / `DiagDone(reason=...)` →
|
||||||
|
`ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает
|
||||||
|
на `WaitingScreen(disconnect_reason=...)`.
|
||||||
|
- `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем
|
||||||
|
продолжает обычный автодетект.
|
||||||
|
|
||||||
|
Архитектурное решение: **сессия никогда не восстанавливается** — после
|
||||||
|
разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует
|
||||||
|
диагностику с нуля.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. AppFrame — общий каркас экранов
|
||||||
|
|
||||||
|
`widgets/app_frame.py` — единый контейнер, который оборачивает содержимое
|
||||||
|
всех трёх основных экранов:
|
||||||
|
|
||||||
|
```css
|
||||||
|
AppFrame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 112;
|
||||||
|
max-height: 35;
|
||||||
|
border: heavy $primary;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Решает две задачи:
|
||||||
|
|
||||||
|
1. **Визуальная консистентность** — одна и та же рамка на всех экранах.
|
||||||
|
2. **Устраняет краш Textual 8.x** при mouse drag
|
||||||
|
(`assert isinstance(content_widget.parent, Widget)`) — раньше `Screen` мог
|
||||||
|
выступать `content_widget` напрямую; с `AppFrame` между `Screen` и
|
||||||
|
контентом всегда есть валидный промежуточный `Widget`.
|
||||||
|
|
||||||
|
Адаптивный размер (`100%` с потолком `112×35`) — гарантирует, что элементы
|
||||||
|
управления (кнопки, таблицы) никогда не обрезаются на маленьком терминале и
|
||||||
|
не расползаются на огромном мониторе. Потолок подобран и подтверждён
|
||||||
|
визуально на скриншотах всех пяти экранов; нижняя граница по ширине
|
||||||
|
обоснована жёстко: `TestListPanel(width:38)` + `ResultsPanel(width:70)` рядом
|
||||||
|
на `DiagScreen` дают 108 + рамка = 110 — меньше сжимать уже нельзя.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Архитектура экранов
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph ServiceApp["ServiceApp (app.py)"]
|
||||||
|
direction LR
|
||||||
|
WS["WaitingScreen"]
|
||||||
|
FS["FlashScreen"]
|
||||||
|
PF["PostFlashScreen"]
|
||||||
|
DS["DiagScreen"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DiagInternals["DiagScreen (screens/diag/)"]
|
||||||
|
TL["TestListPanel\ntest_list.py"]
|
||||||
|
RP["ResultsPanel (DataTable)\nresults.py"]
|
||||||
|
CP["ConfirmPanel\nconfirm_panel.py"]
|
||||||
|
OR["Orchestrator\norchestrator.py"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Clients["Клиенты"]
|
||||||
|
FC["FirmwareClient"]
|
||||||
|
M5["M5Client"]
|
||||||
|
FL["Flasher"]
|
||||||
|
end
|
||||||
|
|
||||||
|
WS -->|"DeviceDetected(FLASHING)"| FS
|
||||||
|
WS -->|"DeviceDetected(DIAGNOSING)"| DS
|
||||||
|
FS -->|"FlashDone(success=True, target=FIRMWARE_TEST)"| PF
|
||||||
|
FS -->|"FlashDone(остальное)"| WS
|
||||||
|
PF -->|"Done"| WS
|
||||||
|
DS -->|"DiagDone(reason)"| WS
|
||||||
|
|
||||||
|
FS --> FL
|
||||||
|
DS --> OR
|
||||||
|
DS --> TL
|
||||||
|
DS --> RP
|
||||||
|
DS --> CP
|
||||||
|
CP -->|"Confirmed"| DS
|
||||||
|
OR --> FC
|
||||||
|
OR --> M5
|
||||||
|
WS --> FL
|
||||||
|
|
||||||
|
FS -.->|"ConnectionWatcherMixin"| FL
|
||||||
|
DS -.->|"ConnectionWatcherMixin"| FL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Жизненный цикл диагностической сессии
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant OP as Оператор
|
||||||
|
participant TUI as ServiceApp
|
||||||
|
participant WS as WaitingScreen
|
||||||
|
participant DS as DiagScreen
|
||||||
|
participant FW as firmware_test
|
||||||
|
participant M5 as M5StampPLC
|
||||||
|
|
||||||
|
OP->>TUI: запустить service_tui
|
||||||
|
TUI->>WS: push_screen()
|
||||||
|
WS->>WS: pyusb poll каждые 1.5 с
|
||||||
|
|
||||||
|
OP->>FW: подключить плату USB
|
||||||
|
WS->>TUI: DeviceDetected(DIAGNOSING)
|
||||||
|
TUI->>FW: auto_connect() → ping→pong
|
||||||
|
TUI->>FW: get_version()
|
||||||
|
TUI->>M5: auto_connect() (опционально)
|
||||||
|
TUI->>DS: switch_screen(fw_version=...)
|
||||||
|
|
||||||
|
DS->>FW: list_tests() → TestInfo×N
|
||||||
|
DS->>FW: get_uid()
|
||||||
|
DS->>DS: populate (тесты НЕ выбраны по умолчанию)
|
||||||
|
DS->>DS: ConnectionWatcherMixin: старт мониторинга
|
||||||
|
|
||||||
|
OP->>DS: выбрать тесты / "Выбрать все" → Запустить
|
||||||
|
DS->>FW: run_selected([...])
|
||||||
|
|
||||||
|
loop Для каждого теста
|
||||||
|
FW-->>DS: test_begin
|
||||||
|
DS->>DS: ResultsPanel.set_running()
|
||||||
|
|
||||||
|
opt progress (напр. usd)
|
||||||
|
FW-->>DS: progress {step, status}
|
||||||
|
DS->>DS: обновить прогресс-строку
|
||||||
|
end
|
||||||
|
|
||||||
|
alt HIL confirm (opto / can)
|
||||||
|
FW-->>DS: confirm_request
|
||||||
|
DS->>M5: relay_set() / can_send() / can_recv()
|
||||||
|
DS->>FW: send_confirm(true/false)
|
||||||
|
else Оператор (display / mqs)
|
||||||
|
FW-->>DS: confirm_request
|
||||||
|
DS->>DS: ConfirmPanel.show_operator()
|
||||||
|
OP->>DS: OK / Нет
|
||||||
|
DS->>FW: send_confirm(true/false)
|
||||||
|
else Кнопки
|
||||||
|
FW-->>DS: confirm_request
|
||||||
|
DS->>DS: ConfirmPanel.show_buttons_hint()
|
||||||
|
OP->>FW: физическое нажатие
|
||||||
|
end
|
||||||
|
|
||||||
|
FW-->>DS: test_result
|
||||||
|
DS->>DS: ResultsPanel.set_result() + сортировка FAIL-наверх
|
||||||
|
end
|
||||||
|
|
||||||
|
FW-->>DS: summary
|
||||||
|
DS->>DS: показать итог PASS / FAIL
|
||||||
|
|
||||||
|
alt Нормальное завершение
|
||||||
|
OP->>DS: ESC / Выйти → DiagDone()
|
||||||
|
TUI->>WS: switch_screen()
|
||||||
|
else Потеря USB
|
||||||
|
DS->>DS: ConnectionLost
|
||||||
|
TUI->>FW: disconnect()
|
||||||
|
TUI->>WS: switch_screen(disconnect_reason=...)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Версионирование firmware
|
||||||
|
|
||||||
|
`firmware_test` версионируется через CMake
|
||||||
|
(`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через
|
||||||
|
`configure_file`. Команда протокола `get_version` (по аналогии с `get_uid`)
|
||||||
|
запрашивается один раз при подключении в `ServiceApp._connect_and_diagnose()`
|
||||||
|
и передаётся в `DiagScreen` параметром конструктора — версия не запрашивается
|
||||||
|
повторно внутри самого экрана.
|
||||||
|
|
||||||
|
Версия самого TUI (`service_tool vX.Y.Z` на `WaitingScreen`) читается
|
||||||
|
отдельно — `waiting.py::_read_app_version()` парсит `[project].version` из
|
||||||
|
`pyproject.toml` напрямую через `tomllib` (stdlib). `importlib.metadata`
|
||||||
|
сознательно не используется — проект не ставится как пакет
|
||||||
|
(`tool.uv.package = false`), метаданных может не быть.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Логотип (`boot_art.py`)
|
||||||
|
|
||||||
|
`LOGO_ART` — Rich-markup строка (29×21 символов, цвета `#3ca0dc` для синей
|
||||||
|
части логотипа, `white` для тёмной, `grey37` для фоновых точек), полученная
|
||||||
|
одноразовой coverage-based растеризацией `logo.png` (300×300 RGBA) через
|
||||||
|
Pillow: разбор на сетку символов с компенсацией аспекта шрифта терминала
|
||||||
|
(`ASPECT = 0.5`), классификация фона/двух цветовых групп логотипа по каналам,
|
||||||
|
плотность символа на ячейку — по доле непрозрачных пикселей
|
||||||
|
(`.::+*#`/`.::+%@`).
|
||||||
|
|
||||||
|
Сам скрипт растеризации **не сохранён в репозитории** (использовался
|
||||||
|
разово в песочнице, не входит в `pyproject.toml` TUI — новых
|
||||||
|
runtime-зависимостей `boot_art.py` не добавляет). Если логотип компании
|
||||||
|
сменится — скрипт нужно будет написать заново; логика воспроизводима (см.
|
||||||
|
абзац выше).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Известные грабли Textual 8.x
|
||||||
|
|
||||||
|
Зафиксировано на практике — экономит время при будущих доработках:
|
||||||
|
|
||||||
|
- **`Screen.Message` не существует.** Вложенные сообщения экранов
|
||||||
|
наследуются от `textual.message.Message` напрямую, не от несуществующего
|
||||||
|
атрибута `Screen.Message`.
|
||||||
|
- **`self._running` — зарезервированное имя.** `MessagePump` (предок
|
||||||
|
`Screen`) использует это поле для своего внутреннего message loop.
|
||||||
|
Случайное совпадение имени тихо ломает логику без исключения — в
|
||||||
|
`DiagScreen` переименовано в `_tests_running`.
|
||||||
|
- **`row.mount(child)` сразу после `self.mount(row)` бросает `MountError`** —
|
||||||
|
`row` ещё не прикреплён к DOM. Решение: передавать детей в конструктор
|
||||||
|
контейнера (`Horizontal(cb, label, classes=...)`) и монтировать одним
|
||||||
|
`mount_all()`.
|
||||||
|
- **`CSS_PATH` резолвится относительно файла класса**, не относительно корня
|
||||||
|
проекта — постоянно расходится при рефакторинге структуры. Решение: один
|
||||||
|
`CSS_PATH` только на `ServiceApp`, все стили в едином `app.tcss`.
|
||||||
|
- **`table.add_columns(*labels)` не принимает `width=`.** Колонка получает
|
||||||
|
ширину по умолчанию равную длине заголовка — длинный контент обрезается
|
||||||
|
независимо от `height` строки. Нужно использовать `add_column(label,
|
||||||
|
width=N)` по одной колонке.
|
||||||
|
- **`DataTable.sort(*columns, key=fn)` передаёт в `key()` кортеж значений
|
||||||
|
ячеек** (для указанных `columns`), не `row_key` и не `(row_key, row_data)`.
|
||||||
|
Сортировка по `test_id` напрямую невозможна без парсинга содержимого ячеек,
|
||||||
|
которые сами полностью контролируем.
|
||||||
|
- **Нет публичного API для изменения высоты уже добавленной строки.**
|
||||||
|
`update_cell()` меняет только содержимое. Если нужно изменить `height`
|
||||||
|
(например, под более длинный текст) — единственный надёжный путь:
|
||||||
|
`remove_row()` + `add_row(..., height=N)`.
|
||||||
|
- **Центровка текста внутри full-width виджета не решается `Center()`.**
|
||||||
|
`Static`/`Label` без `text-align`, растянутый на всю ширину родителя,
|
||||||
|
прижимает текст к левому краю — `Center()` вокруг такого виджета не
|
||||||
|
помогает (центрировать нечего, ребёнок и так 100% ширины). Нужен
|
||||||
|
`text-align: center` в CSS на самом элементе. Обратный случай — виджет с
|
||||||
|
"естественной" (auto) шириной — центрируется именно через `Center()`.
|
||||||
|
Важно не путать эти два случая (`#waiting-version` — первый случай,
|
||||||
|
`#post-flash-title`/`#post-flash-instruction` — второй).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Известные открытые вопросы
|
||||||
|
|
||||||
|
- **Release-сборка firmware нестабильна** (медленное мигание — подозрение на
|
||||||
|
проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно
|
||||||
|
форсирует Debug через `FIRMWARE_BUILD_TYPE`.
|
||||||
|
- **`tools/shared/m5_agent.py`** — сознательно не делался: pytest
|
||||||
|
HIL-окружение и TUI используют независимые M5-клиенты, признано правильным
|
||||||
|
архитектурным решением, а не техдолгом.
|
||||||
|
- Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и
|
||||||
|
«копирование UID с экрана» — отложены, не начаты.
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +0,0 @@
|
||||||
# TUI Service Tools замечания №2
|
|
||||||
|
|
||||||
1. Тест usd проводится только один раз, при втором прогоне зависает намертво (возможно usdhc стек не деинициализируется корректно)
|
|
||||||
2. При выполнении теста psd в прогресс баре ошибка - unknown event progress
|
|
||||||
3. Нужно вообще убрать проценты в статус баре (при прохождении тестов) - лишняя история
|
|
||||||
4. Fallback при потере USB соединения в Flasher и Testing screen - возможно ли отображать статус поключения к таргету? (SDP, firmware_test) и есть статус отключен не давать взаимодействовать с элементами экранов (кроме выйти)
|
|
||||||
5. После таймаута по выполнению теста (recv_until timeout after 120s) нет никакой индикации о статусе теста в таблице (от по факту завален) + после этого не получается выйти из приложения с помощью кнопки и не получается запустить ни один тест
|
|
||||||
6. В таблице тестов в столбце Детали при неудачном выполнение теста обрезается часть информации
|
|
||||||
163
tools/production/docs/TUI_SESSION_2.md
Normal file
163
tools/production/docs/TUI_SESSION_2.md
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Отчёт: сессия доработки service-tui (продолжение)
|
||||||
|
|
||||||
|
**Период:** один рабочий тред, продолжение после `TUI_SESSION.md` (архитектура и стабилизация Этапа 8)
|
||||||
|
**Объект:** `tools/production/` — TUI сервисного инженера
|
||||||
|
**Вход в сессию:** `TUI_PLAN.md` (6 пунктов), реальный HIL-стенд (M5StampPLC подключён)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Пункт 6 — проверка HIL (CAN & Opto) ✅ ЗАКРЫТ
|
||||||
|
|
||||||
|
M5StampPLC физически был подключён, но `DiagScreen` показывал "M5 Bench: ✕ нет связи".
|
||||||
|
Три независимых бага в одном файле `tools/production/app/m5_client.py` — рассинхрон
|
||||||
|
между тем, что реально шлёт `tools/hil/m5/agent.py`, и тем, что ожидал клиент TUI.
|
||||||
|
HIL pytest (`06_test_firmware_opto.py`/`06_test_firmware_can.py`) эти баги не ловил,
|
||||||
|
потому что ходит по своему пути — порт берёт напрямую из `.env` (`HIL_M5_PORT`), а не
|
||||||
|
через VID/PID автодетект, и не использует `M5Client` вовсе.
|
||||||
|
|
||||||
|
| # | Баг | Было | Стало | Как нашли |
|
||||||
|
| --- | ------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 1 | PID автодетекта M5 | `_M5_PID = 0x1001` (hardcode) | `_M5_PID = int(os.environ.get("SERVICE_M5_PID", "0x4001"), 16)` | `just host::m5-scan` показал `303a:4001`, не `303a:1001` — `0x1001` это PID ROM-режима ESP32-S3, `0x4001` — PID рантайма с запущенным MicroPython/агентом |
|
||||||
|
| 2 | Поле успеха ответа агента | `resp.get("status") == "ok"` (везде: `ping`, `relay_set`, `relay_get`, `can_send`, `can_recv`) | `resp.get("ok") is True` | Сверка с `HIL_BENCH.md`/`HIL_HOW_TO.md` — агент отвечает `{"ok": true, ...}`, поля `"status"` в протоколе агента нет вообще |
|
||||||
|
| 3 | Ключ канала реле | `{"cmd": "relay_set", "relay": relay, ...}` | `{"cmd": "relay_set", "ch": relay, ...}` (и `relay_get`) | Подтверждено `grep` по `tools/hil/m5/agent.py` — обработчик читает `cmd["ch"]`, ключа `"relay"` не существует |
|
||||||
|
|
||||||
|
`_M5_VID` заодно вынесен в `os.environ` по аналогии с `SERVICE_CDC_VID/PID` в `app.py`
|
||||||
|
(был раньше hardcode `0x303A`, поведение не поменялось, только стал переопределяемым).
|
||||||
|
|
||||||
|
`can_send`/`can_recv` сверены отдельно (`grep -A 12` по агенту) — багов не найдено,
|
||||||
|
формат ответа (`{"ok": true, "id": ..., "data": [...]}`) уже совпадал с ожиданиями клиента.
|
||||||
|
|
||||||
|
**Подтверждено на живом стенде:**
|
||||||
|
```
|
||||||
|
connected: True
|
||||||
|
ping: True
|
||||||
|
relay_set(3, True): True
|
||||||
|
relay_get(3): True
|
||||||
|
relay_set(3, False): True
|
||||||
|
```
|
||||||
|
Прогон `opto`/`can` через `service-tui` целиком (не только pytest) — пройден, о чём
|
||||||
|
сообщил заказчик по итогу. Пункт 6 закрыт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Пункт 2 — единый размер активной области ✅ ЗАКРЫТ
|
||||||
|
|
||||||
|
**Файл:** `tools/production/app/app.tcss`, блок `AppFrame`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* было */
|
||||||
|
max-width: 160;
|
||||||
|
max-height: 50;
|
||||||
|
|
||||||
|
/* стало */
|
||||||
|
max-width: 112;
|
||||||
|
max-height: 35;
|
||||||
|
```
|
||||||
|
|
||||||
|
Итоговый размер `112×35` подобран и подтверждён визуально пользователем на реальных
|
||||||
|
скриншотах всех пяти экранов (`WAITING`, `WAITING_AFTER_LOSS`, `FLASH`, `POST_FLASH`,
|
||||||
|
`TEST_SCREEN`). Нижняя граница по ширине обоснована жёстко: `TestListPanel(width:38)`
|
||||||
|
+ `ResultsPanel(width:70)` рядом на `DiagScreen` дают 108 + рамка = 110 — меньше сжимать
|
||||||
|
уже нельзя. `width/height: 100%` (адаптивность под маленькие терминалы) не тронуты —
|
||||||
|
менялся только потолок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Пункт 3 — countdown в тесте buttons ❌ РЕШЕНО НЕ ДЕЛАТЬ
|
||||||
|
|
||||||
|
Разобрал возможную реализацию (передать `timeout_ms` в `ConfirmPanel.show_buttons_hint()`,
|
||||||
|
завести отдельный `auto_fail`-флаг в таймере, чтобы не дёргать случайный
|
||||||
|
`Confirmed`-message в ветке buttons, где хост не должен ничего отправлять). Заказчик
|
||||||
|
решил не усложнять рабочую логику ради UX-мелочи — пункт закрыт без изменений в коде.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Пункт 1 — bootlogo / логотип компании / центровка экранов ✅ ЗАКРЫТ
|
||||||
|
|
||||||
|
Изначально запрошенная "чистка лога USB-POLL/TRANSFER" (часть А) — отменена заказчиком,
|
||||||
|
не актуальна. Весь пункт свёлся к части Б: логотип + центровка.
|
||||||
|
|
||||||
|
### 4.1 Логотип — программная растеризация `logo.png`
|
||||||
|
|
||||||
|
Ручная отрисовка ASCII-арта отклонена как ненадёжная — вместо этого написан
|
||||||
|
одноразовый скрипт (Pillow, вне рантайма приложения) с coverage-based растеризацией:
|
||||||
|
|
||||||
|
1. Разбор `logo.png` (300×300 RGBA) на сетку символов с компенсацией соотношения
|
||||||
|
сторон шрифта терминала (`ASPECT = 0.5`).
|
||||||
|
2. Фон определялся не только по альфа-каналу (у файла альфа=0 только по внешним
|
||||||
|
углам), но и по близости цвета к белому (`r,g,b > 235`) — иначе весь непрозрачный
|
||||||
|
белый подложечный слой считался "закрашенным".
|
||||||
|
3. Классификация двух цветовых групп логотипа по каналам (`avgB - avgR > 15` → синий,
|
||||||
|
иначе тёмный/чёрный), калибровка порога — по гистограмме реальных цветов пикселей
|
||||||
|
(`(60,160,220)` синий кластер vs `(0,0,0)` чёрный кластер, чётко разделены).
|
||||||
|
4. Плотность символа на ячейку — по доле непрозрачных пикселей (`.::+*#` для синего,
|
||||||
|
`.::+%@` для тёмного), обрезка до bounding box контента → финальный размер 29×21.
|
||||||
|
|
||||||
|
**Результат вынесен в новый файл** `tools/production/app/boot_art.py` — константа
|
||||||
|
`LOGO_ART` (Rich-markup строка, цвета `#3ca0dc` для синей части, `white` для тёмной,
|
||||||
|
`grey37` для фоновых точек). Никаких новых рантайм-зависимостей (Pillow использовался
|
||||||
|
только в песочнице для генерации, не входит в `pyproject.toml` TUI).
|
||||||
|
|
||||||
|
### 4.2 Версия из `pyproject.toml`
|
||||||
|
|
||||||
|
`tools/production/app/screens/waiting.py` — функция `_read_app_version()`, читает
|
||||||
|
`[project].version` напрямую через `tomllib` (stdlib, `requires-python >= 3.11` уже
|
||||||
|
задан). `importlib.metadata` сознательно не использован — проект не ставится как пакет
|
||||||
|
(`tool.uv.package = false` в `pyproject.toml`), метаданных может не быть.
|
||||||
|
|
||||||
|
### 4.3 Финальная компоновка WaitingScreen
|
||||||
|
|
||||||
|
- Убран старый текст `"TFT Indicator Board\nService Tool"`.
|
||||||
|
- Добавлена строка `"service_tool vX.Y.Z"` — **над** артом (не под), цвет `$warning`
|
||||||
|
(тот же оранжевый, что в заголовке `FlashScreen` — единообразие между экранами).
|
||||||
|
- `#waiting-frame`: `align: center middle` → `align: center top` (блок прижат к верху,
|
||||||
|
не висит по вертикальному центру).
|
||||||
|
- Баг центровки самого текста версии: `Static` без `text-align` растягивается на всю
|
||||||
|
ширину родителя и текст внутри прижимается к левому краю — обёртка `Center()` тут
|
||||||
|
не помогает (центрировать нечего, ребёнок и так 100% ширины). Фикс — `text-align:
|
||||||
|
center` в CSS на самом `#waiting-version`, а не контейнер-обёртка.
|
||||||
|
- Та же причина и тот же фикс повторно всплыли на `PostFlashScreen` для
|
||||||
|
`#post-flash-title` (`Label`) и `#post-flash-instruction` (`Static`, `width: auto`) —
|
||||||
|
здесь, наоборот, у элементов есть "естественная" ширина, поэтому раз уже `Center()`
|
||||||
|
вокруг них — верное решение (в отличие от `#waiting-version`, где элемент full-width).
|
||||||
|
Оба случая — одна и та же путаница между "центрировать full-width текст" (нужен
|
||||||
|
`text-align`) и "центрировать auto-width блок" (нужен `Center()`), просто с
|
||||||
|
противоположными по природе виджетами.
|
||||||
|
|
||||||
|
**Файлы, изменённые в рамках пункта 1:**
|
||||||
|
- `tools/production/app/boot_art.py` — новый, константа `LOGO_ART`
|
||||||
|
- `tools/production/app/screens/waiting.py` — `compose()`, `_read_app_version()`
|
||||||
|
- `tools/production/app/screens/post_flash.py` — `compose()` (обёртки `Center()`)
|
||||||
|
- `tools/production/app/app.tcss` — секции `WaitingScreen`, `PostFlashScreen`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Текущее состояние `TUI_PLAN.md`
|
||||||
|
|
||||||
|
| # | Пункт | Статус |
|
||||||
|
| --- | -------------------------------------------- | -------------------------------------------- |
|
||||||
|
| 1 | Экраны USB-POLL/TRANSFER, bootlogo | ✅ закрыт (часть А отменена, часть Б сделана) |
|
||||||
|
| 2 | Единый размер активной области | ✅ закрыт (112×35) |
|
||||||
|
| 3 | Countdown в тесте buttons | ❌ решено не делать |
|
||||||
|
| 4 | Экспорт результатов в JSON с привязкой к UID | ⏸ отложен |
|
||||||
|
| 5 | Копирование UID с экрана терминала | ⏸ отложен |
|
||||||
|
| 6 | Проверка HIL (CAN & Opto) | ✅ закрыт, 3 бага найдены и исправлены |
|
||||||
|
|
||||||
|
Пункты 4 и 5 сохранены в памяти для следующих сессий — не начаты, ждут возврата.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Рекомендации для следующего треда
|
||||||
|
|
||||||
|
1. Перед стартом пунктов 4/5 — обсудить формат JSON-экспорта (структура файла,
|
||||||
|
куда сохраняется, привязка к UID в имени/содержимом) и способ копирования UID
|
||||||
|
(зависит от поддержки OSC 52 в целевом терминале сервисника — стоит уточнить
|
||||||
|
заранее, SSH-сессии могут не поддерживать).
|
||||||
|
2. `m5_client.py` теперь соответствует реальному протоколу `agent.py` — при любых
|
||||||
|
будущих изменениях `agent.py` (новые команды, смена формата ответа) стоит сразу
|
||||||
|
сверяться через `grep` по агенту, а не полагаться на `HIL_BENCH.md`/`HIL_HOW_TO.md`
|
||||||
|
(документация местами не успевала за кодом — минимум один пример уже был найден).
|
||||||
|
3. `LOGO_ART` в `boot_art.py` — статичный артефакт. Если лого компании поменяется,
|
||||||
|
скрипт растеризации не сохранён в репозитории (был одноразовым в песочнице) —
|
||||||
|
при необходимости регенерации нужно будет написать заново (логика описана в
|
||||||
|
разделе 4.1 этого отчёта, воспроизводима).
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Loading…
Reference in a new issue