diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 5ea3a34..6b7e74a 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.20) project( firmware_test - VERSION 0.0.1 + VERSION 0.1.1 LANGUAGES C ASM) set(TARGET_NAME firmware_test) diff --git a/tools/production/README.md b/tools/production/README.md index ec6d72e..4a7226d 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -3,412 +3,124 @@ TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. +> Внутреннее устройство, диаграммы архитектуры и заметки для разработчиков — +> в [DEV_ARCH.md](DEV_ARCH.md). Этот документ — только про то, как приложением +> пользоваться. + --- -## Структура проекта +## Экраны приложения + +### Ожидание подключения (WaitingScreen) + +Стартовый экран. Автодетект USB — как только плата подключена, TUI сам +определяет режим (прошивка или диагностика) и переключается. ```bash -tools/production/ -├── main.py ← точка входа (10 строк) -├── pyproject.toml ← зависимости uv -├── uv.lock -└── app/ - ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов - ├── 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 +┌────────────────────────────────────────────────────┐ +│ service_tool v0.3.0 │ +│ │ +│ [LOGO_ART] │ +│ │ +│ Подключите плату индикатора к USB... ⠋ │ +└────────────────────────────────────────────────────┘ ``` ---- +При потере соединения на любом другом экране сессия разрывается полностью — +TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над +подсказкой на 4 секунды появляется строка `⚠ <причина>` (например, +«⚠ Соединение с платой потеряно»), затем скрывается сама — обычный +автодетект продолжается без вмешательства оператора. -## Концепция +### Прошивка платы (FlashScreen) -```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. - ---- - -## Состояния приложения - -Состояние определяется автодетектом 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`. +Триггер: обнаружена плата в режиме BootROM SDP. ```bash -┌─ Прошивка платы ─────────────────────────────────┐ -│ ⚡ BootROM SDP обнаружен │ +┌────────────────────────────────────────────────────┐ +│ ⚡ Загрузка прошивки на плату индикатора │ +│ (режим BootROM) │ │ │ -│ Что прошить? │ -│ ◉ firmware_test (диагностическая прошивка) │ -│ ○ Production (bootloader + tft_app) │ -│ ○ Кастомный бинарь... │ +│ Выбор загружаемой прошивки │ +│ ◉ Диагностическая прошивка (firmware_test) │ +│ ○ Серийная прошивка (bootloader + tft_app) │ +│ ○ Другое │ +│ [ Имя бинарного файла в custom_binaries/ (.bin) ]│ ← только если «Другое» │ │ -│ [ ▶ Прошить ] [ ⚠ Chip Erase ] [ ✕ Выйти ] │ +│ [ ▶ Загрузить ] [ ⚠ Очистить память ] [ ✕ Выйти из приложения ] │ │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ │ │ ▶ Прошивка: firmware_test │ │ -│ │ $ blhost -u 0x15A2,0x0073 -- write-memory… │ │ +│ │ ... │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` -ProgressBar виден только во время активной операции (скрыт в простое), без числового `%` — только полоса и построчный лог в реальном времени. +Лог виден постоянно (не только во время прошивки), прогресс-бар — только +во время активной операции (скрыт в простое), без числового `%` — только +полоса и построчный лог в реальном времени. -### Промежуточный экран — PostFlashScreen +### Переход в рабочий режим (PostFlashScreen) -Показывается **только** после успешной прошивки `firmware_test` (не для Production/Custom — им этот шаг не нужен). +Показывается **только** после успешной прошивки `firmware_test` (для +Production/Custom этот шаг не нужен). ```bash -┌─ Прошивка завершена ──────────────────────────────┐ +┌────────────────────────────────────────────────────┐ │ ✅ firmware_test успешно записан │ │ │ │ Переведите плату в нормальный режим: │ │ BOOT_MOD_1 → GND → Reset │ │ │ -│ [ ✓ Готово, перешёл ] [ ✕ Выйти ] │ +│ [ ✓ Готово ] [ ✕ Выйти из приложения ] │ │ Автопереход через: 40с │ └────────────────────────────────────────────────────┘ ``` -### Режим B — Диагностика (DiagScreen) +### Диагностика (DiagScreen) -Триггер: CDC-порт `1996:00AD` виден + `ping→pong` прошёл. +Триггер: плата видна как CDC-устройство и отвечает на связь. ```bash -┌─ Диагностика fw:0.2.0 UID:A1B2C3D4E5F60011 M5: ✓ подключён ─┐ -│ Тесты │ Результаты (DataTable) │ -│ [Выбрать все][Снять все] │ Тест HIL Статус Время │ -│ ☐ SDRAM 32 MB │ microSD ✗ FAIL 0.1с │ -│ ☐ QSPI Flash │ no card detected (без обрезки) │ -│ ☐ microSD │ SDRAM 32 MB ✓ PASS 1.8с │ -│ ☐ TFT Display │ QSPI Flash ✓ PASS 0.6с │ -│ ☐ CAN loopback [HIL] │ TFT Display … running │ -│ ☐ Оптовходы [HIL] │ Кнопки pending │ -├─────────────────────────────────────────────────────────────────┤ -│ [▶ Запустить выбранные] [▶▶ Все тесты] [✕ Выйти] │ -│ ████████░░░░ Тест: usd — mount: ok │ -├─────────────────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? 28с │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└─────────────────────────────────────────────────────────────────┘ +┌────────────────────────────────────────────────────────────────┐ +│ fw: 0.2.0 MCU ID: A1B2C3D4E5F60011 M5 Bench: ✓ подключён │ +├───────────────────────────┬──────────────────────────────────────┤ +│ Доступные тесты │ Тест M5 Bench Статус Время │ +│ [Выбрать все][Снять все] │ microSD [-] ✗ FAIL 0.1с │ +│ ☐ SDRAM 32 MB │ no card detected (без обрезки) │ +│ ☐ QSPI Flash │ SDRAM 32 MB [-] ✓ PASS 1.8с │ +│ ☐ microSD │ QSPI Flash [-] ✓ PASS 0.6с │ +│ ☐ TFT Display │ TFT Display [-] … │ +│ ☐ CAN loopback [HIL] │ Кнопки [-] │ +│ ☐ Оптовходы [HIL] │ │ +├──────────────────────────────────────────────────────────────────┤ +│ ████████░░░░ Тест: usd — mount: ok ← только во время прогона │ +├──────────────────────────────────────────────────────────────────┤ +│ [▶ Запустить выбранные тесты] [▶▶ Запустить все тесты] [✕ Выйти из приложения] │ +├──────────────────────────────────────────────────────────────────┤ +│ ⚠ Экран залит красным цветом? 28с │ +│ [ ✓ Да ] [ ✗ Нет ] ← только во время confirm │ +└────────────────────────────────────────────────────────────────┘ ``` -Ключевые отличия от ранних версий TUI: -- **Тесты изначально не выбраны** — сервисник выбирает явно, либо кнопками "Выбрать все"/"Снять все" -- **Результаты — `DataTable`**, не текстовые строки: сортировка FAIL-наверх (стабильная внутри группы по порядку реестра), FAIL-строка подсвечена красным фоном целиком, длинные `detail`-сообщения переносятся на несколько строк без обрезания (явная ширина колонок, см. раздел «Известные грабли Textual») -- **`progress`-события** теста USD (`card_detect`, `mount`, `write`, `read_compare`) отображаются в прогресс-строке как текущая фаза -- HIL-тесты без M5StampPLC — серые, недоступны для выбора (постоянное состояние, не путается с временной блокировкой во время прогона) - ---- - -## Обработка 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). - ---- - -## Мониторинг соединения и разрыв сессии - -`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`). +Что важно знать: +- **Тесты изначально не выбраны** — выбирайте вручную либо кнопками "Выбрать + все"/"Снять все". +- **Результаты** — таблица с сортировкой FAIL-наверх, FAIL-строка подсвечена + целиком, длинные сообщения переносятся на несколько строк без обрезания. + Колонка `M5 Bench` отмечает `[*]`/`[-]` — требует ли тест HIL-стенд. +- **HIL-тесты** (CAN, оптовходы) без подключённого M5StampPLC — серые, + недоступны для выбора. +- **Интерактивные проверки во время прогона теста** бывают трёх видов: + - HIL-тесты (CAN, оптовходы) при наличии M5 — отвечает стенд автоматически, + без участия оператора; + - тесты кнопок — экран показывает подсказку `⌨ <инструкция>`, ответ не + требуется, нужно физически нажать кнопку на плате; + - остальное (дисплей, аудио) — форма "Да/Нет" с таймером `⚠ <вопрос>` на + экране. +- До завершения загрузки списка тестов правая колонка показывает подсказку + «Выберите тесты и нажмите «Запустить выбранные»» вместо таблицы. --- @@ -428,7 +140,7 @@ just host::service-build ```bash 1. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen -2. Выбрать firmware_test → Прошить +2. Выбрать «Диагностическая прошивка (firmware_test)» → Загрузить 3. PostFlashScreen: BOOT_MOD_1 → GND, сбросить плату 4. Нажать "Готово" (или дождаться авто-перехода через 40с) 5. TUI автоматически попадает в DiagScreen при следующем подключении @@ -438,12 +150,63 @@ just host::service-build ```bash 1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3) -2. FlashScreen → Chip Erase (~30 с) +2. FlashScreen → Очистить память (~30 с) 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` | | `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | -**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)`. +**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает +`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть +инициализирован (`just host::setup-tools`). --- @@ -479,4 +230,5 @@ tools/production/service_tui.log ← по умолчанию $SERVICE_LOG_DIR/service_tui.log ← если задан в .env ``` -Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не пишет в stdout — Textual захватывает терминал. +Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не +пишет в stdout — Textual захватывает терминал. diff --git a/tools/production/docs/DEV_ARCH.md b/tools/production/docs/DEV_ARCH.md new file mode 100644 index 0000000..821a1a7 --- /dev/null +++ b/tools/production/docs/DEV_ARCH.md @@ -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 с экрана» — отложены, не начаты. diff --git a/tools/production/docs/TUI_REPORT_1.md b/tools/production/docs/TUI_REPORT_1.md deleted file mode 100644 index ef9b6e6..0000000 --- a/tools/production/docs/TUI_REPORT_1.md +++ /dev/null @@ -1,209 +0,0 @@ -# TUI Service Tool замечания - -## Прочие замечания - -После внедрения в firmware/test/CMakeLists.txt: - -```cmake -# firmware/test/CMakeLists.txt Тестовая прошивка — входной контроль платы на -# производстве -cmake_minimum_required(VERSION 3.20) -project( - firmware_test - VERSION 0.0.1 - LANGUAGES C ASM) - -set(TARGET_NAME firmware_test) - -# Генерация version.h из шаблона -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" - "${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY) - -add_subdirectory(fatfs) - -add_executable( - ${TARGET_NAME} - src/main.c - 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_mqs.c - src/tests/test_qspi.c - src/tests/test_usd.c - src/tests/test_display.c - src/tests/test_buttons.c - ${BSP_GENERATED}/clock_config.c - ${BSP_STARTUP_FILE} - ${BSP_SYSCALLS_FILE}) - -target_include_directories(firmware_test PRIVATE src/) - -target_include_directories(firmware_test - PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated") -# __STARTUP_INITIALIZE_RAMFUNCTION - очистка секции .ram_function и копирование -# туда данных из __ram_function_flash_start; __STARTUP_INITIALIZE_NONCACHEDATA - -# инициализация некешируемое секции нулями -target_compile_definitions( - ${TARGET_NAME} - PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1 - DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8 __STARTUP_INITIALIZE_RAMFUNCTION - __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_NONCACHEDATA) -# -# ----------------------------------------------------------------------------- -# Зависимости — только то что нужно для входного контроля bsp_board транзитивно -# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны -# ----------------------------------------------------------------------------- -target_link_libraries( - ${TARGET_NAME} - PRIVATE bsp_board - bsp_can - bsp_led - bsp_button - bsp_display - bsp_tick - bsp_boot_xip - bsp_usb_cdc - bsp_provisioning - bsp_sdram - bsp_qspi_flash - bsp_uart_host - bsp_opto - bsp_mqs - bsp_sd - firmware_test_fatfs) - -# ----------------------------------------------------------------------------- -# Linker script -# ----------------------------------------------------------------------------- -# --gc-sections — удалять неиспользуемые секции (работает с -# -ffunction/data-sections) --print-memory-usage — выводить таблицу -# использования Flash/RAM после линковки -Map — генерировать -# map-файл для анализа размещения символов -T — линкерный -# скрипт с описанием карты памяти IMXRT1052 -target_link_options( - ${TARGET_NAME} - PRIVATE - -Wl,--gc-sections - -Wl,--print-memory-usage - -Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map - -Wl,--defsym=__stack_size__=0x2000 - -Wl,--defsym=__heap_size__=0x2000 - -T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld) - -set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY - ${CMAKE_BINARY_DIR}) -# ----------------------------------------------------------------------------- -# Post-build: генерация .bin для прошивки через blhost -# ----------------------------------------------------------------------------- -add_custom_command( - TARGET firmware_test - POST_BUILD - COMMAND ${CMAKE_OBJCOPY} -O binary $ - ${CMAKE_BINARY_DIR}/firmware_test.bin - COMMAND ${CMAKE_SIZE} $ - COMMENT "Generating firmware_test.bin") - -``` - -сборка начала валиться с ошибками: - -```bash -Executing task: just build::hab-firmware-test-debug - -cmake --preset Debug -Preset CMake variables: - - CMAKE_BUILD_TYPE="Debug" - CMAKE_EXPORT_COMPILE_COMMANDS="ON" - CMAKE_TOOLCHAIN_FILE:FILEPATH="/workspace/cmake/toolchain_arm.cmake" - SEGGER_RTT_ENABLED="OFF" - UNITY_TESTING_ENABLED="OFF" - --- ARM Toolchain: /opt/arm-toolchain --- Build type: Debug --- ==== Included external libraries ==== --- SEGGER RTT ❎ --- Unity ❎ --- FFF ❎ --- ==== ---------------- ==== --- Configuring done --- Generating done --- Build files have been written to: /workspace/build/Debug -cmake --build --preset firmware-test-debug -[108/108] Linking C executable firmware_test.elf -FAILED: firmware_test.elf -: && /opt/arm-toolchain/bin/arm-none-eabi-gcc -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -ffreestanding -O0 -g3 -gdwarf-4 --specs=nano.specs -Wl,--no-warn-rwx-segments -Wl,--gc-sections -Wl,--print-memory-usage -Wl,-Map=/workspace/build/Debug/firmware_test.map -Wl,--defsym=__stack_size__=0x2000 -Wl,--defsym=__heap_size__=0x2000 -T/workspace/firmware/test/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld firmware/test/CMakeFiles/firmware_test.dir/src/main.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/cli.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/protocol.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/test_runner.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_opto.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_sdram.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_can.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_mqs.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_qspi.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_usd.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_display.c.obj firmware/test/CMakeFiles/firmware_test.dir/src/tests/test_buttons.c.obj firmware/test/CMakeFiles/firmware_test.dir/__/__/bsp/generated/clock_config.c.obj firmware/test/CMakeFiles/firmware_test.dir/__/__/bsp/generated/startup/startup_MIMXRT1052.S.obj firmware/test/CMakeFiles/firmware_test.dir/__/__/bsp/generated/syscalls.c.obj -o firmware_test.elf bsp/libbsp_board.a bsp/can/libbsp_can.a bsp/led/libbsp_led.a bsp/button/libbsp_button.a bsp/display/libbsp_display.a bsp/tick/libbsp_tick.a bsp/usb_cdc/libbsp_usb_cdc.a bsp/provisioning/libbsp_provisioning.a bsp/sdram/libbsp_sdram.a bsp/qspi_flash/libbsp_qspi_flash.a bsp/uart_host/libbsp_uart_host.a bsp/opto/libbsp_opto.a bsp/mqs/libbsp_mqs.a bsp/sd/libbsp_sd.a firmware/test/fatfs/libfirmware_test_fatfs.a sdk/libsdk_flexcan.a sdk/libsdk_elcdif.a sdk/libsdk_usb_device_ehci.a sdk/libsdk_usb_phy.a sdk/libsdk_semc.a sdk/libsdk_flexspi.a utils/libutils.a bsp/tick/libbsp_tick.a sdk/libsdk_gpio.a sdk/libsdk_sai_edma.a sdk/libsdk_sai.a sdk/libsdk_edma.a sdk/libsdk_dmamux.a sdk/libsdk_pwm.a sdk/libsdk_xbara.a bsp/sd/libbsp_sd.a bsp/libbsp_sdmmc_config.a bsp/libbsp_board.a sdk/libsdk_lpuart.a sdk/libsdk_sdmmc_sd.a sdk/libsdk_osa_bm.a sdk/libsdk_usdhc.a sdk/libsdk_clock.a sdk/libsdk_common.a sdk/libsdk_cache.a sdk/libsdk_device.a && cd /workspace/build/Debug/firmware/test && /opt/arm-toolchain/bin/arm-none-eabi-objcopy -O binary /workspace/build/Debug/firmware_test.elf /workspace/build/Debug/firmware_test.bin && /opt/arm-toolchain/bin/arm-none-eabi-size /workspace/build/Debug/firmware_test.elf -/opt/arm-gnu-toolchain-13.3.rel1-aarch64-arm-none-eabi/bin/../lib/gcc/arm-none-eabi/13.3.1/../../../../arm-none-eabi/bin/ld: cannot open linker script file /workspace/firmware/test/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld: No such file or directory -collect2: error: ld returned 1 exit status -ninja: build stopped: subcommand failed. -error: Recipe `build-firmware-test-debug` failed on line 38 with exit code 1 - - * The terminal process "/bin/bash '-c', 'just build::hab-firmware-test-debug'" terminated with exit code: 1. - * Terminal will be reused by tasks, press any key to close it. -``` - -## Экран Waiting - -1. Вне зависимости от масштаба окна прямоугольник с названием программы TFT Indicator Board Service Tool всегда находится в левом верхнем углу и выглядит неуместным. Предлагаю для всех экранов для унификации ввести цветную рамку фиксированного размера 640x480/1280x1024, чтобы вне зависимости от размера окна мы всегда видели одно и то же. Если бы еще можно было запретить делать Maximize для конкретного окна - прекрасно. Мне нравится когда TUI приложение ограничено окном - рамкой. В ratatui-rust много похожих приложений. Например [binsider](https://github.com/orhun/binsider) - -## Экран Flasher - -1. progress bar с процентами выполнения: анимация работает даже в случае если ничего не выполняется -2. progress bar с процентами выполнения: при выполнении операций Cheap Erase и Прошить проценты не меняются, просто продолжается анимация. Только в конце операции появляется `done 100%` ниже progress Bar - -Надо тщательно продумать логику работы прогресс бара. Нужно запускать его только при начале операций erase/прошить, можно не показывать проценты вообще. - -3. После выполнения операции Прошить приложение сразу перезапускает экран Waiting и мы опять попадаем в то же самое меню Flasher. Если сервисник хочет провести тесты - это неудобно. Ему нужно будет с помощью Ctrl+C закрывать приложение, менять BootMode перезапускать плату и затем само приложение. Надо продумать здесь следующий механизм: если мы шьем firmware_test можно вывести экран с промптом а-ля - теперь перезапустите плату с другим режимом boot Mode, дать время секунд 40 если сервисник справится раньше - пусть жмет ОК, если он не успелприложениш просто перезапустится и в худщем случае мы опять попадем в Flasher. В общем тут надо подумать. При прошивке кастомного бинаря/production этот промпт не нужен (хотя можно оставить напоминание, что для запуска прошитого бинаря перезапустите плату с другим boot Mode. Надо в экране Flasher предусмотреть кнопку выхода из приложения. - -4. Был странный вылет из приложения (экран Flasher) когда я рандомно выделял элементы на экране приложения и кликал : - -```bash -production git:(dev) ✗ uv run python main.py -╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv/lib/python3.14/site-packages/textual/app.py:4082 in on_event │ -│ │ -│ 4079 │ │ │ │ │ │ # Shouldn't occur, since at the very least this will find the Sc │ -│ 4080 │ │ │ │ │ │ self._mouse_down_widget = None │ -│ 4081 │ │ │ │ │ -│ ❱ 4082 │ │ │ │ self.screen._forward_event(event) │ -│ 4083 │ │ │ │ │ -│ 4084 │ │ │ │ # If a MouseUp occurs at the same widget as a MouseDown, then we should │ -│ 4085 │ │ │ │ # consider it a click, and produce a Click event. │ -│ │ -│ ╭───────────────────────────────────────────────────────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ event = MouseMove(None, x=0, y=34, pointer_x=0.0, pointer_y=34.0, delta_x=-9, delta_y=-3, button=1, style=Style(bgcolor=Color('#121212', ColorType.TRUECOLOR, triplet=ColorTriplet(red=18, green=18, blue=18)))) │ │ -│ │ self = ServiceApp(title='TFT Board Service Tool', classes={'-dark-mode'}, pseudo_classes={'dark', 'focus'}) │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv/lib/python3.14/site-packages/textual/screen.py:1841 in _forward_event │ -│ │ -│ 1838 │ │ │ │ │ if select_offset is not None: │ -│ 1839 │ │ │ │ │ │ content_widget = select_widget │ -│ 1840 │ │ │ │ │ │ content_offset = select_offset │ -│ ❱ 1841 │ │ │ │ │ │ assert isinstance(content_widget.parent, Widget) │ -│ 1842 │ │ │ │ │ │ container = content_widget.parent │ -│ 1843 │ │ │ │ │ else: │ -│ 1844 │ │ │ │ │ │ content_widget = None │ -│ │ -│ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ content_offset = Offset(x=0, y=33) │ │ -│ │ content_widget = FlashScreen() │ │ -│ │ event = MouseMove(None, x=0, y=34, pointer_x=0.0, pointer_y=34.0, delta_x=-9, delta_y=-3, button=1, style=Style(bgcolor=Color('#121212', ColorType.TRUECOLOR, triplet=ColorTriplet(red=18, green=18, blue=18)))) │ │ -│ │ select_offset = Offset(x=0, y=33) │ │ -│ │ select_widget = FlashScreen() │ │ -│ │ self = FlashScreen() │ │ -│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ -╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -AssertionError -``` - -## Экран ConfirmPanel - -1. Изначально все тесты из списка можно сделать некактивными -2. Помимо версии fw давай также будем выводить в тайтле UID камня -3. В поле результаты можно сделать симпатичную таблицу с двумя столбцами: название теста/ результат -4. Аналогично экрану Flasher: прогресс бар постоянно активен, даже в режиме простоя -5. Также можно предусмотреть кнопку выхода из приложения (аналогично Flasher) \ No newline at end of file diff --git a/tools/production/docs/TUI_REPORT_2.md b/tools/production/docs/TUI_REPORT_2.md deleted file mode 100644 index 68dae8d..0000000 --- a/tools/production/docs/TUI_REPORT_2.md +++ /dev/null @@ -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. В таблице тестов в столбце Детали при неудачном выполнение теста обрезается часть информации \ No newline at end of file diff --git a/tools/production/docs/TUI_SESSION.md b/tools/production/docs/TUI_SESSION_1.md similarity index 100% rename from tools/production/docs/TUI_SESSION.md rename to tools/production/docs/TUI_SESSION_1.md diff --git a/tools/production/docs/TUI_SESSION_2.md b/tools/production/docs/TUI_SESSION_2.md new file mode 100644 index 0000000..d4f960d --- /dev/null +++ b/tools/production/docs/TUI_SESSION_2.md @@ -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 этого отчёта, воспроизводима). \ No newline at end of file diff --git a/tools/production/docs/pics/FLASH.png b/tools/production/docs/pics/FLASH.png deleted file mode 100644 index 19c5216..0000000 Binary files a/tools/production/docs/pics/FLASH.png and /dev/null differ diff --git a/tools/production/docs/pics/POST_FLASH.png b/tools/production/docs/pics/POST_FLASH.png deleted file mode 100644 index a0c5998..0000000 Binary files a/tools/production/docs/pics/POST_FLASH.png and /dev/null differ diff --git a/tools/production/docs/pics/TEST_SCREEN.png b/tools/production/docs/pics/TEST_SCREEN.png deleted file mode 100644 index 32c74c0..0000000 Binary files a/tools/production/docs/pics/TEST_SCREEN.png and /dev/null differ diff --git a/tools/production/docs/pics/WAITING.png b/tools/production/docs/pics/WAITING.png deleted file mode 100644 index f13b869..0000000 Binary files a/tools/production/docs/pics/WAITING.png and /dev/null differ diff --git a/tools/production/docs/pics/WAITING_AFTER_LOSS.png b/tools/production/docs/pics/WAITING_AFTER_LOSS.png deleted file mode 100644 index 52544ad..0000000 Binary files a/tools/production/docs/pics/WAITING_AFTER_LOSS.png and /dev/null differ