From 054bbd70f5e1058e191796b740ce949fcf54738d Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 29 Jun 2026 16:09:23 +0300 Subject: [PATCH 1/9] # firmware_test: TUI app skeleton + doc fixes --- .env.example | 2 + .vscode/settings.json | 6 + just/build.just | 2 +- just/host.just | 79 ++++-- tools/host/flash_swd.py | 45 ++-- tools/host/flash_usb.py | 345 +++++++++++++++++++----- tools/production/README.md | 286 ++++++++++++++++++++ tools/production/app/firmware_client.py | 243 +++++++++++++++++ tools/production/app/flasher.py | 233 ++++++++++++++++ tools/production/app/m5_client.py | 188 +++++++++++++ tools/production/app/models.py | 93 +++++++ tools/production/app/orchestrator.py | 345 ++++++++++++++++++++++++ tools/production/main.py | 0 tools/production/pyproject.toml | 21 ++ 14 files changed, 1768 insertions(+), 120 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tools/production/README.md create mode 100644 tools/production/app/firmware_client.py create mode 100644 tools/production/app/flasher.py create mode 100644 tools/production/app/m5_client.py create mode 100644 tools/production/app/models.py create mode 100644 tools/production/app/orchestrator.py create mode 100644 tools/production/main.py create mode 100644 tools/production/pyproject.toml diff --git a/.env.example b/.env.example index 7efa4d3..5b80699 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,8 @@ BOOTROM_VID=1fc9 BOOTROM_PID=0130 FLASHLOADER_VID=15a2 FLASHLOADER_PID=0073 +SERVICE_CDC_VID=1996 +SERVICE_CDC_PID=00ad # --- Paths --- # BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b46aab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ +"[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" + } +} diff --git a/just/build.just b/just/build.just index d2d95c7..58f2f95 100644 --- a/just/build.just +++ b/just/build.just @@ -1,6 +1,6 @@ # ============================================================================= # scripts/build.just — сборка, тесты, HAB-образы -# Выполняется ТОЛЬКО внутри devcontainer (из терминала VSCode или tasks). +# Выполняется внутри devcontainer # # Рабочая директория — корень репозитория. # ============================================================================= diff --git a/just/host.just b/just/host.just index 7b3de76..a95193a 100644 --- a/just/host.just +++ b/just/host.just @@ -11,6 +11,7 @@ _venv := if _uname =~ "MINGW|MSYS|CYGWIN" { ".venv-host-win" } else { ".venv-hos JUST_MIN := "1.36.0" UV_MIN := "0.4.0" DOCKER_MIN := "24.0.0" +PLATFORM := os() set working-directory := '..' @@ -31,14 +32,6 @@ check-deps: fail() { echo -e " ${RED}❌ ${1}${RESET}"; ERRORS=$((ERRORS+1)); } warn() { echo -e " ${YELLOW}⚠️ ${1}${RESET}"; WARNINGS=$((WARNINGS+1)); } - _uname="$(uname -s)" - case "${_uname}" in - Linux*) PLATFORM="linux" ;; - Darwin*) PLATFORM="macos" ;; - MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;; - *) PLATFORM="unknown" ;; - esac - semver_ge() { local a="$1" b="$2" local a1 a2 a3 b1 b2 b3 @@ -56,7 +49,7 @@ check-deps: install_hint() { local tool=$1 - case "${PLATFORM}" in + case "{{ PLATFORM }}" in linux) case "$tool" in just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;; @@ -100,7 +93,7 @@ check-deps: } echo "" - echo -e "${BOLD} Platform: ${PLATFORM}${RESET}" + echo -e "${BOLD} Platform: {{ PLATFORM }}${RESET}" echo "" echo -e "${BOLD} Checking host dependencies...${RESET}" echo "" @@ -109,7 +102,7 @@ check-deps: check_tool "uv" "uv" "{{ UV_MIN }}" check_tool "docker" "docker" "{{ DOCKER_MIN }}" - if [[ "${PLATFORM}" == "linux" ]]; then + if [[ "{{ PLATFORM }}" == "linux" ]]; then echo "" echo -e "${BOLD} Checking udev rules (Linux only)...${RESET}" echo "" @@ -182,22 +175,16 @@ setup-tools: bootstrap: #!/usr/bin/env bash set -euo pipefail - _uname="$(uname -s)" - case "${_uname}" in - Linux*) PLATFORM="linux" ;; - Darwin*) PLATFORM="macos" ;; - MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;; - *) PLATFORM="unknown" ;; - esac + echo "" echo " === Bootstrap: step 1/3 -- check-deps ===" just host::check-deps - if [[ "${PLATFORM}" == "linux" ]]; then + if [[ "{{ PLATFORM }}" == "linux" ]]; then echo " === Bootstrap: step 2/3 -- setup-udev ===" just host::setup-udev just host::setup-m5-udev else - echo " ℹ️ step 2/3 -- setup-udev skipped (${PLATFORM})" + echo " ℹ️ step 2/3 -- setup-udev skipped ({{ PLATFORM }})" fi echo " === Bootstrap: step 3/3 -- setup-tools ===" just host::setup-tools @@ -255,20 +242,23 @@ flash-ram project type="debug": [doc('Прошить firmware_test Debug через USB SDP')] [group('flash-sdp')] -flash-test-debug: - @just host::flash firmware_test debug +flash-test-debug: (flash "firmware_test" "debug") [doc('Прошить firmware_test Release через USB SDP')] [group('flash-sdp')] -flash-test-release: - @just host::flash firmware_test release +flash-test-release: (flash "firmware_test" "release") [confirm("Flash bootloader + app (Release)?")] [doc('Прошить bootloader + tft_app Release через USB SDP (производство)')] [group('flash-sdp')] -flash-production: - @just host::flash bootloader release - @just host::flash app release +flash-production: (flash "bootloader" "release") (flash "app" "release") + +[confirm("Стереть всю Flash (W25Q128) через USB SDP? Все прошивки будут удалены.")] +[doc('Стереть всю Flash (W25Q128) через USB SDP (chip erase, ~30 с)')] +[group('flash-sdp')] +flash-sdp-erase: + cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \ + --erase-chip # ============================================================================= # ГРУППА: flash-swd — прошивка через SWD (MCU-Link, без смены BOOT_MODE) @@ -319,7 +309,7 @@ flash-swd-erase: uv run --directory {{ HIL_DIR }} pyocd erase \ --target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \ --frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \ - --chip + --chip -O resume_on_disconnect=False # ============================================================================= # ГРУППА: hil — запуск HIL-тестов на реальном железе @@ -543,3 +533,36 @@ incoming: flash-test-release [group('pipeline')] production: flash-production @echo " ✅ Production firmware flashed" + +# ============================================================================= +# ГРУППА: service — TUI сервисного инженера +# ============================================================================= + +PRODUCTION_DIR := justfile_directory() / 'tools/production' + +[doc('Установить/обновить зависимости TUI сервисного инженера')] +[group('service')] +service-setup: + #!/usr/bin/env bash + set -euo pipefail + echo " 📦 Syncing tools/production deps..." + cd "{{ PRODUCTION_DIR }}" && uv sync + echo " ✅ tools/production deps installed" + +[doc('Запустить TUI сервисного инженера')] +[group('service')] +service-tui: + uv run --directory {{ PRODUCTION_DIR }} python main.py + +[doc('Собрать standalone-бинарь TUI (PyInstaller → dist/service_tui)')] +[group('service')] +service-build: + #!/usr/bin/env bash + set -euo pipefail + echo " 🔨 Building standalone service-tui..." + cd "{{ PRODUCTION_DIR }}" && uv run pyinstaller \ + --onefile \ + --name service_tui \ + --add-data "../shared:shared" \ + main.py + echo " ✅ dist/service_tui ready" diff --git a/tools/host/flash_swd.py b/tools/host/flash_swd.py index fdf1b82..81213ac 100644 --- a/tools/host/flash_swd.py +++ b/tools/host/flash_swd.py @@ -48,16 +48,18 @@ REPO_ROOT = Path(__file__).resolve().parents[2] # ── Конфигурация из окружения (just экспортирует .env через set export) ─────── + def _env(key: str, default: str) -> str: return os.environ.get(key, default) -PYOCD_TARGET = _env("PYOCD_TARGET", "mimxrt1050_quadspi") + +PYOCD_TARGET = _env("PYOCD_TARGET", "mimxrt1050_quadspi") PYOCD_FREQUENCY = _env("PYOCD_FREQUENCY", "4000000") -BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build"))) -FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin"))) +BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build"))) +FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin"))) if not FCB_PATH.is_absolute(): FCB_PATH = REPO_ROOT / FCB_PATH -HIL_DIR = REPO_ROOT / "tools" / "hil" +HIL_DIR = REPO_ROOT / "tools" / "hil" # IVT располагается по смещению 0x1000 от начала Flash (ivtOffset в HAB yaml) IVT_OFFSET = 0x1000 @@ -65,12 +67,13 @@ IVT_OFFSET = 0x1000 # Маппинг имён прошивок → имена HAB-файлов (генерируются build::hab-*) HAB_NAMES = { "firmware_test": "firmware_test_hab.bin", - "bootloader": "bootloader_hab.bin", - "app": "app_hab.bin", + "bootloader": "bootloader_hab.bin", + "app": "app_hab.bin", } # ── Helpers ─────────────────────────────────────────────────────────────────── + def build_full_image(fcb_path: Path, hab_path: Path) -> bytes: """Объединить FCB + padding + HAB в единый образ для записи с 0x60000000.""" fcb = fcb_path.read_bytes() @@ -82,7 +85,7 @@ def build_full_image(fcb_path: Path, hab_path: Path) -> bytes: ) padding = b"\xff" * (IVT_OFFSET - len(fcb)) # 0xFF = erased flash value - image = fcb + padding + hab + image = fcb + padding + hab print(f" FCB: {len(fcb):>6} bytes @ 0x60000000") print(f" Padding: {len(padding):>6} bytes @ 0x{0x60000000 + len(fcb):08X}") @@ -95,13 +98,20 @@ def build_full_image(fcb_path: Path, hab_path: Path) -> bytes: def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int: """Запустить pyocd flash через uv run из tools/hil.""" cmd = [ - "uv", "run", - "--directory", str(HIL_DIR), - "pyocd", "flash", - "--target", target, - "--frequency", frequency, - "--base-address", "0x60000000", - "--erase", "sector", + "uv", + "run", + "--directory", + str(HIL_DIR), + "pyocd", + "flash", + "--target", + target, + "--frequency", + frequency, + "--base-address", + "0x60000000", + "--erase", + "sector", str(image_path), ] print(f"\n Running: {' '.join(cmd)}\n") @@ -110,10 +120,11 @@ def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int: # ── Main ────────────────────────────────────────────────────────────────────── + def main() -> int: parser = argparse.ArgumentParser( description="Flash MIMXRT1052 via SWD (MCU-Link). " - "Prepends FCB to HAB image before programming.", + "Prepends FCB to HAB image before programming.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) @@ -170,7 +181,7 @@ def main() -> int: if not hab_path.exists(): print(f" ❌ HAB image not found: {hab_path}", file=sys.stderr) fw_slug = args.firmware.replace("_", "-") - bt_slug = args.build_type.lower() + bt_slug = args.build_type.lower() print( f" Hint: run just build::hab-{fw_slug}-{bt_slug} " "inside devcontainer first.", @@ -216,4 +227,4 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/tools/host/flash_usb.py b/tools/host/flash_usb.py index c0c4fc5..642aa0b 100644 --- a/tools/host/flash_usb.py +++ b/tools/host/flash_usb.py @@ -7,17 +7,25 @@ flash_usb.py — прошивка MIMXRT1052 через USB (BootROM SDP → Fla python3 flash_usb.py --firmware app --build-type Release python3 flash_usb.py --firmware bootloader --build-type Release python3 flash_usb.py --firmware firmware_test --build-type Debug --ram-only + python3 flash_usb.py --bin-path /path/to/custom.bin + python3 flash_usb.py --erase-chip -Шаги: +Шаги (прошивка): 1. Устройство в SDP режиме (VID:PID 1FC9:0130) → sdphost загружает ivt_flashloader.bin в RAM → sdphost прыгает на flashloader 2. Flashloader запущен (VID:PID 15A2:0073) → blhost конфигурирует FlexSPI NOR (пишет FCB) → blhost стирает нужный регион Flash - → blhost пишет HAB образ начиная с 0x60002000 + → blhost пишет HAB образ начиная с 0x60001000 → blhost reset +Шаги (chip erase): + 1. Загрузка Flashloader (аналогично прошивке) + 2. blhost configure-memory (инициализация FlexSPI контроллера) + 3. blhost flash-erase-all (полная очистка W25Q, ~30 с) + 4. blhost reset + Конфигурация: Переменные окружения (задаются в .env, экспортируются через just): BOOTROM_VID — VID BootROM SDP (default: 1fc9) @@ -35,38 +43,40 @@ import time from pathlib import Path # ─── Пути ───────────────────────────────────────────────────────────────────── -SCRIPT_DIR = Path(__file__).parent.resolve() -REPO_ROOT = SCRIPT_DIR.parent.parent +SCRIPT_DIR = Path(__file__).parent.resolve() +REPO_ROOT = SCRIPT_DIR.parent.parent FLASHLOADER = SCRIPT_DIR / "dcd" / "ivt_flashloader.bin" -# BUILD_DIR: берём из окружения (just экспортирует из .env как абсолютный путь), -# fallback — рассчитываем от расположения скрипта BUILD_DIR = Path(os.environ.get("BUILD_DIR", str(REPO_ROOT / "build"))) -# ─── USB VID:PID — из окружения (.env → just set export → uv run) ───────────── +# ─── USB VID:PID ────────────────────────────────────────────────────────────── + + def _usb(vid_key: str, vid_default: str, pid_key: str, pid_default: str) -> str: - vid = os.environ.get(vid_key, vid_default).strip().upper().lstrip("0X") pid = os.environ.get(pid_key, pid_default).strip().upper().lstrip("0X") return f"0x{vid},0x{pid}" -SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130") # BootROM SDP -BLHOST_USB = _usb("FLASHLOADER_VID","15a2", "FLASHLOADER_PID","0073") # Flashloader -# ─── Аппаратные константы (часть логики прошивки, не конфигурация) ──────────── +SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130") +BLHOST_USB = _usb("FLASHLOADER_VID", "15a2", "FLASHLOADER_PID", "0073") + +# ─── Аппаратные константы ───────────────────────────────────────────────────── # FlexSPI NOR config option word: 0xC0000007 # bits[31:28]=0xC — tag (QuadSPI NOR) # bits[3:0]=0x7 — option size -FLEXSPI_OPTION_ADDR = "0x2000" +FLEXSPI_OPTION_ADDR = "0x2000" FLEXSPI_OPTION_VALUE = "0xC0000007" -FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID +FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID # Option word для записи FCB: tag=0xF → Write FCB command -FLEXSPI_FCB_VALUE = "0xF000000F" +FLEXSPI_FCB_VALUE = "0xF000000F" # Flash layout -FLASH_BASE = 0x60000000 -HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET +FLASH_BASE = 0x60000000 +HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET + +# ─── Helpers ────────────────────────────────────────────────────────────────── def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: @@ -79,22 +89,22 @@ def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: def step(msg: str) -> None: - print(f"\n{'─'*60}") + print(f"\n{'─' * 60}") print(f" {msg}") - print(f"{'─'*60}") + print(f"{'─' * 60}") def wait_for_flashloader(timeout: int = 10) -> bool: - """Ждём пока Flashloader поднимется""" + """Ждём пока Flashloader поднимется (опрашиваем blhost раз в секунду).""" print(f"\n Ожидание Flashloader (до {timeout}с)...", end="", flush=True) for i in range(timeout): time.sleep(1) result = subprocess.run( ["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"], - capture_output=True + capture_output=True, ) if result.returncode == 0: - print(f" OK ({i+1}с)") + print(f" OK ({i + 1}с)") return True print(".", end="", flush=True) print(" TIMEOUT") @@ -102,67 +112,147 @@ def wait_for_flashloader(timeout: int = 10) -> bool: def load_flashloader() -> None: - """Загружает Flashloader через SDP если ещё не запущен""" + """Загружает Flashloader через SDP если ещё не запущен.""" result = subprocess.run( ["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"], - capture_output=True + capture_output=True, ) if result.returncode == 0: print(" Flashloader уже запущен — пропускаем загрузку") return if not FLASHLOADER.exists(): - print(f"[ERROR] Не найден: {FLASHLOADER}") - print(" Скачай ivt_flashloader.bin из MCUXpresso Secure Provisioning Tool") - print(" и положи в tools/host/dcd/ivt_flashloader.bin") + print(f"[ERROR] Не найден: {FLASHLOADER}", file=sys.stderr) + print( + " Скачай ivt_flashloader.bin из MCUXpresso Secure Provisioning Tool\n" + " и положи в tools/host/dcd/ivt_flashloader.bin", + file=sys.stderr, + ) sys.exit(1) step("Загрузка Flashloader через SDP (1FC9:0130)") - run(["sdphost", "-u", SDP_USB, "-j", "--", - "write-file", "0x20001C00", str(FLASHLOADER)]) - run(["sdphost", "-u", SDP_USB, "-j", "--", - "jump-address", "0x20001C00"]) + run( + [ + "sdphost", + "-u", + SDP_USB, + "-j", + "--", + "write-file", + "0x20001C00", + str(FLASHLOADER), + ] + ) + run(["sdphost", "-u", SDP_USB, "-j", "--", "jump-address", "0x20001C00"]) if not wait_for_flashloader(): - print("[ERROR] Flashloader не ответил. Проверь BOOT_MOD пины и подключение.") + print( + "[ERROR] Flashloader не ответил. Проверь BOOT_MOD пины и подключение.", + file=sys.stderr, + ) sys.exit(1) def configure_flexspi() -> None: - """Инициализирует FlexSPI NOR контроллер через Flashloader""" + """Инициализирует FlexSPI NOR контроллер через Flashloader.""" step("Конфигурация FlexSPI NOR (инициализация контроллера)") - run(["blhost", "-u", BLHOST_USB, "--", - "fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_OPTION_VALUE, "word"]) - run(["blhost", "-u", BLHOST_USB, "--", - "configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR]) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "fill-memory", + FLEXSPI_OPTION_ADDR, + "4", + FLEXSPI_OPTION_VALUE, + "word", + ] + ) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "configure-memory", + FLEXSPI_MEMORY_ID, + FLEXSPI_OPTION_ADDR, + ] + ) def write_fcb() -> None: - """Записывает Flash Configuration Block в 0x60000000 + """Записывает Flash Configuration Block в 0x60000000. - Отдельный шаг после erase! Flashloader генерирует FCB из параметров FlexSPI + Отдельный шаг после erase: Flashloader генерирует FCB из параметров FlexSPI и пишет его по адресу 0x60000000. Без FCB BootROM не знает как читать Flash. Option word 0xF000000F: tag=0xF → Write FCB command. """ step("Запись FCB в Flash[0x60000000]") - run(["blhost", "-u", BLHOST_USB, "--", - "fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_FCB_VALUE, "word"]) - run(["blhost", "-u", BLHOST_USB, "--", - "configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR]) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "fill-memory", + FLEXSPI_OPTION_ADDR, + "4", + FLEXSPI_FCB_VALUE, + "word", + ] + ) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "configure-memory", + FLEXSPI_MEMORY_ID, + FLEXSPI_OPTION_ADDR, + ] + ) + + +# ─── Основные операции ──────────────────────────────────────────────────────── def flash(hab_bin: Path, ram_only: bool = False) -> None: + """Прошить HAB-образ в Flash или загрузить в RAM.""" if not hab_bin.exists(): - print(f"[ERROR] Файл не найден: {hab_bin}") - print(" Сначала собери образ: uv run nxpimage hab export -c ...") + print(f"[ERROR] Файл не найден: {hab_bin}", file=sys.stderr) + print( + " Сначала собери образ: uv run nxpimage hab export -c ...", file=sys.stderr + ) sys.exit(1) if ram_only: step(f"Загрузка в RAM (без записи во Flash): {hab_bin.name}") - run(["sdphost", "-u", SDP_USB, "-j", "--", - "write-file", f"0x{FLASH_BASE + HAB_OFFSET:08X}", str(hab_bin)]) - run(["sdphost", "-u", SDP_USB, "-j", "--", - "jump-address", f"0x{FLASH_BASE + HAB_OFFSET:08X}"]) + run( + [ + "sdphost", + "-u", + SDP_USB, + "-j", + "--", + "write-file", + f"0x{FLASH_BASE + HAB_OFFSET:08X}", + str(hab_bin), + ] + ) + run( + [ + "sdphost", + "-u", + SDP_USB, + "-j", + "--", + "jump-address", + f"0x{FLASH_BASE + HAB_OFFSET:08X}", + ] + ) return load_flashloader() @@ -177,47 +267,154 @@ def flash(hab_bin: Path, ram_only: bool = False) -> None: print(f" Адрес: {write_addr}") print(f" Стирание: 0x{FLASH_BASE:08X} .. +{erase_size} байт") - run(["blhost", "-u", BLHOST_USB, "--", - "flash-erase-region", f"0x{FLASH_BASE:08X}", - str(erase_size), "0"]) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "flash-erase-region", + f"0x{FLASH_BASE:08X}", + str(erase_size), + "0", + ] + ) write_fcb() - run(["blhost", "-u", BLHOST_USB, "--", - "write-memory", write_addr, str(hab_bin), "0"]) + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "write-memory", + write_addr, + str(hab_bin), + "0", + ] + ) step("Reset") run(["blhost", "-u", BLHOST_USB, "--", "reset"]) - print("\n ✓ Прошивка завершена успешно") + print("\n ✅ Прошивка завершена успешно") + + +def erase_chip() -> None: + """Полная очистка Flash (chip erase) через Flashloader. + + Использует blhost flash-erase-all (memory ID 9 = FlexSPI NOR). + Время операции: ~30 с для W25Q128. + После erase FCB также стёрт — BootROM не сможет загрузить прошивку + до следующей прошивки (flash_usb.py запишет FCB автоматически). + """ + load_flashloader() + configure_flexspi() + + step("Полная очистка Flash (chip erase, ~30 с)") + print(" ⚠️ После chip erase BootROM не сможет загрузить прошивку.") + print(" ⚠️ Используй flash_usb.py для восстановления.\n") + run( + [ + "blhost", + "-t", + "100000", + "-u", + BLHOST_USB, + "--", + "flash-erase-all", + FLEXSPI_MEMORY_ID, + ] + ) + + step("Reset") + run(["blhost", "-u", BLHOST_USB, "--", "reset"]) + print("\n ✅ Chip erase завершён") + + +# ─── CLI ────────────────────────────────────────────────────────────────────── def main() -> None: - print(f"DEBUG SDP_USB = {repr(SDP_USB)}") - print(f"DEBUG BLHOST_USB = {repr(BLHOST_USB)}") - print(f"DEBUG BUILD_DIR = {repr(BUILD_DIR)}") - parser = argparse.ArgumentParser(description="Прошивка MIMXRT1052 через USB") - parser.add_argument("--firmware", required=True, - choices=["firmware_test", "bootloader", "app"], - help="Имя прошивки") - parser.add_argument("--build-type", required=True, - choices=["Debug", "Release"], - help="Тип сборки") - parser.add_argument("--ram-only", action="store_true", - help="Загрузить в RAM без записи во Flash") + parser = argparse.ArgumentParser( + description="Прошивка/очистка MIMXRT1052 через USB SDP", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + # Группа: что прошивать (взаимоисключающие варианты) + target_group = parser.add_mutually_exclusive_group() + target_group.add_argument( + "--firmware", + choices=["firmware_test", "bootloader", "app"], + help="Стандартная прошивка из BUILD_DIR (требует --build-type)", + ) + target_group.add_argument( + "--bin-path", + type=Path, + metavar="PATH", + help="Путь к произвольному HAB-бинарю (.bin) для прошивки", + ) + target_group.add_argument( + "--erase-chip", + action="store_true", + help="Полная очистка Flash (chip erase, ~30 с). Прошивка не выполняется.", + ) + + parser.add_argument( + "--build-type", + choices=["Debug", "Release"], + default="Release", + help="Тип сборки (только для --firmware, default: Release)", + ) + parser.add_argument( + "--ram-only", + action="store_true", + help="Загрузить в RAM без записи во Flash (только для --firmware / --bin-path)", + ) + args = parser.parse_args() - hab_bin = BUILD_DIR / args.build_type / f"{args.firmware}_hab.bin" + # Валидация: --firmware требует --build-type (уже есть default, но запомним) + # --bin-path: build-type игнорируется + # --erase-chip: несовместим с --ram-only + if args.erase_chip and args.ram_only: + parser.error("--erase-chip несовместим с --ram-only") - print(f"\n{'═'*60}") - print(f" MIMXRT1052 Flash Tool") - print(f" Прошивка: {args.firmware} [{args.build_type}]") - print(f" Образ: {hab_bin}") + if args.firmware is None and args.bin_path is None and not args.erase_chip: + parser.error("Укажи --firmware, --bin-path или --erase-chip") + + # ── Определить hab_bin ────────────────────────────────────────────────── + hab_bin: Path | None = None + + if args.firmware is not None: + hab_bin = BUILD_DIR / args.build_type / f"{args.firmware}_hab.bin" + elif args.bin_path is not None: + hab_bin = args.bin_path.resolve() + + # ── Шапка ────────────────────────────────────────────────────────────── + print(f"\n{'═' * 60}") + print(" MIMXRT1052 Flash Tool (USB SDP)") + if args.erase_chip: + print(" Операция: chip erase") + elif hab_bin is not None: + label = ( + f"{args.firmware} [{args.build_type}]" if args.firmware else str(hab_bin) + ) + print(f" Прошивка: {label}") + print(f" Образ: {hab_bin}") + if args.ram_only: + print(" Режим: RAM only (без записи во Flash)") print(f" SDP USB: {SDP_USB}") print(f" BL USB: {BLHOST_USB}") - print(f"{'═'*60}") + print(f"{'═' * 60}\n") - flash(hab_bin, ram_only=args.ram_only) + # ── Выполнить операцию ────────────────────────────────────────────────── + if args.erase_chip: + erase_chip() + elif hab_bin is not None: + flash(hab_bin, ram_only=args.ram_only) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tools/production/README.md b/tools/production/README.md new file mode 100644 index 0000000..9bcd2e2 --- /dev/null +++ b/tools/production/README.md @@ -0,0 +1,286 @@ +# service-tui — TUI сервисного инженера + +TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. +Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. + +--- + +## Концепция + +```mermaid +graph LR + subgraph PC["Сервисный ПК"] + TUI["service-tui\n(Textual App)"] + subgraph tools["tools/"] + FC["firmware_client.py\nUSB CDC ACM"] + M5["m5_client.py\nSerial JSON-lines"] + FL["flasher.py\n→ tools/host/flash_usb.py"] + end + TUI --> FC + TUI --> M5 + TUI --> FL + end + + subgraph Board["Плата TFT (MIMXRT1052)"] + FW["firmware_test\n(USB CDC)"] + ROM["BootROM\n(SDP 1FC9:0130)"] + end + + subgraph HIL["HIL стенд (опционально)"] + M5HW["M5StampPLC\nRLY1–4 + CAN"] + end + + FC <-->|"JSON-lines\nVID:PID 1996:00AD"| FW + FL -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM + M5 <-->|"JSON-lines\nSerial"| M5HW + M5HW -->|"RLY1–4"| Board +``` + +--- + +## Два режима работы + +Режим определяется автодетектом USB при старте и меняется динамически. + +```mermaid +stateDiagram-v2 + [*] --> WAITING : запуск TUI + + WAITING --> FLASHING : обнаружен VID:PID 1FC9:0130\n(BootROM SDP) + WAITING --> DIAGNOSING : обнаружен VID:PID 1996:00AD\n+ session_start по CDC + + FLASHING --> WAITING : прошивка завершена / плата отключена + FLASHING --> DIAGNOSING: плата перезагружена в нормальный режим + + DIAGNOSING --> WAITING : плата отключена + DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка) +``` + +### Режим A — Прошивка + +Триггер: обнаружен BootROM SDP (`1FC9:0130`). + +```bash +┌─ Прошивка платы ─────────────────────────────────┐ +│ Обнаружен BootROM (SDP режим) │ +│ │ +│ Что прошить? │ +│ ◉ firmware_test (диагностика) │ +│ ○ Production (bootloader + tft_app) │ +│ │ +│ ████████████░░░░░░ 64% Запись во Flash... │ +└────────────────────────────────────────────────────┘ +``` + +### Режим B — Диагностика + +Триггер: CDC-порт `1996:00AD` обнаружен и `ping→pong` прошёл. + +``` +┌─ Диагностика fw:0.1.4 UID:A1B2C3D4E5F60011 ─────┐ +│ M5StampPLC: ✓ │ Результаты: │ +├───────────────────────────┤ sdram ✓ PASS │ +│ ☑ SDRAM 32 MB │ qspi ✓ PASS │ +│ ☑ QSPI Flash │ usd ✗ FAIL │ +│ ☑ microSD │ mount failed: 5 │ +│ ☑ TFT Display │ display ✓ PASS │ +│ ☑ Кнопки │ buttons ✓ PASS │ +│ ☑ MQS Audio │ mqs ✓ PASS │ +│ ☑ CAN loopback [HIL] │ can … running │ +│ ☑ Оптовходы [HIL] │ opto ○ pending │ +├───────────────────────────┴─────────────────────────┤ +│ [ Запустить выбранные ] [ Все тесты ] │ +│ ████████████████░░░░ 80% Тест: can │ +├─────────────────────────────────────────────────────┤ +│ ⚠ Экран залит красным цветом? │ +│ [ ✓ Да ] [ ✗ Нет ] │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## Архитектура приложения + +```mermaid +graph TB + subgraph TUI["tui.py — Textual App"] + WS["WaitingScreen"] + FS["FlashScreen"] + DS["DiagScreen"] + end + + subgraph Core["app/"] + OR["orchestrator.py\nмаршрутизация confirm_request"] + FW["firmware_client.py\nasync CDC клиент"] + M5["m5_client.py\nasync M5 клиент"] + FL["flasher.py\nsubprocess flash_usb.py"] + MD["models.py\nTestInfo · TestResult\nSessionState · ConfirmRequest"] + end + + DS --> OR + OR --> FW + OR --> M5 + FS --> FL + DS --> MD + OR --> MD +``` + +--- + +## Обработка confirm_request + +Маршрутизация определяется по `id` поля `confirm_request`: + +```mermaid +flowchart TD + CR["confirm_request\nот firmware_test"] + + CR --> R{confirm_request.id} + + R -->|"opto_*"| HIL_OPTO["HIL: M5 relay_set\n→ settle → confirm"] + R -->|"can_rx_ready"| HIL_CAN_RX["HIL: M5 can_send\n→ confirm"] + R -->|"can_tx_verify"| HIL_CAN_TX["HIL: M5 can_recv\n→ verify → confirm"] + R -->|"btn*"| BTN["показать инструкцию\nне отправлять confirm\nждать test_result"] + R -->|"всё остальное"| OP["показать оператору\nprompt + OK/FAIL\n+ countdown"] + + HIL_OPTO --> AUTO["CONFIRM_RESOLVED\n(автоматически)"] + HIL_CAN_RX --> AUTO + HIL_CAN_TX --> AUTO + OP --> WAIT["CONFIRM_NEEDED\nждём resolve_operator_confirm()"] + WAIT --> SEND["send_confirm(id, confirmed)"] + AUTO --> SEND +``` + +| confirm id | Кто отвечает | Действие TUI | +| --------------- | ------------ | -------------------------------------- | +| `opto_*` | M5 авто | показать прогресс | +| `can_rx_ready` | M5 авто | показать прогресс | +| `can_tx_verify` | M5 авто | показать прогресс | +| `btn*` | физика | показать инструкцию, ждать test_result | +| всё остальное | оператор | prompt + OK/FAIL + countdown | + +--- + +## Структура + +```bash +tools/production/ +├── pyproject.toml ← зависимости: textual, pyserial, python-dotenv, pyinstaller +├── uv.lock +├── main.py ← точка входа: asyncio + Textual App +├── app/ +│ ├── tui.py ← Textual App, экраны (WaitingScreen, FlashScreen, DiagScreen) +│ ├── firmware_client.py ← async USB CDC клиент firmware_test +│ ├── m5_client.py ← async M5StampPLC клиент +│ ├── flasher.py ← subprocess → tools/host/flash_usb.py +│ ├── orchestrator.py ← confirm_request маршрутизатор +│ └── models.py ← AppMode, TestInfo, TestResult, SessionState, … +└── README.md ← этот файл +``` + +--- + +## Конфигурация (`.env`) + +Файл `.env` в корне репозитория — единый источник конфигурации. + +```ini +# USB VID:PID — BootROM SDP (менять нельзя, NXP ROM) +BOOTROM_VID=1fc9 +BOOTROM_PID=0130 + +# USB VID:PID — firmware_test CDC (наше устройство) +SERVICE_CDC_VID=1996 +SERVICE_CDC_PID=00ad + +# Пути к бинарям (опционально, TUI ищет в build/ автоматически) +FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin +PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin +PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin +``` + +--- + +## Запуск + +### Из монорепозитория (разработчик) + +```bash +# Установить зависимости +just host::service-setup + +# Запустить TUI +just host::service-tui +``` + +### Standalone-бинарь (сервисник) + +Скачать `service_tui` из [GitHub Releases](https://github.com/OSabuser/tft_manufacture_test/releases) и запустить двойным кликом. +Для сборки из исходников: + +```bash +just host::service-build +# → tools/production/dist/service_tui +``` + +--- + +## Рабочий процесс сервисника + +```mermaid +sequenceDiagram + participant OP as Оператор + participant TUI as service-tui + participant FW as firmware_test (CDC) + participant M5 as M5StampPLC + + OP->>TUI: запустить service_tui + OP->>TUI: подключить плату USB (нормальный режим) + TUI->>FW: ping → pong (CDC автодетект) + TUI->>FW: list_tests + FW-->>TUI: TestInfo × 8 + TUI-->>OP: показать список тестов + + OP->>TUI: выбрать тесты → Запустить + TUI->>FW: run_selected([...]) + + loop Для каждого теста + FW-->>TUI: test_begin + TUI-->>OP: прогресс + + alt HIL тест (opto/can) + FW-->>TUI: confirm_request + TUI->>M5: relay_set / can_send + TUI->>FW: confirm(true) + else Интерактивный (display/mqs) + FW-->>TUI: confirm_request + TUI-->>OP: показать prompt + countdown + OP->>TUI: OK / FAIL + TUI->>FW: confirm(true/false) + else Кнопки + FW-->>TUI: confirm_request (инструкция) + TUI-->>OP: "Нажмите кнопку..." + OP->>FW: физическое нажатие + end + + FW-->>TUI: test_result + TUI-->>OP: результат теста + end + + FW-->>TUI: summary + TUI-->>OP: итог: PASS / FAIL +``` + +--- + +## Зависимости + +| Пакет | Версия | Назначение | +| --------------- | ------ | ------------------------ | +| `textual` | ≥ 0.80 | TUI фреймворк | +| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | +| `python-dotenv` | ≥ 1.0 | загрузка `.env` | +| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | + +**Runtime зависимость (не в pyproject.toml):** +`tools/host/flash_usb.py` вызывается через `subprocess` с `uv run` — `tools/host/` uv-проект должен быть инициализирован (`just host::setup-tools`). diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py new file mode 100644 index 0000000..85e7b87 --- /dev/null +++ b/tools/production/app/firmware_client.py @@ -0,0 +1,243 @@ +""" +firmware_client.py — async CDC-клиент firmware_test. + +Транспорт: USB CDC ACM (pyserial в asyncio thread executor). +Протокол: JSON-lines v2 (один JSON-объект на строку, завершается '\\n'). + +Публичный API: + FirmwareClient.connect() — открыть порт, проверить ping→pong + FirmwareClient.disconnect() — закрыть порт + FirmwareClient.ping() — ping → pong, вернуть True/False + FirmwareClient.list_tests() — list_tests → list[TestInfo] + FirmwareClient.run_selected() — запустить тесты, вернуть AsyncGenerator событий + FirmwareClient.send_confirm() — отправить confirm + FirmwareClient.get_uid() — get_uid → str (hex UID) + FirmwareClient.send_cmd_raw() — отправить произвольную JSON-команду + +Все blocking-операции с pyserial выполняются в asyncio.get_event_loop().run_in_executor() +чтобы не блокировать event loop Textual. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +from typing import AsyncGenerator, Optional + +import serial +import serial.tools.list_ports + +from .models import ConfirmRequest, TestInfo, TestResult, TestStatus + +logger = logging.getLogger(__name__) + +# Таймаут чтения одной строки (сек) +_READLINE_TIMEOUT_S = 0.1 +# Таймаут ping→pong при подключении (сек) +_PING_TIMEOUT_S = 5.0 +# Таймаут ожидания событий теста (сек) — длиннее самого долгого теста (SDRAM ~15 с) +_TEST_EVENT_TIMEOUT_S = 120.0 + + +def _find_cdc_port(vid: int, pid: int) -> Optional[str]: + """Найти первый CDC-порт с заданным VID/PID.""" + for info in serial.tools.list_ports.comports(): + if info.vid == vid and info.pid == pid: + return info.device + return None + + +def _parse_event(line: str) -> Optional[dict]: + """Распарсить JSON-строку. Вернуть None при ошибке.""" + line = line.strip() + if not line: + return None + try: + return json.loads(line) + except json.JSONDecodeError: + logger.warning("Bad JSON from firmware: %r", line) + return None + + +def _parse_test_status(s: str) -> TestStatus: + return { + "pass": TestStatus.PASS, + "fail": TestStatus.FAIL, + "skip": TestStatus.SKIP, + }.get(s, TestStatus.FAIL) + + +class FirmwareClient: + """ + Async-клиент для общения с firmware_test по USB CDC ACM. + + Пример использования:: + + client = FirmwareClient(port="/dev/ttyACM0", baudrate=115200) + await client.connect() + tests = await client.list_tests() + async for event in client.run_selected([t.id for t in tests]): + ... + await client.disconnect() + """ + + def __init__(self, port: str, baudrate: int = 115200) -> None: + self._port = port + self._baudrate = baudrate + self._ser: Optional[serial.Serial] = None + self._lock = asyncio.Lock() + + # ── Connection ────────────────────────────────────────────────────────── + + async def connect(self) -> None: + """Открыть порт и проверить связь через ping→pong.""" + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._open) + ok = await self.ping() + if not ok: + await self.disconnect() + raise ConnectionError(f"firmware_test не отвечает на ping: {self._port}") + + def _open(self) -> None: + self._ser = serial.Serial( + port=self._port, + baudrate=self._baudrate, + timeout=_READLINE_TIMEOUT_S, + ) + # сбросить входной буфер — session_start уже ушёл при старте прошивки + self._ser.reset_input_buffer() + + async def disconnect(self) -> None: + """Закрыть порт.""" + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._close) + + def _close(self) -> None: + if self._ser and self._ser.is_open: + self._ser.close() + self._ser = None + + @classmethod + async def auto_connect(cls, vid: int, pid: int, baudrate: int = 115200) -> "FirmwareClient": + """ + Найти CDC-порт по VID/PID и подключиться. + + :raises RuntimeError: если порт не найден. + :raises ConnectionError: если ping не прошёл. + """ + port = _find_cdc_port(vid, pid) + if port is None: + raise RuntimeError(f"CDC-порт с VID={vid:04X}:PID={pid:04X} не найден") + client = cls(port=port, baudrate=baudrate) + await client.connect() + return client + + # ── Low-level I/O ─────────────────────────────────────────────────────── + + def _write_line(self, obj: dict) -> None: + """Сериализовать dict в JSON и отправить строку (blocking).""" + assert self._ser is not None + line = json.dumps(obj, separators=(",", ":")) + "\n" + self._ser.write(line.encode("ascii")) + self._ser.flush() + + def _read_line(self) -> Optional[dict]: + """Прочитать одну строку и распарсить (blocking, таймаут _READLINE_TIMEOUT_S).""" + assert self._ser is not None + raw = self._ser.readline() + if not raw: + return None + return _parse_event(raw.decode("ascii", errors="replace")) + + async def _send(self, obj: dict) -> None: + """Отправить JSON-команду (async wrapper).""" + loop = asyncio.get_event_loop() + async with self._lock: + await loop.run_in_executor(None, self._write_line, obj) + + async def _recv_until( + self, + stop_types: set[str], + timeout_s: float = _TEST_EVENT_TIMEOUT_S, + ) -> AsyncGenerator[dict, None]: + """ + Читать события до получения одного из stop_types или таймаута. + Генератор — yield каждого полученного события. + """ + loop = asyncio.get_event_loop() + deadline = loop.time() + timeout_s + while loop.time() < deadline: + event = await loop.run_in_executor(None, self._read_line) + if event is None: + await asyncio.sleep(0) + continue + yield event + if event.get("type") in stop_types: + return + logger.warning("_recv_until timeout after %.1f s", timeout_s) + + # ── Public API ────────────────────────────────────────────────────────── + + async def ping(self) -> bool: + """Отправить ping, ждать pong. Вернуть True при успехе.""" + await self._send({"type": "cmd", "cmd": "ping"}) + loop = asyncio.get_event_loop() + deadline = loop.time() + _PING_TIMEOUT_S + while loop.time() < deadline: + event = await loop.run_in_executor(None, self._read_line) + if event and event.get("type") == "pong": + return True + await asyncio.sleep(0) + return False + + async def list_tests(self) -> list[TestInfo]: + """Запросить список тестов. Вернуть list[TestInfo].""" + await self._send({"type": "cmd", "cmd": "list_tests"}) + async for event in self._recv_until({"test_list"}, timeout_s=5.0): + if event.get("type") == "test_list": + return [ + TestInfo( + id=t["id"], + name=t["name"], + critical=t.get("critical", False), + requires_hil=t.get("requires_hil", False), + ) + for t in event.get("tests", []) + ] + return [] + + async def run_selected( + self, test_ids: list[str] + ) -> AsyncGenerator[dict, None]: + """ + Запустить выбранные тесты. Возвращает async generator событий: + test_begin, test_result, confirm_request, summary, error. + + Вызывающий код должен обрабатывать confirm_request и вызывать + send_confirm() не дожидаясь следующего события. + """ + await self._send({"type": "cmd", "cmd": "run_selected", "tests": test_ids}) + async for event in self._recv_until({"summary"}, timeout_s=_TEST_EVENT_TIMEOUT_S): + yield event + + async def send_confirm(self, confirm_id: str, confirmed: bool) -> None: + """Отправить confirm в ответ на confirm_request.""" + await self._send({"type": "confirm", "id": confirm_id, "confirmed": confirmed}) + + async def get_uid(self) -> str: + """Запросить OCOTP UID. Вернуть hex-строку (16 символов) или "".""" + await self._send({"type": "cmd", "cmd": "get_uid"}) + async for event in self._recv_until({"uid_response"}, timeout_s=3.0): + if event.get("type") == "uid_response": + return event.get("uid", "") + return "" + + async def send_cmd_raw(self, cmd: dict) -> None: + """Отправить произвольную команду (для отладки/расширения).""" + await self._send(cmd) + + @staticmethod + def find_port(vid: int, pid: int) -> Optional[str]: + """Найти CDC-порт по VID/PID. None если не найден.""" + return _find_cdc_port(vid, pid) \ No newline at end of file diff --git a/tools/production/app/flasher.py b/tools/production/app/flasher.py new file mode 100644 index 0000000..e4d4e37 --- /dev/null +++ b/tools/production/app/flasher.py @@ -0,0 +1,233 @@ +""" +flasher.py — обёртка над tools/host/flash_usb.py для TUI. + +Запускает flash_usb.py как subprocess, парсит stdout для прогресса, +пробрасывает события через asyncio.Queue в TUI. + +Не дублирует spsdk-окружение — переиспользует tools/host/ uv-проект. + +Публичный API: + Flasher.flash(target, bin_path, progress_cb) — async, прогресс через callback + Flasher.detect_sdp() — проверить наличие BootROM SDP + Flasher.detect_cdc() — проверить наличие CDC firmware_test +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import re +from pathlib import Path +from typing import Awaitable, Callable, Optional + +from .models import FlashProgress, FlashTarget + +logger = logging.getLogger(__name__) + +# VID/PID констант — читаются из env, fallback на известные значения +_BOOTROM_VID = int(os.environ.get("BOOTROM_VID", "0x1fc9"), 16) +_BOOTROM_PID = int(os.environ.get("BOOTROM_PID", "0x0130"), 16) +_CDC_VID = int(os.environ.get("SERVICE_CDC_VID", "0x1996"), 16) +_CDC_PID = int(os.environ.get("SERVICE_CDC_PID", "0x00ad"), 16) + +# Путь до flash_usb.py относительно корня репозитория +_FLASH_USB_SCRIPT = Path(__file__).parents[3] / "tools" / "host" / "flash_usb.py" +_HOST_TOOLS_DIR = _FLASH_USB_SCRIPT.parent + + +# Паттерны stdout flash_usb.py для извлечения прогресса +_RE_PERCENT = re.compile(r"(\d{1,3})\s*%") +_RE_PHASE = re.compile(r"(sdphost|blhost|Writing|Erasing|Verifying)", re.IGNORECASE) + + +ProgressCallback = Callable[[FlashProgress], Awaitable[None]] + + +def _detect_usb(vid: int, pid: int) -> bool: + """Проверить наличие USB-устройства по VID/PID (синхронно).""" + try: + import serial.tools.list_ports + + for info in serial.tools.list_ports.comports(): + if info.vid == vid and info.pid == pid: + return True + except Exception: + pass + return False + + +class Flasher: + """ + Async-обёртка над flash_usb.py. + + Пример:: + + flasher = Flasher() + await flasher.flash( + target=FlashTarget.FIRMWARE_TEST, + progress_cb=lambda p: print(p.message), + ) + """ + + def __init__(self) -> None: + self._proc: Optional[asyncio.subprocess.Process] = None + + # ── Detection ─────────────────────────────────────────────────────────── + + @staticmethod + def detect_sdp() -> bool: + """True если виден BootROM SDP (1FC9:0130).""" + return _detect_usb(_BOOTROM_VID, _BOOTROM_PID) + + @staticmethod + def detect_cdc() -> bool: + """True если виден CDC firmware_test (1996:00AD).""" + return _detect_usb(_CDC_VID, _CDC_PID) + + # ── Erase ──────────────────────────────────────────────────────────────── + + async def erase_chip( + self, + progress_cb: Optional[ProgressCallback] = None, + ) -> bool: + """ + Chip erase Flash через USB SDP (flash_usb.py --erase-chip). + Занимает ~30 с для W25Q128. FCB будет стёрт. + + :return: True при успехе. + """ + cmd = [ + "uv", + "run", + "--directory", + str(_HOST_TOOLS_DIR), + "python", + str(_FLASH_USB_SCRIPT), + "--erase-chip", + ] + return await self._run_cmd(cmd, "erase", progress_cb) + + # ── Flash ──────────────────────────────────────────────────────────────── + + async def flash( + self, + target: FlashTarget, + progress_cb: Optional[ProgressCallback] = None, + bin_path: Optional[Path] = None, + ) -> bool: + """ + Запустить прошивку через flash_usb.py. + + :param target: Что прошиваем (firmware_test или production). + :param progress_cb: Async callback с FlashProgress (может быть None). + :param bin_path: Путь к бинарю (опционально, если overriding default). + :return: True при успехе. + """ + if target == FlashTarget.FIRMWARE_TEST: + if bin_path is not None: + return await self._run_flash_bin(bin_path, progress_cb) + return await self._run_flash("firmware_test", "Release", progress_cb) + elif target == FlashTarget.PRODUCTION: + ok = await self._run_flash("bootloader", "Release", progress_cb) + if ok: + ok = await self._run_flash("app", "Release", progress_cb) + return ok + elif target == FlashTarget.CUSTOM: + if bin_path is None: + raise ValueError("FlashTarget.CUSTOM требует bin_path") + return await self._run_flash_bin(bin_path, progress_cb) + return False + + async def _run_flash( # прошивка стандартного firmware по имени + self, + firmware: str, + build_type: str, + progress_cb: Optional[ProgressCallback], + ) -> bool: + """Запустить flash_usb.py для одного бинаря.""" + cmd = [ + "uv", + "run", + "--directory", + str(_HOST_TOOLS_DIR), + "python", + str(_FLASH_USB_SCRIPT), + "--firmware", + firmware, + "--build-type", + build_type, + ] + return await self._run_cmd(cmd, firmware, progress_cb) + + async def _run_flash_bin( # прошивка произвольного бинаря + self, + bin_path: Path, + progress_cb: Optional[ProgressCallback], + ) -> bool: + cmd = [ + "uv", + "run", + "--directory", + str(_HOST_TOOLS_DIR), + "python", + str(_FLASH_USB_SCRIPT), + "--bin-path", + str(bin_path.resolve()), + ] + return await self._run_cmd(cmd, bin_path.stem, progress_cb) + + async def _run_cmd( # общий subprocess runner + self, + cmd: list[str], + label: str, + progress_cb: Optional[ProgressCallback], + ) -> bool: + logger.info("Running: %s", " ".join(cmd)) + try: + self._proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=str(_HOST_TOOLS_DIR), + ) + assert self._proc.stdout is not None + async for raw_line in self._proc.stdout: + line = raw_line.decode("utf-8", errors="replace").rstrip() + logger.debug("flash_usb [%s]: %s", label, line) + if progress_cb is not None: + progress = _parse_progress(line) + if progress is not None: + await progress_cb(progress) + await self._proc.wait() + success = self._proc.returncode == 0 + if progress_cb is not None: + phase = "done" if success else "error" + msg = "Завершено" if success else "Ошибка" + await progress_cb(FlashProgress(phase=phase, percent=100, message=msg)) + return success + except Exception as exc: + logger.error("Subprocess error [%s]: %s", label, exc) + if progress_cb is not None: + await progress_cb( + FlashProgress(phase="error", percent=0, message=str(exc)) + ) + return False + finally: + self._proc = None + + +def _parse_progress(line: str) -> Optional[FlashProgress]: + """ + Извлечь прогресс из строки stdout flash_usb.py. + Возвращает None если строка не несёт прогресс-информации. + """ + percent_m = _RE_PERCENT.search(line) + phase_m = _RE_PHASE.search(line) + + if percent_m is None and phase_m is None: + return None + + percent = int(percent_m.group(1)) if percent_m else 0 + phase = phase_m.group(1).lower() if phase_m else "flash" + return FlashProgress(phase=phase, percent=percent, message=line.strip()) diff --git a/tools/production/app/m5_client.py b/tools/production/app/m5_client.py new file mode 100644 index 0000000..a282622 --- /dev/null +++ b/tools/production/app/m5_client.py @@ -0,0 +1,188 @@ +""" +m5_client.py — клиент M5StampPLC для TUI. + +Тонкая async-обёртка над синхронным JSON-lines протоколом M5 агента. +Blocking-вызовы выполняются в executor чтобы не блокировать Textual. + +Поддерживаемые команды агента (JSON-lines): + {"cmd": "ping"} + {"cmd": "relay_set", "relay": 1..4, "state": true/false} + {"cmd": "relay_get", "relay": 1..4} + {"cmd": "can_send", "id": 0x100, "data": [0xDE, 0xAD, 0xBE, 0xEF]} + {"cmd": "can_recv", "timeout_ms": 500} + +Карта реле (из HIL_BENCH.md): + RLY1 — питание таргета (VIN) + RLY2 — RS_RX (оптовход RS) + RLY3 — EXT_IN1 (оптовход IN1) + RLY4 — EXT_IN2 (оптовход IN2) +""" + +from __future__ import annotations + +import asyncio +import json +import logging +from typing import Optional + +import serial +import serial.tools.list_ports + +logger = logging.getLogger(__name__) + +# VID/PID M5StampPLC (Espressif USB JTAG/serial) +_M5_VID = 0x303A +_M5_PID = 0x1001 + +_READLINE_TIMEOUT_S = 0.5 +_CMD_TIMEOUT_S = 3.0 + + +def _find_m5_port() -> Optional[str]: + """Автодетект M5StampPLC по VID/PID.""" + for info in serial.tools.list_ports.comports(): + if info.vid == _M5_VID and info.pid == _M5_PID: + return info.device + return None + + +class M5Client: + """ + Async-клиент для M5StampPLC. + + Пример:: + + m5 = await M5Client.auto_connect() + if m5: + await m5.relay_set(3, True) # RLY3 ON → EXT_IN1 ACTIVE + await m5.relay_set(3, False) # RLY3 OFF + await m5.disconnect() + """ + + def __init__(self, port: str, baudrate: int = 115200) -> None: + self._port = port + self._baudrate = baudrate + self._ser: Optional[serial.Serial] = None + self._lock = asyncio.Lock() + + # ── Connection ────────────────────────────────────────────────────────── + + @classmethod + async def auto_connect(cls, baudrate: int = 115200) -> Optional["M5Client"]: + """ + Попытаться найти и подключиться к M5StampPLC. + Вернуть None если не найден — M5 опционален. + """ + port = _find_m5_port() + if port is None: + logger.info("M5StampPLC не найден") + return None + client = cls(port=port, baudrate=baudrate) + try: + await client.connect() + return client + except Exception as exc: + logger.warning("M5StampPLC connect failed: %s", exc) + return None + + async def connect(self) -> None: + """Открыть порт и проверить связь через ping.""" + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._open) + ok = await self.ping() + if not ok: + await self.disconnect() + raise ConnectionError(f"M5StampPLC не отвечает: {self._port}") + + def _open(self) -> None: + self._ser = serial.Serial( + port=self._port, + baudrate=self._baudrate, + timeout=_READLINE_TIMEOUT_S, + ) + self._ser.reset_input_buffer() + + async def disconnect(self) -> None: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._close) + + def _close(self) -> None: + if self._ser and self._ser.is_open: + self._ser.close() + self._ser = None + + @property + def connected(self) -> bool: + return self._ser is not None and self._ser.is_open + + # ── Low-level I/O ─────────────────────────────────────────────────────── + + def _send_recv(self, cmd: dict) -> Optional[dict]: + """Отправить команду, прочитать ответ (blocking).""" + assert self._ser is not None + line = json.dumps(cmd, separators=(",", ":")) + "\n" + self._ser.write(line.encode("ascii")) + self._ser.flush() + raw = self._ser.readline() + if not raw: + return None + try: + return json.loads(raw.decode("ascii", errors="replace").strip()) + except json.JSONDecodeError: + return None + + async def _cmd(self, cmd: dict) -> Optional[dict]: + """Async wrapper над _send_recv.""" + loop = asyncio.get_event_loop() + async with self._lock: + return await loop.run_in_executor(None, self._send_recv, cmd) + + # ── Public API ────────────────────────────────────────────────────────── + + async def ping(self) -> bool: + """Проверить связь с агентом.""" + resp = await self._cmd({"cmd": "ping"}) + return resp is not None and resp.get("status") == "ok" + + async def relay_set(self, relay: int, state: bool) -> bool: + """ + Переключить реле. + + :param relay: Номер реле 1..4. + :param state: True = ON, False = OFF. + :return: True при успехе. + """ + resp = await self._cmd({"cmd": "relay_set", "relay": relay, "state": state}) + return resp is not None and resp.get("status") == "ok" + + async def relay_get(self, relay: int) -> Optional[bool]: + """ + Прочитать состояние реле. + + :return: True/False или None при ошибке. + """ + resp = await self._cmd({"cmd": "relay_get", "relay": relay}) + if resp and resp.get("status") == "ok": + return bool(resp.get("state")) + return None + + async def can_send(self, can_id: int, data: list[int]) -> bool: + """Отправить CAN-фрейм через M5.""" + resp = await self._cmd({"cmd": "can_send", "id": can_id, "data": data}) + return resp is not None and resp.get("status") == "ok" + + async def can_recv(self, timeout_ms: int = 500) -> Optional[dict]: + """ + Принять CAN-фрейм. + + :return: {"id": int, "data": list[int]} или None при таймауте/ошибке. + """ + resp = await self._cmd({"cmd": "can_recv", "timeout_ms": timeout_ms}) + if resp and resp.get("status") == "ok": + return {"id": resp["id"], "data": resp["data"]} + return None + + @staticmethod + def find_port() -> Optional[str]: + """Найти порт M5StampPLC. None если не найден.""" + return _find_m5_port() \ No newline at end of file diff --git a/tools/production/app/models.py b/tools/production/app/models.py new file mode 100644 index 0000000..80ca804 --- /dev/null +++ b/tools/production/app/models.py @@ -0,0 +1,93 @@ +""" +models.py — типы данных TUI сервисного инженера. + +Все модели — frozen dataclasses или IntEnum. Без бизнес-логики. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import Optional + + +class AppMode(Enum): + """Режим работы приложения — определяется автодетектом USB.""" + + WAITING = auto() # ждём подключения устройства + FLASHING = auto() # обнаружен BootROM SDP (1FC9:0130) + DIAGNOSING = auto() # получен session_start по CDC + + +class TestStatus(Enum): + """Статус выполнения теста.""" + + PENDING = auto() + RUNNING = auto() + PASS = auto() + FAIL = auto() + SKIP = auto() + + +class FlashTarget(Enum): + """Что прошиваем.""" + + FIRMWARE_TEST = "firmware_test" + PRODUCTION = "production" # bootloader + tft_app + CUSTOM = "custom" # произвольный HAB-бинарь, путь задаётся отдельно + + +@dataclass(frozen=True) +class TestInfo: + """Метаданные теста из list_tests.""" + + id: str + name: str + critical: bool + requires_hil: bool + + +@dataclass +class TestResult: + """Результат выполнения теста.""" + + id: str + status: TestStatus + duration_ms: int = 0 + detail: str = "" + + +@dataclass +class SessionState: + """Состояние текущей диагностической сессии.""" + + fw_version: str = "" + target: str = "" + chip_uid: str = "" + m5_connected: bool = False + tests: list[TestInfo] = field(default_factory=list) + results: dict[str, TestResult] = field(default_factory=dict) + + def get_result(self, test_id: str) -> Optional[TestResult]: + return self.results.get(test_id) + + def set_result(self, result: TestResult) -> None: + self.results[result.id] = result + + +@dataclass(frozen=True) +class ConfirmRequest: + """confirm_request от таргета.""" + + id: str + prompt: str + timeout_ms: int + + +@dataclass(frozen=True) +class FlashProgress: + """Прогресс прошивки.""" + + phase: str # "sdphost" | "blhost" | "done" | "error" + percent: int # 0..100 + message: str \ No newline at end of file diff --git a/tools/production/app/orchestrator.py b/tools/production/app/orchestrator.py new file mode 100644 index 0000000..748ba43 --- /dev/null +++ b/tools/production/app/orchestrator.py @@ -0,0 +1,345 @@ +""" +orchestrator.py — оркестратор confirm_request для TUI. + +Получает события от FirmwareClient, принимает решение кто должен +ответить на confirm_request, и отправляет confirm обратно. + +Три режима confirm (из закрытых решений Этапа 8): + + standalone/interactive (display, mqs, usd): + → TUI показывает prompt оператору + кнопки OK/FAIL + countdown + → ждёт действия оператора + → отправляет confirm + + standalone/buttons: + → TUI показывает инструкцию оператору + → НЕ отправляет confirm — таргет сам детектирует нажатие + → ждёт следующего confirm_request или test_result + + HIL (opto, can): + → TUI автоматически командует M5 через M5Client + → отправляет confirm без участия оператора + → оператор видит только прогресс + +Маршрутизация определяется по prefix confirm_request.id: + "opto_*" → HIL (M5 relay) + "can_rx_ready" → HIL (M5 can_send) + "can_tx_verify" → HIL (M5 can_recv + verify) + "btn*" → buttons (нет confirm, ждём test_result) + всё остальное → operator (показать prompt) + +Публичный API: + Orchestrator.run_tests(test_ids) — запустить тесты, yield OrchestratorEvent +""" + +from __future__ import annotations + +import asyncio +import logging +from dataclasses import dataclass +from enum import Enum, auto +from typing import AsyncGenerator, Callable, Coroutine, Optional + +from .firmware_client import FirmwareClient +from .m5_client import M5Client +from .models import ConfirmRequest, TestInfo, TestResult, TestStatus + +logger = logging.getLogger(__name__) + +# Задержки для HIL (из закрытых решений: RELAY_ON_S=0.15, RELAY_OFF_S=0.5) +_RELAY_ON_S = 0.15 +_RELAY_OFF_S = 0.50 + +# Карта confirm_id → реле M5 для opto-теста +# Формат: confirm_id_prefix → (relay_num, target_state) +_OPTO_RELAY_MAP: dict[str, tuple[int, bool]] = { + "opto_in1_active": (3, True), + "opto_in1_inactive": (3, False), + "opto_in2_active": (4, True), + "opto_in2_inactive": (4, False), + "opto_rs_active": (2, True), + "opto_rs_inactive": (2, False), +} + +# CAN параметры (из закрытых решений) +_CAN_RX_ID = 0x100 +_CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF] +_CAN_TX_ID = 0x200 +_CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE] + + +# ── Типы событий оркестратора ──────────────────────────────────────────── + +class OrchestratorEventType(Enum): + TEST_BEGIN = auto() # тест начался + TEST_RESULT = auto() # тест завершился + CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone) + CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически + BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm) + SUMMARY = auto() # итог всей сессии + ERROR = auto() # ошибка протокола или M5 + + +@dataclass +class OrchestratorEvent: + """Событие от оркестратора — передаётся в TUI через asyncio.Queue.""" + + type: OrchestratorEventType + test_id: str = "" + test_name: str = "" + result: Optional[TestResult] = None + confirm: Optional[ConfirmRequest] = None + summary: Optional[dict] = None + message: str = "" + + +# Тип callback для ответа оператора: +# TUI вызывает его с True (OK) или False (FAIL) после показа prompt +OperatorConfirmCallback = Callable[[bool], Coroutine] + + +class Orchestrator: + """ + Оркестратор confirm_request. + + Пример:: + + orch = Orchestrator(firmware_client, m5_client) + async for event in orch.run_tests(["sdram", "opto", "display"]): + if event.type == OrchestratorEventType.CONFIRM_NEEDED: + # TUI показывает prompt, получает ответ оператора + confirmed = await tui.ask_operator(event.confirm) + await orch.resolve_operator_confirm(confirmed) + elif event.type == OrchestratorEventType.TEST_RESULT: + tui.update_result(event.result) + """ + + def __init__( + self, + firmware: FirmwareClient, + m5: Optional[M5Client] = None, + ) -> None: + self._fw = firmware + self._m5 = m5 + # Queue для ответов оператора на CONFIRM_NEEDED + self._operator_queue: asyncio.Queue[bool] = asyncio.Queue(maxsize=1) + + async def resolve_operator_confirm(self, confirmed: bool) -> None: + """ + TUI вызывает этот метод когда оператор нажал OK или FAIL. + Разблокирует ожидание внутри run_tests(). + """ + await self._operator_queue.put(confirmed) + + async def run_tests( + self, test_ids: list[str] + ) -> AsyncGenerator[OrchestratorEvent, None]: + """ + Запустить тесты и оркестрировать confirm_request. + + Yields OrchestratorEvent в порядке поступления событий от firmware_test. + Блокируется на CONFIRM_NEEDED до вызова resolve_operator_confirm(). + """ + async for raw in self._fw.run_selected(test_ids): + event_type = raw.get("type", "") + + if event_type == "test_begin": + yield OrchestratorEvent( + type=OrchestratorEventType.TEST_BEGIN, + test_id=raw.get("id", ""), + test_name=raw.get("name", ""), + ) + + elif event_type == "test_result": + status_str = raw.get("status", "fail") + status = { + "pass": TestStatus.PASS, + "fail": TestStatus.FAIL, + "skip": TestStatus.SKIP, + }.get(status_str, TestStatus.FAIL) + + result = TestResult( + id=raw.get("id", ""), + status=status, + duration_ms=raw.get("ms", 0), + detail=raw.get("detail", ""), + ) + yield OrchestratorEvent( + type=OrchestratorEventType.TEST_RESULT, + test_id=result.id, + result=result, + ) + + elif event_type == "confirm_request": + confirm = ConfirmRequest( + id=raw.get("id", ""), + prompt=raw.get("prompt", ""), + timeout_ms=raw.get("timeout_ms", 30000), + ) + async for orch_event in self._handle_confirm(confirm): + yield orch_event + + elif event_type == "summary": + yield OrchestratorEvent( + type=OrchestratorEventType.SUMMARY, + summary=raw, + ) + + elif event_type in ("error", None): + yield OrchestratorEvent( + type=OrchestratorEventType.ERROR, + message=raw.get("error", "unknown error"), + ) + + # ── Маршрутизация confirm ──────────────────────────────────────────── + + async def _handle_confirm( + self, confirm: ConfirmRequest + ) -> AsyncGenerator[OrchestratorEvent, None]: + """Определить тип confirm и обработать соответственно.""" + cid = confirm.id + + # HIL: opto-тест + if cid in _OPTO_RELAY_MAP: + async for ev in self._handle_hil_opto(confirm): + yield ev + return + + # HIL: CAN RX + if cid == "can_rx_ready": + async for ev in self._handle_hil_can_rx(confirm): + yield ev + return + + # HIL: CAN TX verify + if cid == "can_tx_verify": + async for ev in self._handle_hil_can_tx(confirm): + yield ev + return + + # buttons: нет JSON-confirm, только инструкция оператору + if cid.startswith("btn"): + yield OrchestratorEvent( + type=OrchestratorEventType.BUTTONS_PROMPT, + confirm=confirm, + ) + return + + # standalone interactive: показать оператору + async for ev in self._handle_operator_confirm(confirm): + yield ev + + # ── HIL opto ──────────────────────────────────────────────────────── + + async def _handle_hil_opto( + self, confirm: ConfirmRequest + ) -> AsyncGenerator[OrchestratorEvent, None]: + relay_num, relay_state = _OPTO_RELAY_MAP[confirm.id] + + if self._m5 is None: + logger.error("HIL confirm без M5: %s", confirm.id) + await self._fw.send_confirm(confirm.id, False) + yield OrchestratorEvent( + type=OrchestratorEventType.ERROR, + message=f"M5 не подключён для HIL confirm: {confirm.id}", + ) + return + + settle_s = _RELAY_ON_S if relay_state else _RELAY_OFF_S + ok = await self._m5.relay_set(relay_num, relay_state) + await asyncio.sleep(settle_s) + + await self._fw.send_confirm(confirm.id, ok) + + yield OrchestratorEvent( + type=OrchestratorEventType.CONFIRM_RESOLVED, + test_id="opto", + message=f"RLY{relay_num} {'ON' if relay_state else 'OFF'} → {confirm.id}", + ) + + # ── HIL CAN RX ────────────────────────────────────────────────────── + + async def _handle_hil_can_rx( + self, confirm: ConfirmRequest + ) -> AsyncGenerator[OrchestratorEvent, None]: + if self._m5 is None: + await self._fw.send_confirm(confirm.id, False) + yield OrchestratorEvent( + type=OrchestratorEventType.ERROR, + message="M5 не подключён для CAN RX confirm", + ) + return + + # M5 отправляет CAN-фрейм, таргет принимает после confirmed:true + ok = await self._m5.can_send(_CAN_RX_ID, _CAN_RX_DATA) + await self._fw.send_confirm(confirm.id, ok) + + yield OrchestratorEvent( + type=OrchestratorEventType.CONFIRM_RESOLVED, + test_id="can", + message=f"M5 CAN TX id=0x{_CAN_RX_ID:03X} data={_CAN_RX_DATA}", + ) + + # ── HIL CAN TX verify ─────────────────────────────────────────────── + + async def _handle_hil_can_tx( + self, confirm: ConfirmRequest + ) -> AsyncGenerator[OrchestratorEvent, None]: + if self._m5 is None: + await self._fw.send_confirm(confirm.id, False) + yield OrchestratorEvent( + type=OrchestratorEventType.ERROR, + message="M5 не подключён для CAN TX verify", + ) + return + + # Таргет уже отправил фрейм, M5 принимает и верифицирует + frame = await self._m5.can_recv(timeout_ms=500) + verified = ( + frame is not None + and frame["id"] == _CAN_TX_ID + and frame["data"] == _CAN_TX_DATA + ) + await self._fw.send_confirm(confirm.id, verified) + + msg = ( + f"M5 CAN RX ok: id=0x{_CAN_TX_ID:03X}" + if verified + else "M5 CAN RX: фрейм не получен или не совпадает" + ) + yield OrchestratorEvent( + type=OrchestratorEventType.CONFIRM_RESOLVED, + test_id="can", + message=msg, + ) + + # ── Operator confirm ───────────────────────────────────────────────── + + async def _handle_operator_confirm( + self, confirm: ConfirmRequest + ) -> AsyncGenerator[OrchestratorEvent, None]: + """ + Передать confirm оператору. Блокируется до resolve_operator_confirm(). + TUI должен показать prompt и дать возможность ответить. + """ + # очистить очередь на случай устаревшего ответа + while not self._operator_queue.empty(): + self._operator_queue.get_nowait() + + yield OrchestratorEvent( + type=OrchestratorEventType.CONFIRM_NEEDED, + confirm=confirm, + ) + + # ждём ответа от оператора (TUI вызовет resolve_operator_confirm) + timeout_s = confirm.timeout_ms / 1000.0 + try: + confirmed = await asyncio.wait_for( + self._operator_queue.get(), + timeout=timeout_s, + ) + except asyncio.TimeoutError: + logger.warning("Operator confirm timeout: %s", confirm.id) + confirmed = False + + await self._fw.send_confirm(confirm.id, confirmed) \ No newline at end of file diff --git a/tools/production/main.py b/tools/production/main.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/production/pyproject.toml b/tools/production/pyproject.toml new file mode 100644 index 0000000..b7f6ac8 --- /dev/null +++ b/tools/production/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "service-tui" +version = "0.1.0" +description = "TUI сервисного инженера для диагностики платы MIMXRT1052" +requires-python = ">=3.11" +dependencies = [ + "textual>=0.80.0", + "pyserial>=3.5", + "python-dotenv>=1.0.0", + "pyinstaller>=6.0.0", +] + +[project.scripts] +service-tui = "main:main" + +[tool.uv] +package = false + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" \ No newline at end of file From 55cd9e8273333b53412c8162eddd1ffb0e9b4a12 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 29 Jun 2026 19:54:44 +0300 Subject: [PATCH 2/9] # firmware_test: TUI app current state --- .env.example | 4 + bsp/usb_cdc/src/usb_device_descriptor.h | 4 +- firmware/test/CMakeLists.txt | 11 + firmware/test/src/cli.c | 6 + firmware/test/src/protocol.c | 9 + firmware/test/src/protocol.h | 11 +- firmware/test/src/version.h.in | 25 + tools/production/README.md | 694 ++++++++++++++---- tools/production/TUI_REPORT.md | 209 ++++++ tools/production/app/app.py | 87 +++ tools/production/app/app.tcss | 283 +++++++ tools/production/app/firmware_client.py | 34 +- tools/production/app/flasher.py | 32 +- tools/production/app/m5_client.py | 8 +- tools/production/app/screens/__init__.py | 7 + tools/production/app/screens/diag/__init__.py | 261 +++++++ .../app/screens/diag/confirm_panel.py | 140 ++++ tools/production/app/screens/diag/results.py | 115 +++ .../production/app/screens/diag/test_list.py | 83 +++ tools/production/app/screens/flash.py | 197 +++++ tools/production/app/screens/waiting.py | 83 +++ tools/production/main.py | 42 ++ tools/production/pyproject.toml | 1 + tools/production/service_tui.log | 662 +++++++++++++++++ tools/production/uv.lock | 264 +++++++ 25 files changed, 3104 insertions(+), 168 deletions(-) create mode 100644 firmware/test/src/version.h.in create mode 100644 tools/production/TUI_REPORT.md create mode 100644 tools/production/app/app.py create mode 100644 tools/production/app/app.tcss create mode 100644 tools/production/app/screens/__init__.py create mode 100644 tools/production/app/screens/diag/__init__.py create mode 100644 tools/production/app/screens/diag/confirm_panel.py create mode 100644 tools/production/app/screens/diag/results.py create mode 100644 tools/production/app/screens/diag/test_list.py create mode 100644 tools/production/app/screens/flash.py create mode 100644 tools/production/app/screens/waiting.py create mode 100644 tools/production/service_tui.log create mode 100644 tools/production/uv.lock diff --git a/.env.example b/.env.example index 5b80699..2b17a6e 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,10 @@ FLASHLOADER_PID=0073 SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad + +# Тип сборки firmware_test для TUI (Debug | Release) +FIRMWARE_BUILD_TYPE=Debug + # --- Paths --- # BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile # через justfile_directory(), здесь можно переопределить если нужно diff --git a/bsp/usb_cdc/src/usb_device_descriptor.h b/bsp/usb_cdc/src/usb_device_descriptor.h index c8a4dc8..b52e2a3 100644 --- a/bsp/usb_cdc/src/usb_device_descriptor.h +++ b/bsp/usb_cdc/src/usb_device_descriptor.h @@ -19,8 +19,8 @@ #define USB_DEVICE_DEMO_BCD_VERSION (0x0101U) /* ---- VID / PID --------------------------------------------------------- */ -#define USB_DEVICE_VID (0x1996U) /* TODO: заменить на свой */ -#define USB_DEVICE_PID (0x00ADU) /* TODO: заменить на свой */ +#define USB_DEVICE_VID (0x1996U) +#define USB_DEVICE_PID (0x00ADU) /* ---- CDC коды классов -------------------------------------------------- */ #define CDC_COMM_CLASS (0x02U) diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 798068e..fb3586d 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -1,8 +1,17 @@ # 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( @@ -25,6 +34,8 @@ add_executable( 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 - # инициализация некешируемое секции нулями diff --git a/firmware/test/src/cli.c b/firmware/test/src/cli.c index 738d58f..70ff248 100644 --- a/firmware/test/src/cli.c +++ b/firmware/test/src/cli.c @@ -331,6 +331,12 @@ static void handle_cmd(const char *p_line) return; } + if (strcmp(cmd_name, "get_version") == 0) + { + protocol_send_version_response(); + return; + } + if (strcmp(cmd_name, "list_tests") == 0) { test_runner_send_list(); diff --git a/firmware/test/src/protocol.c b/firmware/test/src/protocol.c index a8db0c9..4fc3b62 100644 --- a/firmware/test/src/protocol.c +++ b/firmware/test/src/protocol.c @@ -157,4 +157,13 @@ void protocol_send_uid_response(const uint8_t *p_uid) (unsigned int) p_uid[3U], (unsigned int) p_uid[4U], (unsigned int) p_uid[5U], (unsigned int) p_uid[6U], (unsigned int) p_uid[7U]); cli_send(buf); +} + +void protocol_send_version_response(void) +{ + char buf[PROTO_BUF_SIZE]; + (void) snprintf(buf, sizeof(buf), + "{\"type\":\"version_response\"," + "\"fw\":\"" FIRMWARE_TEST_VERSION "\"}\n"); + cli_send(buf); } \ No newline at end of file diff --git a/firmware/test/src/protocol.h b/firmware/test/src/protocol.h index 0bbff09..b4c7988 100644 --- a/firmware/test/src/protocol.h +++ b/firmware/test/src/protocol.h @@ -19,12 +19,14 @@ #define PROTOCOL_H_ #include "test_module.h" +#include "version.h" #include #include #include + /** @brief Строка версии прошивки, вставляемая в session_start. */ -#define FIRMWARE_TEST_VERSION "0.1.4" +#define FIRMWARE_TEST_VERSION FIRMWARE_TEST_VERSION_STR /** @brief Таймаут подтверждения по умолчанию, мс. */ #define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U @@ -100,4 +102,11 @@ void protocol_send_error(const char *p_code); */ void protocol_send_uid_response(const uint8_t *p_uid); +/** + * @brief Отправить ответ на команду get_version. + * + * Формат: {"type":"version_response","fw":"X.Y.Z"} + */ +void protocol_send_version_response(void); + #endif /* PROTOCOL_H_ */ \ No newline at end of file diff --git a/firmware/test/src/version.h.in b/firmware/test/src/version.h.in new file mode 100644 index 0000000..a80b12c --- /dev/null +++ b/firmware/test/src/version.h.in @@ -0,0 +1,25 @@ +/** + * @file version.h + * @brief Версия firmware_test — генерируется CMake из CMakeLists.txt. + * + * НЕ редактировать вручную. Версию менять в firmware/test/CMakeLists.txt: + * project(firmware_test VERSION X.Y.Z) + */ + +#ifndef VERSION_H_ +#define VERSION_H_ + +/** @brief Мажорная версия. */ +#define FIRMWARE_TEST_VERSION_MAJOR @firmware_test_VERSION_MAJOR@ + +/** @brief Минорная версия. */ +#define FIRMWARE_TEST_VERSION_MINOR @firmware_test_VERSION_MINOR@ + +/** @brief Патч-версия. */ +#define FIRMWARE_TEST_VERSION_PATCH @firmware_test_VERSION_PATCH@ + +/** @brief Версия строкой для протокола: "X.Y.Z". */ +#define FIRMWARE_TEST_VERSION_STR \ + "@firmware_test_VERSION_MAJOR@.@firmware_test_VERSION_MINOR@.@firmware_test_VERSION_PATCH@" + +#endif /* VERSION_H_ */ \ No newline at end of file diff --git a/tools/production/README.md b/tools/production/README.md index 9bcd2e2..2d4c0cc 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -5,200 +5,605 @@ TUI-приложение для диагностики и прошивки пл --- +## Структура проекта + +```bash +tools/production/ +├── main.py ← точка входа (10 строк) +├── pyproject.toml ← зависимости uv +├── uv.lock +└── app/ + ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов + ├── models.py ← все типы данных (dataclass/Enum) + ├── firmware_client.py ← async USB CDC клиент firmware_test + ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines) + ├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py + ├── orchestrator.py ← маршрутизация confirm_request + └── screens/ + ├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, DiagScreen + ├── waiting.py ← WaitingScreen — ожидание USB + ├── flash.py ← FlashScreen — прошивка / chip erase + └── diag/ + ├── __init__.py ← DiagScreen — координатор диагностики + ├── test_list.py ← TestListPanel — чекбоксы тестов + ├── results.py ← ResultsPanel — строки результатов + ├── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown + └── styles/ + ├── waiting.tcss + ├── flash.tcss + └── diag.tcss +``` + +--- + ## Концепция ```mermaid graph LR subgraph PC["Сервисный ПК"] TUI["service-tui\n(Textual App)"] - subgraph tools["tools/"] + subgraph app["app/"] FC["firmware_client.py\nUSB CDC ACM"] M5["m5_client.py\nSerial JSON-lines"] - FL["flasher.py\n→ tools/host/flash_usb.py"] + FL["flasher.py\nsubprocess"] + OR["orchestrator.py\nconfirm router"] end - TUI --> FC - TUI --> M5 - TUI --> FL + TUI --> FC & M5 & FL & OR end subgraph Board["Плата TFT (MIMXRT1052)"] FW["firmware_test\n(USB CDC)"] - ROM["BootROM\n(SDP 1FC9:0130)"] + ROM["BootROM SDP\n(1FC9:0130)"] end subgraph HIL["HIL стенд (опционально)"] M5HW["M5StampPLC\nRLY1–4 + CAN"] end - FC <-->|"JSON-lines\nVID:PID 1996:00AD"| FW - FL -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM - M5 <-->|"JSON-lines\nSerial"| M5HW - M5HW -->|"RLY1–4"| Board + subgraph Host["tools/host/"] + FU["flash_usb.py\nsdphost + blhost"] + end + + FC <-->|"JSON-lines\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 при старте и меняется динамически. +Режим определяется автодетектом USB и меняется динамически без перезапуска TUI. ```mermaid stateDiagram-v2 [*] --> WAITING : запуск TUI - WAITING --> FLASHING : обнаружен VID:PID 1FC9:0130\n(BootROM SDP) - WAITING --> DIAGNOSING : обнаружен VID:PID 1996:00AD\n+ session_start по CDC + WAITING --> FLASHING : VID:PID 1FC9:0130\n(BootROM SDP) + WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC - FLASHING --> WAITING : прошивка завершена / плата отключена - FLASHING --> DIAGNOSING: плата перезагружена в нормальный режим + FLASHING --> WAITING : FlashDone / ESC / плата отключена + FLASHING --> DIAGNOSING: плата перезагружена после прошивки - DIAGNOSING --> WAITING : плата отключена - DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка) + DIAGNOSING --> WAITING : DiagDone / ESC / плата отключена + DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка BOOT_MOD) ``` ### Режим A — Прошивка -Триггер: обнаружен BootROM SDP (`1FC9:0130`). +Триггер: BootROM SDP `1FC9:0130` виден в `serial.tools.list_ports`. ```bash ┌─ Прошивка платы ─────────────────────────────────┐ -│ Обнаружен BootROM (SDP режим) │ +│ ⚡ BootROM SDP обнаружен │ │ │ │ Что прошить? │ -│ ◉ firmware_test (диагностика) │ +│ ◉ firmware_test (диагностическая прошивка) │ │ ○ Production (bootloader + tft_app) │ +│ ○ Кастомный бинарь... │ │ │ -│ ████████████░░░░░░ 64% Запись во Flash... │ +│ [ ▶ Прошить ] [ ⚠ Chip Erase ] │ +│ │ +│ ████████████░░░░░░ 64% blhost 64% │ +│ ┌────────────────────────────────────────────┐ │ +│ │ ▶ Прошивка: firmware_test │ │ +│ │ $ blhost -u 0x15A2,0x0073 -- write-memory… │ │ +│ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ``` ### Режим B — Диагностика -Триггер: CDC-порт `1996:00AD` обнаружен и `ping→pong` прошёл. +Триггер: CDC-порт `1996:00AD` виден + `ping→pong` прошёл. +```bash +┌─ Диагностика fw:0.1.4 UID:A1B2C3D4E5F60011 ────────────────┐ +│ M5: ✓ подключён │ +├────────────────────────────┬───────────────────────────────────┤ +│ Тесты │ Результаты │ +│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ +│ ☑ QSPI Flash │ qspi ✓ PASS │ +│ ☑ microSD │ usd ✗ FAIL mount err: 5 │ +│ ☑ TFT Display │ display ✓ PASS │ +│ ☑ Кнопки │ buttons ✓ PASS │ +│ ☑ MQS Audio │ mqs ✓ PASS │ +│ ☑ CAN loopback [HIL] │ can … running │ +│ ☑ Оптовходы [HIL] │ opto pending │ +├────────────────────────────┴───────────────────────────────────┤ +│ [ ▶ Запустить выбранные ] [ ▶▶ Все тесты ] │ +│ ████████████████░░░░ 80% Тест: can │ +├────────────────────────────────────────────────────────────────┤ +│ ⚠ Экран залит красным цветом? 28с │ +│ [ ✓ Да ] [ ✗ Нет ] │ +└────────────────────────────────────────────────────────────────┘ ``` -┌─ Диагностика fw:0.1.4 UID:A1B2C3D4E5F60011 ─────┐ -│ M5StampPLC: ✓ │ Результаты: │ -├───────────────────────────┤ sdram ✓ PASS │ -│ ☑ SDRAM 32 MB │ qspi ✓ PASS │ -│ ☑ QSPI Flash │ usd ✗ FAIL │ -│ ☑ microSD │ mount failed: 5 │ -│ ☑ TFT Display │ display ✓ PASS │ -│ ☑ Кнопки │ buttons ✓ PASS │ -│ ☑ MQS Audio │ mqs ✓ PASS │ -│ ☑ CAN loopback [HIL] │ can … running │ -│ ☑ Оптовходы [HIL] │ opto ○ pending │ -├───────────────────────────┴─────────────────────────┤ -│ [ Запустить выбранные ] [ Все тесты ] │ -│ ████████████████░░░░ 80% Тест: can │ -├─────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└─────────────────────────────────────────────────────┘ -``` + +HIL-тесты без M5StampPLC отображаются серыми и не выбираются автоматически. --- -## Архитектура приложения +## Диаграмма классов ```mermaid -graph TB - subgraph TUI["tui.py — Textual App"] - WS["WaitingScreen"] - FS["FlashScreen"] - DS["DiagScreen"] - end +classDiagram + direction TB - subgraph Core["app/"] - OR["orchestrator.py\nмаршрутизация confirm_request"] - FW["firmware_client.py\nasync CDC клиент"] - M5["m5_client.py\nasync M5 клиент"] - FL["flasher.py\nsubprocess flash_usb.py"] - MD["models.py\nTestInfo · TestResult\nSessionState · ConfirmRequest"] - end + %% ── Точка входа ────────────────────────────────────────── + class ServiceApp { + -_fw: FirmwareClient + -_m5: M5Client + +on_mount() + +_on_device_detected(event) + +_on_flash_done(event) + +_on_diag_done() + +_connect_and_diagnose() + +_disconnect() + } - DS --> OR - OR --> FW - OR --> M5 - FS --> FL - DS --> MD - OR --> MD + %% ── Экраны ─────────────────────────────────────────────── + class WaitingScreen { + -_spinner_idx: int + -_detect_timer: Timer + -_spin_timer: Timer + +on_mount() + +on_unmount() + -_poll_usb() + -_spin() + -_stop_timers() + } + class WaitingScreen.DeviceDetected { + +mode: AppMode + } + + class FlashScreen { + -_flasher: Flasher + -_flashing: bool + +compose() + -_on_radio_changed(event) + -_on_flash_pressed() + -_on_erase_pressed() + -_do_flash(target, bin_path) + -_do_erase() + -_resolve_target() + -_on_progress(progress) + -_set_busy(busy) + } + class FlashScreen.FlashDone { + +success: bool + } + + class DiagScreen { + -_fw: FirmwareClient + -_m5: M5Client + -_orchestrator: Orchestrator + -_session: SessionState + -_running: bool + +on_mount() + -_init_session() + -_update_header() + -_on_run_selected() + -_on_run_all() + -_on_confirmed(event) + -_start_run(test_ids) + -_run_worker(test_ids) + -_handle_event(event, total, done) + -_on_summary(summary) + } + class DiagScreen.DiagDone + + %% ── Виджеты DiagScreen ─────────────────────────────────── + class TestListPanel { + -_checkboxes: dict + +populate(tests, m5_connected) + +get_selected_ids() list + +set_enabled(enabled) + } + + class ResultsPanel { + +populate(tests) + +set_running(test_id) + +set_result(result) + +reset() + -_update(test_id, status, detail) + } + + class ConfirmPanel { + -_timer: Timer + -_remaining: int + +show_operator(prompt, timeout_ms) + +show_buttons_hint(prompt) + +hide() + -_tick() + -_start_timer() + -_stop_timer() + } + class ConfirmPanel.Confirmed { + +confirmed: bool + } + + %% ── Клиенты ────────────────────────────────────────────── + class FirmwareClient { + -_port: str + -_ser: Serial + -_lock: Lock + +connect() + +disconnect() + +ping() bool + +list_tests() list + +run_selected(test_ids) AsyncGenerator + +send_confirm(id, confirmed) + +get_uid() str + +auto_connect(vid, pid)$ + +find_port(vid, pid)$ + } + + class M5Client { + -_port: str + -_ser: Serial + -_lock: Lock + +connect() + +disconnect() + +ping() bool + +relay_set(relay, state) bool + +relay_get(relay) bool + +can_send(id, data) bool + +can_recv(timeout_ms) dict + +auto_connect()$ + +find_port()$ + } + + class Flasher { + -_proc: Process + +detect_sdp()$ bool + +detect_cdc()$ bool + +flash(target, progress_cb, bin_path) bool + +erase_chip(progress_cb) bool + -_run_flash(firmware, build_type, cb) + -_run_flash_bin(bin_path, cb) + -_run_cmd(cmd, label, cb) + } + + %% ── Оркестратор ────────────────────────────────────────── + class Orchestrator { + -_fw: FirmwareClient + -_m5: M5Client + -_operator_queue: Queue + +run_tests(test_ids) AsyncGenerator + +resolve_operator_confirm(confirmed) + -_handle_confirm(confirm) + -_handle_hil_opto(confirm) + -_handle_hil_can_rx(confirm) + -_handle_hil_can_tx(confirm) + -_handle_operator_confirm(confirm) + } + + %% ── Модели ─────────────────────────────────────────────── + class SessionState { + +fw_version: str + +chip_uid: str + +m5_connected: bool + +tests: list + +results: dict + +get_result(id) + +set_result(result) + } + + class OrchestratorEvent { + +type: OrchestratorEventType + +test_id: str + +result: TestResult + +confirm: ConfirmRequest + +summary: dict + +message: str + } + + class TestInfo { + +id: str + +name: str + +critical: bool + +requires_hil: bool + } + + class TestResult { + +id: str + +status: TestStatus + +duration_ms: int + +detail: str + } + + class ConfirmRequest { + +id: str + +prompt: str + +timeout_ms: int + } + + class FlashProgress { + +phase: str + +percent: int + +message: str + } + + %% ── Enum ───────────────────────────────────────────────── + class AppMode { + <> + WAITING + FLASHING + DIAGNOSING + } + + class TestStatus { + <> + PENDING + RUNNING + PASS + FAIL + SKIP + } + + class FlashTarget { + <> + FIRMWARE_TEST + PRODUCTION + CUSTOM + } + + class OrchestratorEventType { + <> + TEST_BEGIN + TEST_RESULT + CONFIRM_NEEDED + CONFIRM_RESOLVED + BUTTONS_PROMPT + SUMMARY + ERROR + } + + %% ── Связи ──────────────────────────────────────────────── + + %% App → экраны + ServiceApp --> WaitingScreen : push/switch + ServiceApp --> FlashScreen : switch + ServiceApp --> DiagScreen : switch + ServiceApp --> FirmwareClient : создаёт + ServiceApp --> M5Client : создаёт + + %% Сообщения от экранов + WaitingScreen ..> WaitingScreen.DeviceDetected : posts + FlashScreen ..> FlashScreen.FlashDone : posts + DiagScreen ..> DiagScreen.DiagDone : posts + ConfirmPanel ..> ConfirmPanel.Confirmed : posts + + %% App слушает сообщения + ServiceApp ..> WaitingScreen.DeviceDetected : on() + ServiceApp ..> FlashScreen.FlashDone : on() + ServiceApp ..> DiagScreen.DiagDone : on() + + %% Экраны → компоненты + FlashScreen --> Flasher : использует + DiagScreen --> Orchestrator : создаёт + DiagScreen --> TestListPanel : монтирует + DiagScreen --> ResultsPanel : монтирует + DiagScreen --> ConfirmPanel : монтирует + DiagScreen --> SessionState : владеет + DiagScreen ..> ConfirmPanel.Confirmed : on() + + %% Оркестратор + Orchestrator --> FirmwareClient : вызывает + Orchestrator --> M5Client : вызывает + Orchestrator ..> OrchestratorEvent : yields + + %% Flasher детект + WaitingScreen --> Flasher : detect_sdp/cdc + + %% Модели + Flasher --> FlashProgress + Orchestrator --> ConfirmRequest + Orchestrator --> TestResult + DiagScreen --> OrchestratorEvent + SessionState --> TestInfo + SessionState --> TestResult + + %% Enum использование + ServiceApp --> AppMode + WaitingScreen.DeviceDetected --> AppMode + TestResult --> TestStatus + Flasher --> FlashTarget + Orchestrator --> OrchestratorEventType + OrchestratorEvent --> OrchestratorEventType ``` --- ## Обработка confirm_request -Маршрутизация определяется по `id` поля `confirm_request`: +Маршрутизация реализована в `Orchestrator._handle_confirm()` по значению `confirm_request.id`: ```mermaid flowchart TD CR["confirm_request\nот firmware_test"] - CR --> R{confirm_request.id} - R -->|"opto_*"| HIL_OPTO["HIL: M5 relay_set\n→ settle → confirm"] - R -->|"can_rx_ready"| HIL_CAN_RX["HIL: M5 can_send\n→ confirm"] - R -->|"can_tx_verify"| HIL_CAN_TX["HIL: M5 can_recv\n→ verify → confirm"] - R -->|"btn*"| BTN["показать инструкцию\nне отправлять confirm\nждать test_result"] - R -->|"всё остальное"| OP["показать оператору\nprompt + OK/FAIL\n+ countdown"] + R -->|"opto_in1_active\nopto_in1_inactive\nopto_in2_active\nopto_in2_inactive\nopto_rs_active\nopto_rs_inactive"| HIL_OPTO + R -->|"can_rx_ready"| HIL_CAN_RX + R -->|"can_tx_verify"| HIL_CAN_TX + R -->|"btn*"| BTN + R -->|"всё остальное"| OP - HIL_OPTO --> AUTO["CONFIRM_RESOLVED\n(автоматически)"] - HIL_CAN_RX --> AUTO - HIL_CAN_TX --> AUTO - OP --> WAIT["CONFIRM_NEEDED\nждём resolve_operator_confirm()"] - WAIT --> SEND["send_confirm(id, confirmed)"] - AUTO --> SEND + HIL_OPTO["M5: relay_set(rly, state)\nsleep(settle)\nsend_confirm(true/false)"] + HIL_CAN_RX["M5: can_send(0x100, data)\nsend_confirm(ok)"] + HIL_CAN_TX["M5: can_recv()\nverify id+data\nsend_confirm(verified)"] + BTN["DiagScreen: show_buttons_hint()\nНЕ отправлять confirm\nтаргет сам детектирует нажатие"] + OP["DiagScreen: show_operator()\nCountdown таймер\nОжидать resolve_operator_confirm()"] + + HIL_OPTO --> RAUTO["CONFIRM_RESOLVED → прогресс"] + HIL_CAN_RX --> RAUTO + HIL_CAN_TX --> RAUTO + OP --> RNEED["CONFIRM_NEEDED → UI панель\nОператор нажимает OK/Нет"] + RNEED --> RESOLVE["ConfirmPanel.Confirmed\n→ resolve_operator_confirm()\n→ send_confirm()"] ``` -| confirm id | Кто отвечает | Действие TUI | -| --------------- | ------------ | -------------------------------------- | -| `opto_*` | M5 авто | показать прогресс | -| `can_rx_ready` | M5 авто | показать прогресс | -| `can_tx_verify` | M5 авто | показать прогресс | -| `btn*` | физика | показать инструкцию, ждать test_result | -| всё остальное | оператор | prompt + OK/FAIL + countdown | +| `confirm_request.id` | Кто отвечает | Действие TUI | Реле M5 | +| -------------------- | ------------ | -------------------- | ---------- | +| `opto_in1_active` | M5 авто | прогресс | RLY3 ON | +| `opto_in1_inactive` | M5 авто | прогресс | RLY3 OFF | +| `opto_in2_active` | M5 авто | прогресс | RLY4 ON | +| `opto_in2_inactive` | M5 авто | прогресс | RLY4 OFF | +| `opto_rs_active` | M5 авто | прогресс | RLY2 ON | +| `opto_rs_inactive` | M5 авто | прогресс | RLY2 OFF | +| `can_rx_ready` | M5 авто | прогресс | — (CAN TX) | +| `can_tx_verify` | M5 авто | прогресс | — (CAN RX) | +| `btn*` | физика | инструкция оператору | — | +| всё остальное | оператор | prompt + countdown | — | --- -## Структура +## Архитектура экранов -```bash -tools/production/ -├── pyproject.toml ← зависимости: textual, pyserial, python-dotenv, pyinstaller -├── uv.lock -├── main.py ← точка входа: asyncio + Textual App -├── app/ -│ ├── tui.py ← Textual App, экраны (WaitingScreen, FlashScreen, DiagScreen) -│ ├── firmware_client.py ← async USB CDC клиент firmware_test -│ ├── m5_client.py ← async M5StampPLC клиент -│ ├── flasher.py ← subprocess → tools/host/flash_usb.py -│ ├── orchestrator.py ← confirm_request маршрутизатор -│ └── models.py ← AppMode, TestInfo, TestResult, SessionState, … -└── README.md ← этот файл +```mermaid +graph TB + subgraph ServiceApp["ServiceApp (app.py)"] + direction LR + WS["WaitingScreen"] + FS["FlashScreen"] + DS["DiagScreen"] + end + + subgraph DiagInternals["DiagScreen (screens/diag/)"] + TL["TestListPanel\ntest_list.py"] + RP["ResultsPanel\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"| WS + DS -->|"DiagDone"| WS + + FS --> FL + DS --> OR + DS --> TL + DS --> RP + DS --> CP + CP -->|"Confirmed"| DS + OR --> FC + OR --> M5 + WS --> 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: poll USB каждые 1.5 с + + OP->>FW: подключить плату USB + WS->>TUI: DeviceDetected(DIAGNOSING) + TUI->>FW: auto_connect() → ping→pong + TUI->>M5: auto_connect() (опционально) + TUI->>DS: switch_screen() + + DS->>FW: list_tests() → TestInfo×8 + DS->>FW: get_uid() → "A1B2C3D4..." + DS->>DS: populate TestListPanel + ResultsPanel + + OP->>DS: выбрать тесты → Запустить + DS->>FW: run_selected([...]) + + loop Для каждого теста + FW-->>DS: test_begin + DS->>DS: ResultsPanel.set_running() + + alt HIL confirm (opto / can) + FW-->>DS: confirm_request + DS->>M5: relay_set() / can_send() / can_recv() + DS->>FW: send_confirm(true/false) + DS->>DS: прогресс CONFIRM_RESOLVED + 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() + end + + FW-->>DS: summary + DS->>DS: показать итог PASS / FAIL + OP->>DS: ESC → DiagDone + TUI->>WS: switch_screen() ``` --- ## Конфигурация (`.env`) -Файл `.env` в корне репозитория — единый источник конфигурации. +Файл `.env` в корне репозитория — единый источник. Загружается через `python-dotenv` в `main.py` до импорта app-модулей. ```ini -# USB VID:PID — BootROM SDP (менять нельзя, NXP ROM) +# 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 -# Пути к бинарям (опционально, TUI ищет в build/ автоматически) -FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin -PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin -PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin +# Опционально: путь к директории лога TUI +# SERVICE_LOG_DIR=/tmp ``` +Пути к бинарям `flash_usb.py` вычисляет автоматически из `BUILD_DIR` (также из `.env`). + --- ## Запуск @@ -206,7 +611,7 @@ PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin ### Из монорепозитория (разработчик) ```bash -# Установить зависимости +# Установить зависимости tools/production/ just host::service-setup # Запустить TUI @@ -215,7 +620,8 @@ just host::service-tui ### Standalone-бинарь (сервисник) -Скачать `service_tui` из [GitHub Releases](https://github.com/OSabuser/tft_manufacture_test/releases) и запустить двойным кликом. +Скачать `service_tui` из [GitHub Releases](https://github.com/OSabuser/tft_manufacture_test/releases) и запустить двойным кликом — Python не требуется. + Для сборки из исходников: ```bash @@ -223,52 +629,40 @@ just host::service-build # → tools/production/dist/service_tui ``` +> **Важно:** standalone-бинарь не включает `tools/host/`. Перед сборкой убедитесь что `tools/host/` инициализирован (`just host::setup-tools`) и доступен рядом с бинарём, либо измените `_FLASH_USB_SCRIPT` в `flasher.py` на абсолютный путь. + --- -## Рабочий процесс сервисника +## Рабочие процессы сервисника -```mermaid -sequenceDiagram - participant OP as Оператор - participant TUI as service-tui - participant FW as firmware_test (CDC) - participant M5 as M5StampPLC +### Диагностика (firmware_test уже прошит) - OP->>TUI: запустить service_tui - OP->>TUI: подключить плату USB (нормальный режим) - TUI->>FW: ping → pong (CDC автодетект) - TUI->>FW: list_tests - FW-->>TUI: TestInfo × 8 - TUI-->>OP: показать список тестов +```bash +1. BOOT_MOD_1 → GND, сбросить плату +2. Подключить USB к сервисному ПК +3. TUI: WaitingScreen → обнаружен CDC 1996:00AD → DiagScreen +4. Выбрать тесты (или Все тесты) → Запустить +5. Ответить на интерактивные запросы (display, mqs) +6. Получить итог PASS / FAIL +``` - OP->>TUI: выбрать тесты → Запустить - TUI->>FW: run_selected([...]) +### Перепрошивка firmware_test - loop Для каждого теста - FW-->>TUI: test_begin - TUI-->>OP: прогресс +```bash +1. BOOT_MOD_1 → 3V3, сбросить плату +2. Подключить USB → TUI: FlashScreen +3. Выбрать firmware_test → Прошить +4. BOOT_MOD_1 → GND, сбросить плату +5. TUI автоматически переходит в DiagScreen +``` - alt HIL тест (opto/can) - FW-->>TUI: confirm_request - TUI->>M5: relay_set / can_send - TUI->>FW: confirm(true) - else Интерактивный (display/mqs) - FW-->>TUI: confirm_request - TUI-->>OP: показать prompt + countdown - OP->>TUI: OK / FAIL - TUI->>FW: confirm(true/false) - else Кнопки - FW-->>TUI: confirm_request (инструкция) - TUI-->>OP: "Нажмите кнопку..." - OP->>FW: физическое нажатие - end +### Chip Erase (сброс Flash в FF) - FW-->>TUI: test_result - TUI-->>OP: результат теста - end - - FW-->>TUI: summary - TUI-->>OP: итог: PASS / FAIL +```bash +1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3) +2. FlashScreen → Chip Erase (~30 с) +3. После erase: BootROM не загрузит прошивку — + необходимо перепрошить (пункт выше) ``` --- @@ -282,5 +676,19 @@ sequenceDiagram | `python-dotenv` | ≥ 1.0 | загрузка `.env` | | `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | -**Runtime зависимость (не в pyproject.toml):** -`tools/host/flash_usb.py` вызывается через `subprocess` с `uv run` — `tools/host/` uv-проект должен быть инициализирован (`just host::setup-tools`). +**Runtime-зависимость (не в `pyproject.toml`):** +`flasher.py` вызывает `tools/host/flash_usb.py` через `uv run` — uv-окружение `tools/host/` должно быть инициализировано командой `just host::setup-tools`. + +--- + +## Логирование + +TUI логирует в файл (не в stdout — Textual захватывает терминал): + +```bash +tools/production/service_tui.log ← по умолчанию +$SERVICE_LOG_DIR/service_tui.log ← если задан в .env +``` + +Уровень: `DEBUG` для всех модулей, `WARNING` для textual. +При standalone-запуске лог создаётся рядом с исполняемым файлом. diff --git a/tools/production/TUI_REPORT.md b/tools/production/TUI_REPORT.md new file mode 100644 index 0000000..ef9b6e6 --- /dev/null +++ b/tools/production/TUI_REPORT.md @@ -0,0 +1,209 @@ +# 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/app/app.py b/tools/production/app/app.py new file mode 100644 index 0000000..da08b36 --- /dev/null +++ b/tools/production/app/app.py @@ -0,0 +1,87 @@ +""" +app.py — корневое Textual приложение. + +Управляет сменой экранов и жизненным циклом клиентов +(FirmwareClient, M5Client). +""" + +from __future__ import annotations + +import logging +import os +from typing import Optional + +from textual import on, work +from textual.app import App +from textual.binding import Binding + +from .firmware_client import FirmwareClient +from .m5_client import M5Client +from .models import AppMode +from .screens import DiagScreen, FlashScreen, WaitingScreen + +logger = logging.getLogger(__name__) + +_CDC_VID = int(os.environ.get("SERVICE_CDC_VID", "0x1996"), 16) +_CDC_PID = int(os.environ.get("SERVICE_CDC_PID", "0x00ad"), 16) + + +class ServiceApp(App): + """Корневое приложение service-tui.""" + + TITLE = "TFT Board Service Tool" + BINDINGS = [ + Binding("ctrl+c", "quit", "Выход", show=True), + Binding("ctrl+q", "quit", "Выход"), + ] + CSS_PATH = "app.tcss" + + def __init__(self) -> None: + super().__init__() + self._fw: Optional[FirmwareClient] = None + self._m5: Optional[M5Client] = None + + def on_mount(self) -> None: + self.push_screen(WaitingScreen()) + + # ── Переходы между экранами ─────────────────────────────────────────────── + + @on(WaitingScreen.DeviceDetected) + def _on_device_detected(self, event: WaitingScreen.DeviceDetected) -> None: + if event.mode == AppMode.FLASHING: + self.switch_screen(FlashScreen()) + elif event.mode == AppMode.DIAGNOSING: + self._connect_and_diagnose() + + @on(FlashScreen.FlashDone) + def _on_flash_done(self, _event: FlashScreen.FlashDone) -> None: + """После прошивки — вернуться на WaitingScreen.""" + self.switch_screen(WaitingScreen()) + + @on(DiagScreen.DiagDone) + def _on_diag_done(self) -> None: + """После диагностики — отключиться, вернуться в Waiting.""" + self._disconnect() + self.switch_screen(WaitingScreen()) + + # ── Подключение к firmware_test ─────────────────────────────────────────── + + @work(thread=False) + async def _connect_and_diagnose(self) -> None: + try: + self._fw = await FirmwareClient.auto_connect(vid=_CDC_VID, pid=_CDC_PID) + except Exception as exc: + logger.error("CDC connect failed: %s", exc) + self.switch_screen(WaitingScreen()) + return + + self._m5 = await M5Client.auto_connect() + self.switch_screen(DiagScreen(firmware=self._fw, m5=self._m5)) + + def _disconnect(self) -> None: + if self._fw is not None: + self.call_later(self._fw.disconnect) + self._fw = None + if self._m5 is not None: + self.call_later(self._m5.disconnect) + self._m5 = None diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss new file mode 100644 index 0000000..602dffc --- /dev/null +++ b/tools/production/app/app.tcss @@ -0,0 +1,283 @@ +/* ── WaitingScreen ───────────────────────────────────────────────────────── */ + +WaitingScreen { + align: center middle; + layout: vertical; +} + +WaitingScreen > Static { + content-align: center middle; + width: 100%; +} + +#waiting-logo { + width: 52; + height: 7; + border: round $primary; + padding: 1 2; + content-align: center middle; + text-style: bold; + color: $primary; + margin-bottom: 0; +} + +#waiting-hint { + margin-top: 2; + color: $text-muted; + content-align: center middle; +} + +#waiting-spinner { + margin-top: 1; + color: $accent; + content-align: center middle; +} + +/* ── FlashScreen ─────────────────────────────────────────────────────────── */ + +FlashScreen { + padding: 1 2; +} + +#flash-title { + text-style: bold; + color: $warning; + margin-bottom: 1; +} + +#flash-target-group { + border: round $panel; + padding: 1 2; + margin-bottom: 1; + height: auto; +} + + +#flash-custom-path { + margin-top: 1; + height: auto; +} + +#flash-custom-path.hidden { + display: none; +} + +#flash-custom-input { + width: 1fr; +} + +#flash-btn-row { + height: auto; + margin-top: 1; + align: left middle; +} + +#flash-btn-flash { + margin-right: 2; +} + +#flash-btn-flash:disabled, +#flash-btn-erase:disabled { + background: $panel; + color: $text-muted; +} + +#flash-progress-bar { + margin-top: 1; +} + +#flash-progress-label { + color: $text-muted; +} + +#flash-log { + height: 1fr; + margin-top: 1; + border: round $panel; + background: $surface-darken-1; + overflow-y: auto; +} + +/* ── DiagScreen ──────────────────────────────────────────────────────────── */ + +DiagScreen { + layout: grid; + grid-size: 1; + grid-rows: 3 1fr 1 1 3 auto; +} + +/* Шапка */ +#diag-header { + height: 3; + background: $panel; + padding: 0 2; + align: left middle; +} + +#diag-header-fw { + color: $text-muted; + margin-right: 3; +} + +#diag-header-uid { + color: $text-muted; + margin-right: 3; +} + +#diag-header-m5 { + color: $success; +} + +#diag-header-m5.m5-absent { + color: $text-muted; +} + +/* Рабочая зона */ +#diag-main { + height: 1fr; +} + +/* Прогресс */ +#diag-progress-bar { + height: 1; + margin: 0 2; +} + +#diag-progress-label { + height: 1; + color: $text-muted; + padding: 0 2; +} + +/* Кнопки запуска */ +#diag-btn-row { + height: 3; + align: left middle; + padding: 0 2; + background: $panel; +} + +#diag-btn-run-selected { + margin-right: 2; +} + +#diag-btn-run-selected:disabled, +#diag-btn-run-all:disabled { + background: $panel; + color: $text-muted; +} + +/* ── TestListPanel ───────────────────────────────────────────────────────── */ + +TestListPanel { + width: 38; + border-right: solid $panel; + padding: 1 1; + overflow-y: auto; +} + +TestListPanel .section-title { + text-style: bold; + color: $text-muted; + padding: 0 0 1 0; +} + +.test-row { + align: left middle; + height: auto; +} + +.test-row-hil-badge { + width: 6; + color: $accent; +} + +.test-row-hil-badge.hil-disabled { + color: $text-muted; +} + +/* ── ResultsPanel ────────────────────────────────────────────────────────── */ + +ResultsPanel { + width: 1fr; + padding: 1 2; + overflow-y: auto; +} + +ResultsPanel .section-title { + text-style: bold; + color: $text-muted; + padding: 0 0 1 0; +} + +.result-row { + height: 1; + align: left middle; +} + +.result-id { + color: $text-muted; + width: 12; +} + +.result-status-pass { + color: $success; + width: 8; + text-style: bold; +} + +.result-status-fail { + color: $error; + width: 8; + text-style: bold; +} + +.result-status-running { + color: $warning; + width: 8; +} + +.result-status-skip { + color: $text-muted; + width: 8; +} + +.result-detail { + color: $text-muted; + width: 1fr; + overflow: hidden; +} + +/* ── ConfirmPanel ────────────────────────────────────────────────────────── */ + +ConfirmPanel { + height: auto; + background: $warning 15%; + border-top: solid $warning; + padding: 1 2; + align: left middle; +} + +ConfirmPanel.hidden { + display: none; +} + +#confirm-prompt { + width: 1fr; + color: $warning; + text-style: bold; +} + +#confirm-countdown { + color: $text-muted; + margin-right: 2; +} + +#confirm-btn-ok { + margin-right: 1; +} + +#confirm-btn-ok.hidden, +#confirm-btn-fail.hidden { + display: none; +} \ No newline at end of file diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py index 85e7b87..a20ee8e 100644 --- a/tools/production/app/firmware_client.py +++ b/tools/production/app/firmware_client.py @@ -28,7 +28,7 @@ from typing import AsyncGenerator, Optional import serial import serial.tools.list_ports -from .models import ConfirmRequest, TestInfo, TestResult, TestStatus +from .models import TestInfo, TestStatus logger = logging.getLogger(__name__) @@ -92,7 +92,7 @@ class FirmwareClient: async def connect(self) -> None: """Открыть порт и проверить связь через ping→pong.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._open) ok = await self.ping() if not ok: @@ -110,7 +110,7 @@ class FirmwareClient: async def disconnect(self) -> None: """Закрыть порт.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._close) def _close(self) -> None: @@ -119,7 +119,9 @@ class FirmwareClient: self._ser = None @classmethod - async def auto_connect(cls, vid: int, pid: int, baudrate: int = 115200) -> "FirmwareClient": + async def auto_connect( + cls, vid: int, pid: int, baudrate: int = 115200 + ) -> "FirmwareClient": """ Найти CDC-порт по VID/PID и подключиться. @@ -152,7 +154,7 @@ class FirmwareClient: async def _send(self, obj: dict) -> None: """Отправить JSON-команду (async wrapper).""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() async with self._lock: await loop.run_in_executor(None, self._write_line, obj) @@ -165,7 +167,7 @@ class FirmwareClient: Читать события до получения одного из stop_types или таймаута. Генератор — yield каждого полученного события. """ - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() deadline = loop.time() + timeout_s while loop.time() < deadline: event = await loop.run_in_executor(None, self._read_line) @@ -182,7 +184,7 @@ class FirmwareClient: async def ping(self) -> bool: """Отправить ping, ждать pong. Вернуть True при успехе.""" await self._send({"type": "cmd", "cmd": "ping"}) - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() deadline = loop.time() + _PING_TIMEOUT_S while loop.time() < deadline: event = await loop.run_in_executor(None, self._read_line) @@ -191,6 +193,14 @@ class FirmwareClient: await asyncio.sleep(0) return False + async def get_version(self) -> str: + """Запросить версию firmware_test. Вернуть строку X.Y.Z или ''.""" + await self._send({"type": "cmd", "cmd": "get_version"}) + async for event in self._recv_until({"version_response"}, timeout_s=3.0): + if event.get("type") == "version_response": + return event.get("fw", "") + return "" + async def list_tests(self) -> list[TestInfo]: """Запросить список тестов. Вернуть list[TestInfo].""" await self._send({"type": "cmd", "cmd": "list_tests"}) @@ -207,9 +217,7 @@ class FirmwareClient: ] return [] - async def run_selected( - self, test_ids: list[str] - ) -> AsyncGenerator[dict, None]: + async def run_selected(self, test_ids: list[str]) -> AsyncGenerator[dict, None]: """ Запустить выбранные тесты. Возвращает async generator событий: test_begin, test_result, confirm_request, summary, error. @@ -218,7 +226,9 @@ class FirmwareClient: send_confirm() не дожидаясь следующего события. """ await self._send({"type": "cmd", "cmd": "run_selected", "tests": test_ids}) - async for event in self._recv_until({"summary"}, timeout_s=_TEST_EVENT_TIMEOUT_S): + async for event in self._recv_until( + {"summary"}, timeout_s=_TEST_EVENT_TIMEOUT_S + ): yield event async def send_confirm(self, confirm_id: str, confirmed: bool) -> None: @@ -240,4 +250,4 @@ class FirmwareClient: @staticmethod def find_port(vid: int, pid: int) -> Optional[str]: """Найти CDC-порт по VID/PID. None если не найден.""" - return _find_cdc_port(vid, pid) \ No newline at end of file + return _find_cdc_port(vid, pid) diff --git a/tools/production/app/flasher.py b/tools/production/app/flasher.py index e4d4e37..c42b0d0 100644 --- a/tools/production/app/flasher.py +++ b/tools/production/app/flasher.py @@ -40,12 +40,32 @@ _HOST_TOOLS_DIR = _FLASH_USB_SCRIPT.parent _RE_PERCENT = re.compile(r"(\d{1,3})\s*%") _RE_PHASE = re.compile(r"(sdphost|blhost|Writing|Erasing|Verifying)", re.IGNORECASE) +_FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug") ProgressCallback = Callable[[FlashProgress], Awaitable[None]] def _detect_usb(vid: int, pid: int) -> bool: - """Проверить наличие USB-устройства по VID/PID (синхронно).""" + """ + Проверить наличие USB-устройства по VID/PID (синхронно). + + Два метода детекта: + 1. pyusb (usb.core) — видит все USB-устройства включая SDP bulk/HID + (на macOS SDP не создаёт serial-порт и невидим через list_ports). + 2. serial.tools.list_ports — fallback для CDC ACM устройств + если pyusb недоступен. + """ + # Метод 1: pyusb — работает для SDP и CDC + try: + import usb.core + + dev = usb.core.find(idVendor=vid, idProduct=pid) + if dev is not None: + return True + except Exception: + pass + + # Метод 2: serial list_ports — fallback для CDC ACM try: import serial.tools.list_ports @@ -54,6 +74,7 @@ def _detect_usb(vid: int, pid: int) -> bool: return True except Exception: pass + return False @@ -127,11 +148,11 @@ class Flasher: if target == FlashTarget.FIRMWARE_TEST: if bin_path is not None: return await self._run_flash_bin(bin_path, progress_cb) - return await self._run_flash("firmware_test", "Release", progress_cb) + return await self._run_flash("firmware_test", progress_cb) elif target == FlashTarget.PRODUCTION: - ok = await self._run_flash("bootloader", "Release", progress_cb) + ok = await self._run_flash("bootloader", progress_cb) if ok: - ok = await self._run_flash("app", "Release", progress_cb) + ok = await self._run_flash("app", progress_cb) return ok elif target == FlashTarget.CUSTOM: if bin_path is None: @@ -142,7 +163,6 @@ class Flasher: async def _run_flash( # прошивка стандартного firmware по имени self, firmware: str, - build_type: str, progress_cb: Optional[ProgressCallback], ) -> bool: """Запустить flash_usb.py для одного бинаря.""" @@ -156,7 +176,7 @@ class Flasher: "--firmware", firmware, "--build-type", - build_type, + _FIRMWARE_BUILD_TYPE, ] return await self._run_cmd(cmd, firmware, progress_cb) diff --git a/tools/production/app/m5_client.py b/tools/production/app/m5_client.py index a282622..99fe3e8 100644 --- a/tools/production/app/m5_client.py +++ b/tools/production/app/m5_client.py @@ -87,7 +87,7 @@ class M5Client: async def connect(self) -> None: """Открыть порт и проверить связь через ping.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._open) ok = await self.ping() if not ok: @@ -103,7 +103,7 @@ class M5Client: self._ser.reset_input_buffer() async def disconnect(self) -> None: - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._close) def _close(self) -> None: @@ -133,7 +133,7 @@ class M5Client: async def _cmd(self, cmd: dict) -> Optional[dict]: """Async wrapper над _send_recv.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() async with self._lock: return await loop.run_in_executor(None, self._send_recv, cmd) @@ -185,4 +185,4 @@ class M5Client: @staticmethod def find_port() -> Optional[str]: """Найти порт M5StampPLC. None если не найден.""" - return _find_m5_port() \ No newline at end of file + return _find_m5_port() diff --git a/tools/production/app/screens/__init__.py b/tools/production/app/screens/__init__.py new file mode 100644 index 0000000..dfe4305 --- /dev/null +++ b/tools/production/app/screens/__init__.py @@ -0,0 +1,7 @@ +"""screens — публичный экспорт экранов TUI.""" + +from .diag import DiagScreen +from .flash import FlashScreen +from .waiting import WaitingScreen + +__all__ = ["WaitingScreen", "FlashScreen", "DiagScreen"] diff --git a/tools/production/app/screens/diag/__init__.py b/tools/production/app/screens/diag/__init__.py new file mode 100644 index 0000000..3e3e1ef --- /dev/null +++ b/tools/production/app/screens/diag/__init__.py @@ -0,0 +1,261 @@ +""" +diag/__init__.py — экран диагностики (режим B). + +DiagScreen координирует три виджета: + TestListPanel — выбор тестов (левая колонка) + ResultsPanel — результаты (правая колонка) + ConfirmPanel — confirm_request оператора (нижняя панель) + +и Orchestrator — маршрутизатор confirm_request. +""" + +from __future__ import annotations + +import logging +from typing import Optional + +from textual import on, work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal +from textual.css.query import NoMatches +from textual.message import Message +from textual.screen import Screen +from textual.widgets import Button, Label, ProgressBar, Static + +from ...firmware_client import FirmwareClient +from ...m5_client import M5Client +from ...models import SessionState +from ...orchestrator import Orchestrator, OrchestratorEvent, OrchestratorEventType +from .confirm_panel import ConfirmPanel +from .results import ResultsPanel +from .test_list import TestListPanel + +logger = logging.getLogger(__name__) + + +class DiagScreen(Screen): + """ + Экран диагностики. + + Messages: + DiagDone() — сессия завершена (плата отключена или summary получен) + """ + + BINDINGS = [ + Binding("escape", "go_back", "Отключиться"), + Binding("r", "run_all", "Все тесты"), + Binding("s", "run_selected", "Выбранные"), + ] + + class DiagDone(Message): + """Сессия диагностики завершена.""" + + def __init__( + self, + firmware: FirmwareClient, + m5: Optional[M5Client] = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self._fw = firmware + self._m5 = m5 + self._orchestrator = Orchestrator(firmware, m5) + self._session = SessionState() + self._running = False + + def compose(self) -> ComposeResult: + # Шапка + with Horizontal(id="diag-header"): + yield Static("fw: —", id="diag-header-fw") + yield Static("UID: —", id="diag-header-uid") + yield Static("M5: —", id="diag-header-m5") + + # Рабочая зона: список тестов + результаты + with Horizontal(id="diag-main"): + yield TestListPanel(id="diag-test-list") + yield ResultsPanel(id="diag-results") + + # Прогресс + yield ProgressBar(id="diag-progress-bar", show_eta=False) + yield Label("", id="diag-progress-label") + + # Кнопки запуска + with Horizontal(id="diag-btn-row"): + yield Button( + "▶ Запустить выбранные", + id="diag-btn-run-selected", + variant="primary", + disabled=True, + ) + yield Button( + "▶▶ Все тесты", + id="diag-btn-run-all", + variant="default", + disabled=True, + ) + + # Панель confirm + yield ConfirmPanel(id="diag-confirm", classes="hidden") + + def on_mount(self) -> None: + self._init_session() + + # ── Инициализация сессии ───────────────────────────────────────────────── + + @work(thread=False) + async def _init_session(self) -> None: + try: + tests = await self._fw.list_tests() + uid = await self._fw.get_uid() + version = await self._fw.get_version() + except Exception as exc: + logger.error("Session init failed: %s", exc) + self.post_message(self.DiagDone()) + return + + self._session.tests = tests + self._session.chip_uid = uid + self._session.fw_version = version + self._session.m5_connected = self._m5 is not None + + self._update_header() + + test_list = self.query_one("#diag-test-list", TestListPanel) + test_list.populate(tests, m5_connected=self._session.m5_connected) + + self.query_one("#diag-results", ResultsPanel).populate(tests) + self._set_run_buttons(enabled=True) + + def _update_header(self) -> None: + self.query_one("#diag-header-fw", Static).update( + f"fw: {self._session.fw_version or '?'}" + ) + uid_short = self._session.chip_uid[:16] if self._session.chip_uid else "—" + self.query_one("#diag-header-uid", Static).update(f"UID: {uid_short}") + + m5_widget = self.query_one("#diag-header-m5", Static) + if self._session.m5_connected: + m5_widget.update("M5: ✓ подключён") + m5_widget.remove_class("m5-absent") + else: + m5_widget.update("M5: — нет") + m5_widget.add_class("m5-absent") + + # ── Кнопки ─────────────────────────────────────────────────────────────── + + @on(Button.Pressed, "#diag-btn-run-selected") + def _on_run_selected(self) -> None: + ids = self.query_one("#diag-test-list", TestListPanel).get_selected_ids() + if ids: + self._start_run(ids) + + @on(Button.Pressed, "#diag-btn-run-all") + def _on_run_all(self) -> None: + ids = [t.id for t in self._session.tests] + if ids: + self._start_run(ids) + + def action_go_back(self) -> None: + if not self._running: + self.post_message(self.DiagDone()) + + def action_run_all(self) -> None: + self._on_run_all() + + def action_run_selected(self) -> None: + self._on_run_selected() + + # ── Confirm ─────────────────────────────────────────────────────────────── + + @on(ConfirmPanel.Confirmed) + def _on_confirmed(self, event: ConfirmPanel.Confirmed) -> None: + """Оператор ответил — передать в оркестратор.""" + self.app.call_later( + self._orchestrator.resolve_operator_confirm, event.confirmed + ) + + # ── Запуск тестов ──────────────────────────────────────────────────────── + + def _start_run(self, test_ids: list[str]) -> None: + results = self.query_one("#diag-results", ResultsPanel) + results.reset() + self._session.results.clear() + self._update_progress(0, len(test_ids), "") + self._set_run_buttons(enabled=False) + self._running = True + self._run_worker(test_ids) + + @work(exclusive=True, thread=False) + async def _run_worker(self, test_ids: list[str]) -> None: + total = len(test_ids) + done = 0 + try: + async for event in self._orchestrator.run_tests(test_ids): + self._handle_event(event, total, done) + if event.type == OrchestratorEventType.TEST_RESULT: + done += 1 + if event.type == OrchestratorEventType.SUMMARY: + break + except Exception as exc: + logger.error("run_worker error: %s", exc) + finally: + self._running = False + self._set_run_buttons(enabled=True) + try: + self.query_one("#diag-confirm", ConfirmPanel).hide() + except NoMatches: + pass + + def _handle_event(self, event: OrchestratorEvent, total: int, done: int) -> None: + results = self.query_one("#diag-results", ResultsPanel) + confirm = self.query_one("#diag-confirm", ConfirmPanel) + + if event.type == OrchestratorEventType.TEST_BEGIN: + results.set_running(event.test_id) + self._update_progress(done, total, f"Тест: {event.test_id}") + + elif event.type == OrchestratorEventType.TEST_RESULT: + assert event.result is not None + self._session.set_result(event.result) + results.set_result(event.result) + + elif event.type == OrchestratorEventType.CONFIRM_NEEDED: + assert event.confirm is not None + confirm.show_operator( + prompt=event.confirm.prompt, + timeout_ms=event.confirm.timeout_ms, + ) + + elif event.type == OrchestratorEventType.CONFIRM_RESOLVED: + self._update_progress(done, total, f"HIL: {event.message}") + + elif event.type == OrchestratorEventType.BUTTONS_PROMPT: + assert event.confirm is not None + confirm.show_buttons_hint(event.confirm.prompt) + + elif event.type == OrchestratorEventType.SUMMARY: + self._on_summary(event.summary or {}) + + elif event.type == OrchestratorEventType.ERROR: + self._update_progress(done, total, f"⚠ {event.message}") + + def _on_summary(self, summary: dict) -> None: + overall = summary.get("overall", "fail") + passed = summary.get("passed", 0) + failed = summary.get("failed", 0) + icon = "✅" if overall == "pass" else "❌" + self.query_one("#diag-progress-label", Label).update( + f"{icon} Итог: {passed} прошли, {failed} не прошли" + ) + + # ── Утилиты ─────────────────────────────────────────────────────────────── + + def _update_progress(self, done: int, total: int, msg: str) -> None: + pct = int(done / total * 100) if total else 0 + self.query_one("#diag-progress-bar", ProgressBar).update(progress=pct) + self.query_one("#diag-progress-label", Label).update(msg) + + def _set_run_buttons(self, enabled: bool) -> None: + self.query_one("#diag-btn-run-selected", Button).disabled = not enabled + self.query_one("#diag-btn-run-all", Button).disabled = not enabled diff --git a/tools/production/app/screens/diag/confirm_panel.py b/tools/production/app/screens/diag/confirm_panel.py new file mode 100644 index 0000000..0570a91 --- /dev/null +++ b/tools/production/app/screens/diag/confirm_panel.py @@ -0,0 +1,140 @@ +""" +confirm_panel.py — виджет панели подтверждения оператора. + +Показывается при confirm_request от firmware_test требующем ответа оператора. +Скрыт по умолчанию (CSS-класс 'hidden'). + +Messages: + ConfirmPanel.Confirmed(confirmed: bool) — оператор нажал OK или Нет +""" + +from __future__ import annotations + +from textual import on +from textual.app import ComposeResult +from textual.css.query import NoMatches +from textual.message import Message +from textual.timer import Timer +from textual.widget import Widget +from textual.widgets import Button, Static + +_TICK_S = 1.0 + + +class ConfirmPanel(Widget): + """ + Панель подтверждения оператора. + + Использование:: + + panel = ConfirmPanel() + panel.show(prompt="Экран залит красным?", timeout_ms=30000) + # Слушать ConfirmPanel.Confirmed в родительском экране + """ + + class Confirmed(Message): + """Оператор ответил на confirm_request.""" + + def __init__(self, confirmed: bool) -> None: + super().__init__() + self.confirmed = confirmed + + DEFAULT_CSS = "" # стили в diag.tcss + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._timer: Timer | None = None + self._remaining: int = 0 + self._operator_mode: bool = True # False → режим buttons (нет кнопок) + + def compose(self) -> ComposeResult: + yield Static("", id="confirm-prompt") + yield Static("", id="confirm-countdown") + yield Button("✓ Да", id="confirm-btn-ok", variant="success") + yield Button("✗ Нет", id="confirm-btn-fail", variant="error") + + # ── Public API ──────────────────────────────────────────────────────────── + + def show_operator(self, prompt: str, timeout_ms: int) -> None: + """Показать панель с кнопками OK/Нет и countdown.""" + self._operator_mode = True + self._remaining = timeout_ms // 1000 + self._set_prompt(prompt) + self._set_countdown(self._remaining) + self._show_buttons(True) + self.remove_class("hidden") + self._start_timer() + + def show_buttons_hint(self, prompt: str) -> None: + """ + Показать инструкцию для теста кнопок. + Без кнопок OK/Нет — оператор только читает, нажимает физическую кнопку. + """ + self._operator_mode = False + self._stop_timer() + self._set_prompt(f"⌨ {prompt}") + self._set_countdown("") + self._show_buttons(False) + self.remove_class("hidden") + + def hide(self) -> None: + """Скрыть панель, остановить таймер.""" + self._stop_timer() + self.add_class("hidden") + self._show_buttons(True) # восстановить на следующий раз + + # ── Обработчики ─────────────────────────────────────────────────────────── + + @on(Button.Pressed, "#confirm-btn-ok") + def _on_ok(self) -> None: + self.hide() + self.post_message(self.Confirmed(confirmed=True)) + + @on(Button.Pressed, "#confirm-btn-fail") + def _on_fail(self) -> None: + self.hide() + self.post_message(self.Confirmed(confirmed=False)) + + # ── Таймер countdown ───────────────────────────────────────────────────── + + def _start_timer(self) -> None: + self._stop_timer() + self._timer = self.set_interval(_TICK_S, self._tick) + + def _stop_timer(self) -> None: + if self._timer is not None: + self._timer.stop() + self._timer = None + + def _tick(self) -> None: + self._remaining -= 1 + self._set_countdown(self._remaining) + if self._remaining <= 0: + self.hide() + self.post_message(self.Confirmed(confirmed=False)) + + # ── Утилиты ─────────────────────────────────────────────────────────────── + + def _set_prompt(self, text: str) -> None: + try: + self.query_one("#confirm-prompt", Static).update(f"⚠ {text}") + except NoMatches: + pass + + def _set_countdown(self, value: int | str) -> None: + text = f"{value}с" if isinstance(value, int) and value > 0 else "" + try: + self.query_one("#confirm-countdown", Static).update(text) + except NoMatches: + pass + + def _show_buttons(self, visible: bool) -> None: + for btn_id in ("#confirm-btn-ok", "#confirm-btn-fail"): + try: + btn = self.query_one(btn_id, Button) + if visible: + btn.remove_class("hidden") + else: + btn.add_class("hidden") + except NoMatches: + pass diff --git a/tools/production/app/screens/diag/results.py b/tools/production/app/screens/diag/results.py new file mode 100644 index 0000000..5045b79 --- /dev/null +++ b/tools/production/app/screens/diag/results.py @@ -0,0 +1,115 @@ +""" +results.py — виджет правой колонки DiagScreen. + +Отображает строки результатов тестов: id | статус | detail. +Обновляется по событиям от Orchestrator через DiagScreen. + +Публичный API: + ResultsPanel.populate(tests) — инициализировать пустые строки + ResultsPanel.set_running(test_id) — показать "выполняется" + ResultsPanel.set_result(result) — показать финальный результат + ResultsPanel.reset() — сбросить все строки +""" + +from __future__ import annotations + +from textual.app import ComposeResult +from textual.containers import Horizontal +from textual.css.query import NoMatches +from textual.widget import Widget +from textual.widgets import Label, Static + +from ...models import TestResult, TestStatus + + +def _status_display(status: TestStatus) -> tuple[str, str]: + """Вернуть (текст, css-класс) для статуса.""" + return { + TestStatus.PASS: ("✓ PASS", "result-status-pass"), + TestStatus.FAIL: ("✗ FAIL", "result-status-fail"), + TestStatus.RUNNING: ("…", "result-status-running"), + TestStatus.SKIP: ("SKIP", "result-status-skip"), + TestStatus.PENDING: ("", "result-status-skip"), + }[status] + + +# CSS-классы статусов — для очистки перед сменой +_STATUS_CLASSES = ( + "result-status-pass", + "result-status-fail", + "result-status-running", + "result-status-skip", +) + + +class ResultsPanel(Widget): + """Правая колонка DiagScreen — результаты тестов.""" + + DEFAULT_CSS = "" # стили в diag.tcss + + def compose(self) -> ComposeResult: + yield Label("Результаты", classes="section-title") + + # ── Public API ──────────────────────────────────────────────────────────── + + def populate(self, tests: list) -> None: + """ + Инициализировать пустые строки результатов. + + :param tests: list[TestInfo] + """ + for child in list(self.children): + if not child.has_class("section-title"): + child.remove() + + for test in tests: + row = Horizontal(classes="result-row", id=f"result-row-{test.id}") + id_lbl = Static(test.id, classes="result-id") + status_lbl = Static( + "", classes="result-status-skip", id=f"result-status-{test.id}" + ) + detail_lbl = Static( + "", classes="result-detail", id=f"result-detail-{test.id}" + ) + self.mount(row) + row.mount(id_lbl) + row.mount(status_lbl) + row.mount(detail_lbl) + + def set_running(self, test_id: str) -> None: + """Пометить тест как выполняющийся.""" + self._update(test_id, TestStatus.RUNNING, "") + + def set_result(self, result: TestResult) -> None: + """Показать финальный результат теста.""" + detail = result.detail if result.status == TestStatus.FAIL else "" + self._update(result.id, result.status, detail) + + def reset(self) -> None: + """Сбросить все строки в пустое состояние.""" + for widget in self.query(Static): + wid = widget.id or "" + if wid.startswith("result-status-"): + for cls in _STATUS_CLASSES: + widget.remove_class(cls) + widget.add_class("result-status-skip") + widget.update("") + elif wid.startswith("result-detail-"): + widget.update("") + + # ── Internal ────────────────────────────────────────────────────────────── + + def _update(self, test_id: str, status: TestStatus, detail: str) -> None: + try: + status_w = self.query_one(f"#result-status-{test_id}", Static) + detail_w = self.query_one(f"#result-detail-{test_id}", Static) + except NoMatches: + return + + for cls in _STATUS_CLASSES: + status_w.remove_class(cls) + + text, css = _status_display(status) + status_w.update(text) + status_w.add_class(css) + detail_w.update(detail) diff --git a/tools/production/app/screens/diag/test_list.py b/tools/production/app/screens/diag/test_list.py new file mode 100644 index 0000000..14bcad8 --- /dev/null +++ b/tools/production/app/screens/diag/test_list.py @@ -0,0 +1,83 @@ +""" +test_list.py — виджет списка тестов с чекбоксами. + +Отображает тесты из list_tests, отмечает HIL-тесты, +серит недоступные (HIL без M5). + +Публичный API: + TestListPanel.populate(tests, m5_connected) — заполнить список + TestListPanel.get_selected_ids() — список выбранных id + TestListPanel.set_enabled(enabled) — блокировать во время прогона +""" + +from __future__ import annotations + +from textual.app import ComposeResult +from textual.containers import Horizontal +from textual.widget import Widget +from textual.widgets import Checkbox, Label + + +class TestListPanel(Widget): + """Левая колонка DiagScreen — список тестов с чекбоксами.""" + + DEFAULT_CSS = "" # стили в diag.tcss + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + # test_id → Checkbox для быстрого доступа + self._checkboxes: dict[str, Checkbox] = {} + + def compose(self) -> ComposeResult: + yield Label("Тесты", classes="section-title") + + # ── Public API ──────────────────────────────────────────────────────────── + + def populate(self, tests: list, m5_connected: bool) -> None: + """ + Заполнить список тестами. + + :param tests: list[TestInfo] + :param m5_connected: True если M5StampPLC подключён + """ + # Удалить старые строки (кроме заголовка) + for child in list(self.children): + if not child.has_class("section-title"): + child.remove() + self._checkboxes.clear() + + for test in tests: + hil_unavailable = test.requires_hil and not m5_connected + + cb = Checkbox( + test.name, + value=not hil_unavailable, + disabled=hil_unavailable, + id=f"cb-{test.id}", + classes="-textual-compact", + ) + self._checkboxes[test.id] = cb + + hil_css = ( + "test-row-hil-badge hil-disabled" + if hil_unavailable + else "test-row-hil-badge" + ) + badge_text = "[HIL]" if test.requires_hil else "" + + row = Horizontal(classes="test-row") + self.mount(row) + row.mount(cb) + row.mount(Label(badge_text, classes=hil_css)) + + def get_selected_ids(self) -> list[str]: + """Вернуть список id выбранных (checked + not disabled) тестов.""" + return [ + tid for tid, cb in self._checkboxes.items() if cb.value and not cb.disabled + ] + + def set_enabled(self, enabled: bool) -> None: + """Разрешить/запретить изменение чекбоксов во время прогона.""" + for cb in self._checkboxes.values(): + if not cb.disabled: # не трогать серые HIL-без-M5 + cb.disabled = not enabled diff --git a/tools/production/app/screens/flash.py b/tools/production/app/screens/flash.py new file mode 100644 index 0000000..a2cce04 --- /dev/null +++ b/tools/production/app/screens/flash.py @@ -0,0 +1,197 @@ +""" +flash.py — экран прошивки (режим A). + +Активен когда обнаружен BootROM SDP (1FC9:0130). +Поддерживает три варианта прошивки и chip erase. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Optional + +from textual import on, work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.css.query import NoMatches +from textual.message import Message +from textual.screen import Screen +from textual.widgets import ( + Button, + Input, + Label, + Log, + ProgressBar, + RadioButton, + RadioSet, +) + +from ..flasher import Flasher +from ..models import FlashProgress, FlashTarget + +logger = logging.getLogger(__name__) + + +class FlashScreen(Screen): + """ + Экран прошивки. + + Messages: + FlashDone(success) — прошивка завершена + """ + + BINDINGS = [ + Binding("escape", "go_back", "Назад"), + ] + + class FlashDone(Message): + def __init__(self, success: bool) -> None: + super().__init__() + self.success = success + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._flasher = Flasher() + self._flashing = False + + def compose(self) -> ComposeResult: + with Vertical(): + yield Label( + "⚡ Прошивка платы — BootROM SDP обнаружен", + id="flash-title", + ) + + with Vertical(id="flash-target-group"): + yield Label("Что прошить?", classes="section-title") + with RadioSet(id="flash-radio"): + yield RadioButton( + "firmware_test (диагностическая прошивка)", + id="radio-fw-test", + value=True, + ) + yield RadioButton( + "Production (bootloader + tft_app)", + id="radio-production", + ) + yield RadioButton( + "Кастомный бинарь...", + id="radio-custom", + ) + with Horizontal(id="flash-custom-path", classes="hidden"): + yield Input( + placeholder="Путь к HAB-бинарю (.bin)", + id="flash-custom-input", + ) + + with Horizontal(id="flash-btn-row"): + yield Button( + "▶ Прошить", + id="flash-btn-flash", + variant="warning", + ) + yield Button( + "⚠ Chip Erase", + id="flash-btn-erase", + variant="error", + ) + + yield ProgressBar(id="flash-progress-bar", show_eta=False) + yield Label("", id="flash-progress-label") + yield Log(id="flash-log", auto_scroll=True) + + # ── Обработчики ─────────────────────────────────────────────────────────── + + @on(RadioSet.Changed, "#flash-radio") + def _on_radio_changed(self, event: RadioSet.Changed) -> None: + is_custom = event.pressed.id == "radio-custom" + path_row = self.query_one("#flash-custom-path") + if is_custom: + path_row.remove_class("hidden") + else: + path_row.add_class("hidden") + + @on(Button.Pressed, "#flash-btn-flash") + def _on_flash_pressed(self) -> None: + if self._flashing: + return + target, bin_path = self._resolve_target() + if target is None: + self._log("⚠ Укажите путь к бинарю") + return + self._do_flash(target, bin_path) + + @on(Button.Pressed, "#flash-btn-erase") + def _on_erase_pressed(self) -> None: + if self._flashing: + return + self._do_erase() + + def action_go_back(self) -> None: + if not self._flashing: + self.post_message(self.FlashDone(success=False)) + + # ── Workers ─────────────────────────────────────────────────────────────── + + @work(exclusive=True, thread=False) + async def _do_flash(self, target: FlashTarget, bin_path: Optional[Path]) -> None: + self._set_busy(True) + self._log(f"▶ Прошивка: {target.value}") + ok = await self._flasher.flash( + target=target, + bin_path=bin_path, + progress_cb=self._on_progress, + ) + self._set_busy(False) + self._log("✅ Готово" if ok else "❌ Ошибка") + self.post_message(self.FlashDone(success=ok)) + + @work(exclusive=True, thread=False) + async def _do_erase(self) -> None: + self._set_busy(True) + self._log("⚠ Chip erase (~30 с)...") + ok = await self._flasher.erase_chip(progress_cb=self._on_progress) + self._set_busy(False) + self._log("✅ Chip erase завершён" if ok else "❌ Chip erase: ошибка") + + # ── Вспомогательные ─────────────────────────────────────────────────────── + + def _resolve_target(self) -> tuple[Optional[FlashTarget], Optional[Path]]: + radio = self.query_one("#flash-radio", RadioSet) + pressed_id = radio.pressed_button.id if radio.pressed_button else None + + if pressed_id == "radio-fw-test": + return FlashTarget.FIRMWARE_TEST, None + if pressed_id == "radio-production": + return FlashTarget.PRODUCTION, None + if pressed_id == "radio-custom": + raw = self.query_one("#flash-custom-input", Input).value.strip() + if not raw: + return None, None + p = Path(raw) + if not p.exists(): + self._log(f"⚠ Файл не найден: {p}") + return None, None + return FlashTarget.CUSTOM, p + return None, None + + async def _on_progress(self, progress: FlashProgress) -> None: + self.query_one("#flash-progress-bar", ProgressBar).update( + progress=progress.percent + ) + self.query_one("#flash-progress-label", Label).update( + f"{progress.phase} {progress.percent}%" + ) + self._log(progress.message) + + def _set_busy(self, busy: bool) -> None: + self._flashing = busy + self.query_one("#flash-btn-flash", Button).disabled = busy + self.query_one("#flash-btn-erase", Button).disabled = busy + + def _log(self, msg: str) -> None: + try: + self.query_one("#flash-log", Log).write_line(msg) + except NoMatches: + pass diff --git a/tools/production/app/screens/waiting.py b/tools/production/app/screens/waiting.py new file mode 100644 index 0000000..379eb1a --- /dev/null +++ b/tools/production/app/screens/waiting.py @@ -0,0 +1,83 @@ +""" +waiting.py — экран ожидания подключения платы. + +Опрашивает USB каждые _DETECT_INTERVAL_S секунд через Flasher. +При обнаружении SDP или CDC отправляет DeviceDetected message в App. +""" + +from __future__ import annotations + +from textual.app import ComposeResult +from textual.css.query import NoMatches +from textual.screen import Screen +from textual.timer import Timer +from textual.widgets import Static +from textual.message import Message +from ..flasher import Flasher +from ..models import AppMode + +_DETECT_INTERVAL_S = 1.5 +_SPIN_INTERVAL_S = 0.1 +_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + + +class WaitingScreen(Screen): + """ + Экран ожидания. + + Messages: + DeviceDetected(mode) — плата обнаружена, mode: FLASHING | DIAGNOSING + """ + + class DeviceDetected(Message): + """Плата обнаружена.""" + + def __init__(self, mode: AppMode) -> None: + super().__init__() + self.mode = mode + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._spinner_idx: int = 0 + self._detect_timer: Timer | None = None + self._spin_timer: Timer | None = None + + def compose(self) -> ComposeResult: + yield Static("TFT Indicator Board\nService Tool", id="waiting-logo") + yield Static("Подключите плату к USB...", id="waiting-hint") + yield Static(_SPINNER_FRAMES[0], id="waiting-spinner") + + def on_mount(self) -> None: + self._detect_timer = self.set_interval(_DETECT_INTERVAL_S, self._poll_usb) + self._spin_timer = self.set_interval(_SPIN_INTERVAL_S, self._spin) + + def on_unmount(self) -> None: + if self._detect_timer: + self._detect_timer.stop() + if self._spin_timer: + self._spin_timer.stop() + + # ── Internal ────────────────────────────────────────────────────────────── + + def _spin(self) -> None: + self._spinner_idx = (self._spinner_idx + 1) % len(_SPINNER_FRAMES) + try: + self.query_one("#waiting-spinner", Static).update( + _SPINNER_FRAMES[self._spinner_idx] + ) + except NoMatches: + pass + + def _poll_usb(self) -> None: + if Flasher.detect_sdp(): + self._stop_timers() + self.post_message(self.DeviceDetected(AppMode.FLASHING)) + elif Flasher.detect_cdc(): + self._stop_timers() + self.post_message(self.DeviceDetected(AppMode.DIAGNOSING)) + + def _stop_timers(self) -> None: + if self._detect_timer: + self._detect_timer.stop() + if self._spin_timer: + self._spin_timer.stop() diff --git a/tools/production/main.py b/tools/production/main.py index e69de29..10ec203 100644 --- a/tools/production/main.py +++ b/tools/production/main.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""main.py — точка входа service-tui.""" + +from __future__ import annotations + +import logging +import os +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_ENV_FILE = _REPO_ROOT / ".env" + +try: + from dotenv import load_dotenv + + if _ENV_FILE.exists(): + load_dotenv(_ENV_FILE) +except ImportError: + pass + + +def _setup_logging() -> None: + log_dir = Path(os.environ.get("SERVICE_LOG_DIR", str(Path(__file__).parent))) + log_file = log_dir / "service_tui.log" + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(levelname)-8s %(name)s %(message)s", + handlers=[logging.FileHandler(log_file, encoding="utf-8")], + ) + logging.getLogger("textual").setLevel(logging.WARNING) + + +def main() -> None: + _setup_logging() + from app.app import ServiceApp + + ServiceApp().run() + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/tools/production/pyproject.toml b/tools/production/pyproject.toml index b7f6ac8..20b5d72 100644 --- a/tools/production/pyproject.toml +++ b/tools/production/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "pyserial>=3.5", "python-dotenv>=1.0.0", "pyinstaller>=6.0.0", + "pyusb>=1.0.0", ] [project.scripts] diff --git a/tools/production/service_tui.log b/tools/production/service_tui.log new file mode 100644 index 0000000..ead3ed2 --- /dev/null +++ b/tools/production/service_tui.log @@ -0,0 +1,662 @@ +2026-06-29 15:20:18,732 DEBUG asyncio Using selector: EpollSelector +2026-06-29 18:21:10,465 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:21:14,987 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:21:53,409 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:22:16,437 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:28:38,572 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:36:00,529 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:36:17,980 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-29 18:36:18,002 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 18:36:18,002 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-29 18:36:18,046 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-29 18:36:18,224 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv +2026-06-29 18:36:18,224 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv +2026-06-29 18:36:18,344 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 93ms +2026-06-29 18:36:23,001 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:36:23,001 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 18:36:23,177 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 18:36:23,198 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 18:36:23,198 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:24,586 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 18:36:24,760 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 18:36:59,315 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 18:36:59,571 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: +2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-29 18:37:20,822 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-29 18:37:20,836 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 18:37:21,381 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:37:21,381 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 18:37:21,559 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 18:37:21,583 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 18:37:21,583 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:22,972 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 18:37:23,148 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:23,979 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:24,183 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:24,357 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:24,534 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 18:37:24,594 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:24,594 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 22616 (0x5858) +2026-06-29 18:37:24,779 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Размер: 22616 байт +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +28672 байт +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 28672 0 +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 18:37:46,867 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-29 18:37:46,883 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 18:37:47,377 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:37:47,377 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 18:37:47,548 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 18:37:47,569 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 18:37:47,569 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:48,972 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 18:37:49,154 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:49,987 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:50,206 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:50,386 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:50,560 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 18:37:50,621 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:50,621 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 22616 (0x5858) +2026-06-29 18:37:50,800 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Размер: 22616 байт +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +28672 байт +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 28672 0 +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 18:38:46,907 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:40:26,915 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:40:28,552 INFO app.m5_client M5StampPLC не найден +2026-06-29 18:41:26,213 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-29 18:44:38,127 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:44:39,837 INFO app.m5_client M5StampPLC не найден +2026-06-29 18:45:28,221 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-29 18:46:57,397 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:46:58,957 INFO app.m5_client M5StampPLC не найден +2026-06-29 18:50:48,270 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:51:01,877 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-29 18:51:02,032 DEBUG app.flasher flash_usb [firmware_test]: Removed virtual environment at: .venv +2026-06-29 18:51:02,032 DEBUG app.flasher flash_usb [firmware_test]: Creating virtual environment at: .venv +2026-06-29 18:51:02,130 DEBUG app.flasher flash_usb [firmware_test]: Installed 66 packages in 75ms +2026-06-29 18:51:06,727 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:51:06,727 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 18:51:06,903 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:08,331 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 18:51:08,506 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:09,215 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:09,429 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:09,621 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:09,804 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 18:51:09,985 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:09,985 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 72116 (0x119b4) +2026-06-29 18:51:10,173 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 18:51:10,191 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Размер: 72116 байт +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +77824 байт +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 77824 0 +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 18:51:36,969 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:57:30,698 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 18:57:32,409 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:00:37,411 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:00:38,953 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:04:04,441 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:04:12,903 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-29 19:04:12,918 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:04:12,919 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-29 19:04:12,921 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-29 19:04:13,106 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv +2026-06-29 19:04:13,106 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv +2026-06-29 19:04:13,225 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 93ms +2026-06-29 19:04:17,826 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:04:17,826 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:04:18,001 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:04:18,019 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:18,019 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:19,412 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:04:19,586 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:04:54,910 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:04:55,150 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-29 19:04:58,515 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug +2026-06-29 19:04:58,542 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:04:58,955 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:04:58,955 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:04:59,131 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:04:59,150 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:00,537 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:05:00,713 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:01,631 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:01,832 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:02,011 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:02,191 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 19:05:02,527 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:02,527 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) +2026-06-29 19:05:02,734 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:05:02,754 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 19:05:23,969 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:05:25,545 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:07:57,549 WARNING app.firmware_client _recv_until timeout after 120.0 s +2026-06-29 19:17:54,908 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:18:31,735 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-29 19:18:31,752 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:18:31,752 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-29 19:18:31,755 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-29 19:18:31,934 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv +2026-06-29 19:18:31,935 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv +2026-06-29 19:18:32,066 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 98ms +2026-06-29 19:18:36,600 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:18:36,600 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:18:36,776 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:18:38,238 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:18:38,414 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:19:13,145 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:19:13,403 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-29 19:20:01,708 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug +2026-06-29 19:20:01,724 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:20:02,300 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:20:02,300 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:20:02,473 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:03,887 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:20:04,068 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:04,968 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:05,182 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:05,355 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:05,534 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 19:20:05,873 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:05,873 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) +2026-06-29 19:20:06,062 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 19:20:59,572 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug +2026-06-29 19:20:59,595 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:21:00,152 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:21:00,152 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:21:00,330 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:01,734 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:21:01,904 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:02,832 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:03,049 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:03,224 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:03,447 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 19:21:03,786 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:03,787 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) +2026-06-29 19:21:04,008 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 19:21:20,423 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-29 19:21:20,437 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:21:20,936 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:21:20,936 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:21:21,112 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:21:21,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:21:21,134 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:22,510 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:21:22,685 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:21:57,783 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:21:58,044 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: +2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-29 19:31:03,319 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:31:12,383 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug +2026-06-29 19:31:12,399 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-29 19:31:12,928 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:31:12,928 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-29 19:31:13,096 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-29 19:31:13,118 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-29 19:31:13,118 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:14,509 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-29 19:31:14,679 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:15,624 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:15,830 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:16,009 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:16,183 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-29 19:31:16,518 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:16,518 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) +2026-06-29 19:31:16,724 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт +2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-29 19:31:45,595 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:31:54,684 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:31:57,741 WARNING app.firmware_client _recv_until timeout after 3.0 s +2026-06-29 19:34:42,219 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-29 19:36:35,663 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:36:37,208 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:36:40,255 WARNING app.firmware_client _recv_until timeout after 3.0 s +2026-06-29 19:36:47,180 DEBUG asyncio Using selector: KqueueSelector +2026-06-29 19:41:53,210 INFO app.m5_client M5StampPLC не найден +2026-06-29 19:41:56,267 WARNING app.firmware_client _recv_until timeout after 3.0 s diff --git a/tools/production/uv.lock b/tools/production/uv.lock new file mode 100644 index 0000000..b041381 --- /dev/null +++ b/tools/production/uv.lock @@ -0,0 +1,264 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "altgraph" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "macholib" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyinstaller" +version = "6.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, + { name = "macholib", marker = "sys_platform == 'darwin'" }, + { name = "packaging" }, + { name = "pefile", marker = "sys_platform == 'win32'" }, + { name = "pyinstaller-hooks-contrib" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/ec706c3fcf39e26888c35b39615ff4d5865d184069666c47492cff1fbe50/pyinstaller-6.21.0.tar.gz", hash = "sha256:bb9fab705983e393a2d1cac77d6972513057ad800215fd861dc15ff5272e98fd", size = 4061519, upload-time = "2026-06-13T14:15:06.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4a/53cf98bf66daed012dc9cd78c8203f19a675d696f2fc12afcf8c5049a0e0/pyinstaller-6.21.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:327d132389f37912609e01be62810cf96b5aa95b613903e4b8692e0d12fb0eda", size = 1052350, upload-time = "2026-06-13T14:13:55.88Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/b591295c352ef464c50b4c6ffff1c4f771d875c9e833f578d1b9f564f6b3/pyinstaller-6.21.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7071d4b094d5b40deeef5fa3d3b98a1b846087f7562b49209663d5f9281fe251", size = 748477, upload-time = "2026-06-13T14:14:00.327Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8f/88fff4e403873b1e22286911350e75ff00db014aa08e57045da9d4328993/pyinstaller-6.21.0-py3-none-manylinux2014_i686.whl", hash = "sha256:6b6374d652107dd4a2eeece903ff82bb4045bb5e1006c5a158a6dcdbefe84bf2", size = 760877, upload-time = "2026-06-13T14:14:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/f0e48fbdfd1d05d948157121cea8b1b823dcb89efe6934b71fdd8bdb3f0f/pyinstaller-6.21.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4e3108b3f02384560da70e39b8bf22b0ad597d02bd68a40d76ea91c1cfa00cad", size = 759194, upload-time = "2026-06-13T14:14:10.61Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/ea7878cf9924ed30d946d8288777424e6d069d94f5bde56b4d0890069664/pyinstaller-6.21.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:697532279f535ad572bda613db4f821540e235c7854ca6da4d3bf0373f4415ee", size = 754979, upload-time = "2026-06-13T14:14:15.226Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/51b8905714b733bac66dbc041a7821372d70d888d273ae474c4037d4202d/pyinstaller-6.21.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:605169523a6b5ace39f13dfbff21add9f2bc43df99c7daf9394fefb2c45e8b6f", size = 754812, upload-time = "2026-06-13T14:14:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/d77779439d8c6c2e27a77bcfbd1d5cc0f568ebb611bb472b11af81b5f177/pyinstaller-6.21.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5fa56746c1e76f93634d018502301378a2d0c382553d37d8c3c34ff436c12dd1", size = 753887, upload-time = "2026-06-13T14:14:25.268Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/c22df1f6837784ac349057ba693f08e7b1ca7a0e06f9c33c63bc6280007b/pyinstaller-6.21.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:42395ec76df8e8120c36b13339d9db8cab83e316a12839ee303cc00fc941bb74", size = 753779, upload-time = "2026-06-13T14:14:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/1ce8a27ce62ba8cf3a87c9ce6d575610f4e55d7cb0123e7512fc3f4b921a/pyinstaller-6.21.0-py3-none-win32.whl", hash = "sha256:c6b28d30d8fd99ce162ff3aab5013ed44dbfb747566b1f01b9bed7964d7c14e9", size = 1336462, upload-time = "2026-06-13T14:14:35.785Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/ca1d7e5257dd8566a9dfc0dfb02f8a8075eeb53d4b2d3c579f1276759042/pyinstaller-6.21.0-py3-none-win_amd64.whl", hash = "sha256:7fae06c494ce0ebfe6bd3055c0e409def884f63af2e3705d06bd431ad9237fc7", size = 1397487, upload-time = "2026-06-13T14:14:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/dc/75/21b51523ce8d96629b71311775a0a65f5f5a872124ab0de33e5c848f8bff/pyinstaller-6.21.0-py3-none-win_arm64.whl", hash = "sha256:f13c95c9c03fb567217135919f93815c305813126780b0ed6e0123cb8acaf025", size = 1346094, upload-time = "2026-06-13T14:14:48.914Z" }, +] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2026.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/5b/c9fe0db5e83ee1c39b2258fa21d23b15e1a60786b6c5990ee5074ead8bb6/pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725", size = 173354, upload-time = "2026-06-08T22:37:16.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/31/f2d7343d8ed5f7c4678377886f6ce533e6eaaa131b252ce950114c2a7efa/pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3", size = 457159, upload-time = "2026-06-08T22:37:14.722Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyusb" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/6b/ce3727395e52b7b76dfcf0c665e37d223b680b9becc60710d4bc08b7b7cb/pyusb-1.3.1.tar.gz", hash = "sha256:3af070b607467c1c164f49d5b0caabe8ac78dbed9298d703a8dbf9df4052d17e", size = 77281, upload-time = "2025-01-08T23:45:01.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/b8/27e6312e86408a44fe16bd28ee12dd98608b39f7e7e57884a24e8f29b573/pyusb-1.3.1-py3-none-any.whl", hash = "sha256:bf9b754557af4717fe80c2b07cc2b923a9151f5c08d17bdb5345dac09d6a0430", size = 58465, upload-time = "2025-01-08T23:45:00.029Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "service-tui" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pyinstaller" }, + { name = "pyserial" }, + { name = "python-dotenv" }, + { name = "pyusb" }, + { name = "textual" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyinstaller", specifier = ">=6.0.0" }, + { name = "pyserial", specifier = ">=3.5" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "pyusb", specifier = ">=1.0.0" }, + { name = "textual", specifier = ">=0.80.0" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "textual" +version = "8.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] From 6e42970fefa5122a09d40f55e3daa5ea59e7385d Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Tue, 30 Jun 2026 15:09:47 +0300 Subject: [PATCH 3/9] # firmware_test: TUI app major refactoring --- .env.example | 12 +- cmake/toolchain_arm.cmake | 4 +- firmware/test/CMakeLists.txt | 2 +- firmware/test/src/tests/test_buttons.c | 2 +- firmware/test/src/tests/test_can.c | 2 +- firmware/test/src/tests/test_display.c | 2 +- firmware/test/src/tests/test_mqs.c | 2 +- firmware/test/src/tests/test_qspi.c | 2 +- firmware/test/src/tests/test_sdram.c | 2 +- firmware/test/src/tests/test_usd.c | 2 +- tools/production/app/app.py | 34 +- tools/production/app/app.tcss | 181 ++- tools/production/app/firmware_client.py | 15 +- tools/production/app/flasher.py | 30 +- tools/production/app/models.py | 14 +- tools/production/app/orchestrator.py | 67 +- tools/production/app/screens/__init__.py | 3 +- tools/production/app/screens/diag/__init__.py | 108 +- .../app/screens/diag/confirm_panel.py | 11 +- tools/production/app/screens/diag/results.py | 241 +++- .../production/app/screens/diag/test_list.py | 75 +- tools/production/app/screens/flash.py | 83 +- tools/production/app/screens/post_flash.py | 88 ++ tools/production/app/screens/waiting.py | 16 +- tools/production/app/widgets/__init__.py | 5 + tools/production/app/widgets/app_frame.py | 40 + tools/production/service_tui.log | 1183 +++++++++++++++++ 27 files changed, 1893 insertions(+), 333 deletions(-) create mode 100644 tools/production/app/screens/post_flash.py create mode 100644 tools/production/app/widgets/__init__.py create mode 100644 tools/production/app/widgets/app_frame.py diff --git a/.env.example b/.env.example index 2b17a6e..fd35c23 100644 --- a/.env.example +++ b/.env.example @@ -16,10 +16,6 @@ FLASHLOADER_PID=0073 SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad - -# Тип сборки firmware_test для TUI (Debug | Release) -FIRMWARE_BUILD_TYPE=Debug - # --- Paths --- # BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile # через justfile_directory(), здесь можно переопределить если нужно @@ -38,9 +34,10 @@ GDB_EXECUTABLE=arm-none-eabi-gdb PYOCD_TARGET=mimxrt1050_quadspi PYOCD_FREQUENCY=4000000 -# FCB-бинарник для flash_swd.py (Flash Configuration Block, W25Q128 Quad SPI) +# FCB-бинарник для flash_swd.py (W25Q128 Quad SPI) FCB_PATH=tools/host/dcd/w25q128_fdcb.bin + # --- HIL (аппаратный стенд) --- HIL_PYOCD_FREQUENCY=1000000 HIL_BUILD_DIR=build/target-debug @@ -61,4 +58,7 @@ HIL_M5_TIMEOUT=3.0 # VCOM на плате таргета HIL_USB_CDC_PORT=/dev/cu.usbmodem11301 HIL_USB_CDC_BAUD=115200 -HIL_USB_CDC_TIMEOUT=5.0 \ No newline at end of file +HIL_USB_CDC_TIMEOUT=5.0 + +# Тип сборки firmware_test для TUI (Debug | Release) +FIRMWARE_BUILD_TYPE=Release \ No newline at end of file diff --git a/cmake/toolchain_arm.cmake b/cmake/toolchain_arm.cmake index 0133953..98e2c82 100644 --- a/cmake/toolchain_arm.cmake +++ b/cmake/toolchain_arm.cmake @@ -109,10 +109,10 @@ set(CMAKE_ASM_FLAGS_DEBUG CACHE INTERNAL "") set(CMAKE_C_FLAGS_RELEASE - "-O3 -DNDEBUG" + "-O1 -DNDEBUG" CACHE INTERNAL "") set(CMAKE_CXX_FLAGS_RELEASE - "-O3 -DNDEBUG" + "-O1 -DNDEBUG" CACHE INTERNAL "") set(CMAKE_ASM_FLAGS_RELEASE "" diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index fb3586d..5ea3a34 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -84,7 +84,7 @@ target_link_options( -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) + -T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld) set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/firmware/test/src/tests/test_buttons.c b/firmware/test/src/tests/test_buttons.c index f3d865c..c5764b4 100644 --- a/firmware/test/src/tests/test_buttons.c +++ b/firmware/test/src/tests/test_buttons.c @@ -165,7 +165,7 @@ static test_result_t buttons_test_run(void) /** @brief Дескриптор тест-модуля кнопок для реестра test_runner. */ const test_module_t K_TEST_BUTTONS = { .id = "buttons", - .name = "Test Buttons", + .name = "Buttons", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_can.c b/firmware/test/src/tests/test_can.c index 2b62673..02aed4a 100644 --- a/firmware/test/src/tests/test_can.c +++ b/firmware/test/src/tests/test_can.c @@ -197,7 +197,7 @@ static test_result_t can_run(void) /** @brief Дескриптор для регистрации в test_runner. */ const test_module_t K_TEST_CAN = { .id = "can", - .name = "CAN loopback", + .name = "CAN Network", .critical = false, .requires_hil = true, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_display.c b/firmware/test/src/tests/test_display.c index a367f05..9af17ac 100644 --- a/firmware/test/src/tests/test_display.c +++ b/firmware/test/src/tests/test_display.c @@ -308,7 +308,7 @@ static void display_test_deinit(void) const test_module_t K_TEST_DISPLAY = { .id = "display", - .name = "TFT Display RGB888", + .name = "TFT Display", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_mqs.c b/firmware/test/src/tests/test_mqs.c index 8a41979..aefb362 100644 --- a/firmware/test/src/tests/test_mqs.c +++ b/firmware/test/src/tests/test_mqs.c @@ -220,7 +220,7 @@ static void test_mqs_deinit(void) const test_module_t K_TEST_MQS = { .id = "mqs", - .name = "MQS Audio Out", + .name = "Audio Out", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_qspi.c b/firmware/test/src/tests/test_qspi.c index 64c104c..7d0b91b 100644 --- a/firmware/test/src/tests/test_qspi.c +++ b/firmware/test/src/tests/test_qspi.c @@ -380,7 +380,7 @@ static void qspi_test_deinit(void) const test_module_t K_TEST_QSPI = { .id = "qspi", - .name = "QSPI Flash W25Qxx", + .name = "NOR Flash W25Qxx", .critical = true, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_sdram.c b/firmware/test/src/tests/test_sdram.c index 76ce69d..459f8de 100644 --- a/firmware/test/src/tests/test_sdram.c +++ b/firmware/test/src/tests/test_sdram.c @@ -367,7 +367,7 @@ static void sdram_test_deinit(void) const test_module_t K_TEST_SDRAM = { .id = "sdram", - .name = "SDRAM 32 MB", + .name = "SDRAM MT48LC16x", .critical = true, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_usd.c b/firmware/test/src/tests/test_usd.c index 6c3e4dc..f136f6d 100644 --- a/firmware/test/src/tests/test_usd.c +++ b/firmware/test/src/tests/test_usd.c @@ -343,7 +343,7 @@ static void usd_deinit(void) const test_module_t K_TEST_USD = { .id = "usd", - .name = "microSD (SDIO)", + .name = "microSD Card", .critical = false, .requires_hil = false, .pre_confirm_prompt = "Insert microSD card and press OK", diff --git a/tools/production/app/app.py b/tools/production/app/app.py index da08b36..51d95b5 100644 --- a/tools/production/app/app.py +++ b/tools/production/app/app.py @@ -17,8 +17,8 @@ from textual.binding import Binding from .firmware_client import FirmwareClient from .m5_client import M5Client -from .models import AppMode -from .screens import DiagScreen, FlashScreen, WaitingScreen +from .models import AppMode, FlashTarget +from .screens import DiagScreen, FlashScreen, PostFlashScreen, WaitingScreen logger = logging.getLogger(__name__) @@ -30,11 +30,12 @@ class ServiceApp(App): """Корневое приложение service-tui.""" TITLE = "TFT Board Service Tool" + CSS_PATH = "app.tcss" # относительно app/app.py → app/app.tcss + BINDINGS = [ Binding("ctrl+c", "quit", "Выход", show=True), Binding("ctrl+q", "quit", "Выход"), ] - CSS_PATH = "app.tcss" def __init__(self) -> None: super().__init__() @@ -54,8 +55,20 @@ class ServiceApp(App): self._connect_and_diagnose() @on(FlashScreen.FlashDone) - def _on_flash_done(self, _event: FlashScreen.FlashDone) -> None: - """После прошивки — вернуться на WaitingScreen.""" + def _on_flash_done(self, event: FlashScreen.FlashDone) -> None: + """ + После прошивки: + - firmware_test + успех → PostFlashScreen (промпт смены BootMode) + - всё остальное (production/custom, либо ошибка) → сразу WaitingScreen + """ + if event.success and event.target == FlashTarget.FIRMWARE_TEST: + self.switch_screen(PostFlashScreen()) + else: + self.switch_screen(WaitingScreen()) + + @on(PostFlashScreen.Done) + def _on_post_flash_done(self) -> None: + """Оператор подтвердил смену BootMode или истёк таймаут.""" self.switch_screen(WaitingScreen()) @on(DiagScreen.DiagDone) @@ -76,7 +89,16 @@ class ServiceApp(App): return self._m5 = await M5Client.auto_connect() - self.switch_screen(DiagScreen(firmware=self._fw, m5=self._m5)) + + fw_version = "" + try: + fw_version = await self._fw.get_version() + except Exception as exc: + logger.warning("get_version failed: %s", exc) + + self.switch_screen( + DiagScreen(firmware=self._fw, m5=self._m5, fw_version=fw_version) + ) def _disconnect(self) -> None: if self._fw is not None: diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss index 602dffc..53bf7f0 100644 --- a/tools/production/app/app.tcss +++ b/tools/production/app/app.tcss @@ -1,13 +1,31 @@ -/* ── WaitingScreen ───────────────────────────────────────────────────────── */ +/* ═══════════════════════════════════════════════════════════ + service-tui — единый файл стилей + Путь резолвится относительно app/app.py → app/app.tcss + ═══════════════════════════════════════════════════════════ */ -WaitingScreen { +/* ── Общая рамка приложения (AppFrame) ──────────────────── */ +/* Все три экрана центрируют один AppFrame фиксированного */ +/* размера — единообразный визуальный каркас независимо от */ +/* размера окна терминала (как в ratatui-приложениях). */ + +Screen { align: center middle; - layout: vertical; } -WaitingScreen > Static { - content-align: center middle; +AppFrame { width: 100%; + height: 100%; + max-width: 160; + max-height: 50; + border: heavy $primary; + background: $surface; + padding: 1 2; +} + +/* ── WaitingScreen ──────────────────────────────────────── */ + +#waiting-frame { + align: center middle; } #waiting-logo { @@ -16,9 +34,8 @@ WaitingScreen > Static { border: round $primary; padding: 1 2; content-align: center middle; - text-style: bold; color: $primary; - margin-bottom: 0; + text-style: bold; } #waiting-hint { @@ -33,11 +50,7 @@ WaitingScreen > Static { content-align: center middle; } -/* ── FlashScreen ─────────────────────────────────────────────────────────── */ - -FlashScreen { - padding: 1 2; -} +/* ── FlashScreen ────────────────────────────────────────── */ #flash-title { text-style: bold; @@ -52,7 +65,6 @@ FlashScreen { height: auto; } - #flash-custom-path { margin-top: 1; height: auto; @@ -72,12 +84,14 @@ FlashScreen { align: left middle; } -#flash-btn-flash { +#flash-btn-flash, +#flash-btn-erase { margin-right: 2; } #flash-btn-flash:disabled, -#flash-btn-erase:disabled { +#flash-btn-erase:disabled, +#flash-btn-quit:disabled { background: $panel; color: $text-muted; } @@ -86,8 +100,8 @@ FlashScreen { margin-top: 1; } -#flash-progress-label { - color: $text-muted; +#flash-progress-bar.hidden { + display: none; } #flash-log { @@ -98,15 +112,51 @@ FlashScreen { overflow-y: auto; } -/* ── DiagScreen ──────────────────────────────────────────────────────────── */ +/* ── PostFlashScreen ────────────────────────────────────── */ -DiagScreen { - layout: grid; - grid-size: 1; - grid-rows: 3 1fr 1 1 3 auto; +#post-flash-frame { + align: center middle; +} + +#post-flash-title { + text-style: bold; + color: $success; + content-align: center middle; + margin-bottom: 2; +} + +#post-flash-instruction { + border: round $warning; + padding: 1 3; + color: $text; + text-style: bold; + content-align: center middle; + width: auto; +} + +#post-flash-countdown { + margin-top: 2; + color: $text-muted; + content-align: center middle; +} + +#post-flash-btn-row { + margin-top: 2; + height: auto; + align: center middle; +} + +#post-flash-btn-ok { + margin-right: 2; +} + +/* ── DiagScreen ─────────────────────────────────────────── */ + +#diag-frame { + layout: vertical; + padding: 0; } -/* Шапка */ #diag-header { height: 3; background: $panel; @@ -115,16 +165,21 @@ DiagScreen { } #diag-header-fw { + width: auto; + min-width: 14; color: $text-muted; margin-right: 3; } #diag-header-uid { + width: auto; + min-width: 28; color: $text-muted; margin-right: 3; } #diag-header-m5 { + width: auto; color: $success; } @@ -132,24 +187,30 @@ DiagScreen { color: $text-muted; } -/* Рабочая зона */ #diag-main { height: 1fr; + min-height: 10; +} + +#diag-progress-row { + height: auto; + padding: 0 2; +} + +#diag-progress-row.hidden { + display: none; } -/* Прогресс */ #diag-progress-bar { height: 1; - margin: 0 2; + margin-top: 1; } #diag-progress-label { height: 1; color: $text-muted; - padding: 0 2; } -/* Кнопки запуска */ #diag-btn-row { height: 3; align: left middle; @@ -157,7 +218,8 @@ DiagScreen { background: $panel; } -#diag-btn-run-selected { +#diag-btn-run-selected, +#diag-btn-run-all { margin-right: 2; } @@ -167,7 +229,7 @@ DiagScreen { color: $text-muted; } -/* ── TestListPanel ───────────────────────────────────────────────────────── */ +/* ── TestListPanel ──────────────────────────────────────── */ TestListPanel { width: 38; @@ -182,9 +244,19 @@ TestListPanel .section-title { padding: 0 0 1 0; } +#test-list-select-row { + height: auto; + margin-bottom: 1; +} + +#test-list-select-row Button { + margin-right: 1; + min-width: 12; +} + .test-row { + height: auto; align: left middle; - height: auto; } .test-row-hil-badge { @@ -196,10 +268,10 @@ TestListPanel .section-title { color: $text-muted; } -/* ── ResultsPanel ────────────────────────────────────────────────────────── */ +/* ── ResultsPanel ───────────────────────────────────────── */ ResultsPanel { - width: 1fr; + width: 70; padding: 1 2; overflow-y: auto; } @@ -210,45 +282,26 @@ ResultsPanel .section-title { padding: 0 0 1 0; } -.result-row { - height: 1; - align: left middle; +#results-empty { + height: 1fr; + width: 100%; } -.result-id { +#results-empty-text { color: $text-muted; - width: 12; + text-align: center; } -.result-status-pass { - color: $success; - width: 8; - text-style: bold; +#results-table { + height: auto; + max-height: 100%; } -.result-status-fail { - color: $error; - width: 8; - text-style: bold; +#results-table.hidden { + display: none; } -.result-status-running { - color: $warning; - width: 8; -} - -.result-status-skip { - color: $text-muted; - width: 8; -} - -.result-detail { - color: $text-muted; - width: 1fr; - overflow: hidden; -} - -/* ── ConfirmPanel ────────────────────────────────────────────────────────── */ +/* ── ConfirmPanel ───────────────────────────────────────── */ ConfirmPanel { height: auto; @@ -280,4 +333,4 @@ ConfirmPanel.hidden { #confirm-btn-ok.hidden, #confirm-btn-fail.hidden { display: none; -} \ No newline at end of file +} diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py index a20ee8e..342869a 100644 --- a/tools/production/app/firmware_client.py +++ b/tools/production/app/firmware_client.py @@ -12,9 +12,10 @@ firmware_client.py — async CDC-клиент firmware_test. FirmwareClient.run_selected() — запустить тесты, вернуть AsyncGenerator событий FirmwareClient.send_confirm() — отправить confirm FirmwareClient.get_uid() — get_uid → str (hex UID) + FirmwareClient.get_version() — get_version → str (X.Y.Z) FirmwareClient.send_cmd_raw() — отправить произвольную JSON-команду -Все blocking-операции с pyserial выполняются в asyncio.get_event_loop().run_in_executor() +Все blocking-операции с pyserial выполняются в run_in_executor() чтобы не блокировать event loop Textual. """ @@ -28,7 +29,7 @@ from typing import AsyncGenerator, Optional import serial import serial.tools.list_ports -from .models import TestInfo, TestStatus +from .models import TestInfo logger = logging.getLogger(__name__) @@ -60,14 +61,6 @@ def _parse_event(line: str) -> Optional[dict]: return None -def _parse_test_status(s: str) -> TestStatus: - return { - "pass": TestStatus.PASS, - "fail": TestStatus.FAIL, - "skip": TestStatus.SKIP, - }.get(s, TestStatus.FAIL) - - class FirmwareClient: """ Async-клиент для общения с firmware_test по USB CDC ACM. @@ -194,7 +187,7 @@ class FirmwareClient: return False async def get_version(self) -> str: - """Запросить версию firmware_test. Вернуть строку X.Y.Z или ''.""" + """Запросить версию firmware_test. Вернуть строку 'X.Y.Z' или ''.""" await self._send({"type": "cmd", "cmd": "get_version"}) async for event in self._recv_until({"version_response"}, timeout_s=3.0): if event.get("type") == "version_response": diff --git a/tools/production/app/flasher.py b/tools/production/app/flasher.py index c42b0d0..3d260b2 100644 --- a/tools/production/app/flasher.py +++ b/tools/production/app/flasher.py @@ -8,6 +8,7 @@ flasher.py — обёртка над tools/host/flash_usb.py для TUI. Публичный API: Flasher.flash(target, bin_path, progress_cb) — async, прогресс через callback + Flasher.erase_chip(progress_cb) — async chip erase Flasher.detect_sdp() — проверить наличие BootROM SDP Flasher.detect_cdc() — проверить наличие CDC firmware_test """ @@ -31,17 +32,18 @@ _BOOTROM_PID = int(os.environ.get("BOOTROM_PID", "0x0130"), 16) _CDC_VID = int(os.environ.get("SERVICE_CDC_VID", "0x1996"), 16) _CDC_PID = int(os.environ.get("SERVICE_CDC_PID", "0x00ad"), 16) +# Тип сборки firmware_test для прошивки (Debug | Release). +# Release временно нестабилен (см. отчёт о тестировании) — по умолчанию Debug. +_FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug") + # Путь до flash_usb.py относительно корня репозитория _FLASH_USB_SCRIPT = Path(__file__).parents[3] / "tools" / "host" / "flash_usb.py" _HOST_TOOLS_DIR = _FLASH_USB_SCRIPT.parent - # Паттерны stdout flash_usb.py для извлечения прогресса _RE_PERCENT = re.compile(r"(\d{1,3})\s*%") _RE_PHASE = re.compile(r"(sdphost|blhost|Writing|Erasing|Verifying)", re.IGNORECASE) -_FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug") - ProgressCallback = Callable[[FlashProgress], Awaitable[None]] @@ -50,10 +52,10 @@ def _detect_usb(vid: int, pid: int) -> bool: Проверить наличие USB-устройства по VID/PID (синхронно). Два метода детекта: - 1. pyusb (usb.core) — видит все USB-устройства включая SDP bulk/HID - (на macOS SDP не создаёт serial-порт и невидим через list_ports). - 2. serial.tools.list_ports — fallback для CDC ACM устройств - если pyusb недоступен. + 1. pyusb (usb.core) — видит все USB-устройства включая SDP bulk/HID. + На macOS SDP-устройство (1FC9:0130) не создаёт serial-порт + и невидимо через serial.tools.list_ports. + 2. serial.tools.list_ports — fallback для CDC ACM устройств. """ # Метод 1: pyusb — работает для SDP и CDC try: @@ -140,9 +142,9 @@ class Flasher: """ Запустить прошивку через flash_usb.py. - :param target: Что прошиваем (firmware_test или production). + :param target: Что прошиваем (firmware_test, production или custom). :param progress_cb: Async callback с FlashProgress (может быть None). - :param bin_path: Путь к бинарю (опционально, если overriding default). + :param bin_path: Путь к бинарю (обязателен для CUSTOM). :return: True при успехе. """ if target == FlashTarget.FIRMWARE_TEST: @@ -160,12 +162,12 @@ class Flasher: return await self._run_flash_bin(bin_path, progress_cb) return False - async def _run_flash( # прошивка стандартного firmware по имени + async def _run_flash( self, firmware: str, progress_cb: Optional[ProgressCallback], ) -> bool: - """Запустить flash_usb.py для одного бинаря.""" + """Запустить flash_usb.py для одного бинаря (стандартный firmware).""" cmd = [ "uv", "run", @@ -180,11 +182,12 @@ class Flasher: ] return await self._run_cmd(cmd, firmware, progress_cb) - async def _run_flash_bin( # прошивка произвольного бинаря + async def _run_flash_bin( self, bin_path: Path, progress_cb: Optional[ProgressCallback], ) -> bool: + """Запустить flash_usb.py для произвольного бинаря.""" cmd = [ "uv", "run", @@ -197,12 +200,13 @@ class Flasher: ] return await self._run_cmd(cmd, bin_path.stem, progress_cb) - async def _run_cmd( # общий subprocess runner + async def _run_cmd( self, cmd: list[str], label: str, progress_cb: Optional[ProgressCallback], ) -> bool: + """Общий subprocess runner для всех операций flash_usb.py.""" logger.info("Running: %s", " ".join(cmd)) try: self._proc = await asyncio.create_subprocess_exec( diff --git a/tools/production/app/models.py b/tools/production/app/models.py index 80ca804..519b3c8 100644 --- a/tools/production/app/models.py +++ b/tools/production/app/models.py @@ -14,9 +14,9 @@ from typing import Optional class AppMode(Enum): """Режим работы приложения — определяется автодетектом USB.""" - WAITING = auto() # ждём подключения устройства - FLASHING = auto() # обнаружен BootROM SDP (1FC9:0130) - DIAGNOSING = auto() # получен session_start по CDC + WAITING = auto() # ждём подключения устройства + FLASHING = auto() # обнаружен BootROM SDP (1FC9:0130) + DIAGNOSING = auto() # получен session_start по CDC class TestStatus(Enum): @@ -34,7 +34,7 @@ class FlashTarget(Enum): FIRMWARE_TEST = "firmware_test" PRODUCTION = "production" # bootloader + tft_app - CUSTOM = "custom" # произвольный HAB-бинарь, путь задаётся отдельно + CUSTOM = "custom" # произвольный HAB-бинарь, путь задаётся отдельно @dataclass(frozen=True) @@ -88,6 +88,6 @@ class ConfirmRequest: class FlashProgress: """Прогресс прошивки.""" - phase: str # "sdphost" | "blhost" | "done" | "error" - percent: int # 0..100 - message: str \ No newline at end of file + phase: str # "sdphost" | "blhost" | "done" | "error" + percent: int # 0..100 + message: str diff --git a/tools/production/app/orchestrator.py b/tools/production/app/orchestrator.py index 748ba43..bb18905 100644 --- a/tools/production/app/orchestrator.py +++ b/tools/production/app/orchestrator.py @@ -4,7 +4,7 @@ orchestrator.py — оркестратор confirm_request для TUI. Получает события от FirmwareClient, принимает решение кто должен ответить на confirm_request, и отправляет confirm обратно. -Три режима confirm (из закрытых решений Этапа 8): +Три режима confirm: standalone/interactive (display, mqs, usd): → TUI показывает prompt оператору + кнопки OK/FAIL + countdown @@ -21,7 +21,7 @@ orchestrator.py — оркестратор confirm_request для TUI. → отправляет confirm без участия оператора → оператор видит только прогресс -Маршрутизация определяется по prefix confirm_request.id: +Маршрутизация определяется по id confirm_request: "opto_*" → HIL (M5 relay) "can_rx_ready" → HIL (M5 can_send) "can_tx_verify" → HIL (M5 can_recv + verify) @@ -42,47 +42,48 @@ from typing import AsyncGenerator, Callable, Coroutine, Optional from .firmware_client import FirmwareClient from .m5_client import M5Client -from .models import ConfirmRequest, TestInfo, TestResult, TestStatus +from .models import ConfirmRequest, TestResult, TestStatus logger = logging.getLogger(__name__) -# Задержки для HIL (из закрытых решений: RELAY_ON_S=0.15, RELAY_OFF_S=0.5) +# Задержки для HIL _RELAY_ON_S = 0.15 _RELAY_OFF_S = 0.50 # Карта confirm_id → реле M5 для opto-теста -# Формат: confirm_id_prefix → (relay_num, target_state) +# Формат: confirm_id → (relay_num, target_state) _OPTO_RELAY_MAP: dict[str, tuple[int, bool]] = { - "opto_in1_active": (3, True), + "opto_in1_active": (3, True), "opto_in1_inactive": (3, False), - "opto_in2_active": (4, True), + "opto_in2_active": (4, True), "opto_in2_inactive": (4, False), - "opto_rs_active": (2, True), - "opto_rs_inactive": (2, False), + "opto_rs_active": (2, True), + "opto_rs_inactive": (2, False), } -# CAN параметры (из закрытых решений) -_CAN_RX_ID = 0x100 +# CAN параметры +_CAN_RX_ID = 0x100 _CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF] -_CAN_TX_ID = 0x200 +_CAN_TX_ID = 0x200 _CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE] # ── Типы событий оркестратора ──────────────────────────────────────────── + class OrchestratorEventType(Enum): - TEST_BEGIN = auto() # тест начался - TEST_RESULT = auto() # тест завершился - CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone) + TEST_BEGIN = auto() # тест начался + TEST_RESULT = auto() # тест завершился + CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone) CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически - BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm) - SUMMARY = auto() # итог всей сессии - ERROR = auto() # ошибка протокола или M5 + BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm) + SUMMARY = auto() # итог всей сессии + ERROR = auto() # ошибка протокола или M5 @dataclass class OrchestratorEvent: - """Событие от оркестратора — передаётся в TUI через asyncio.Queue.""" + """Событие от оркестратора — передаётся в TUI.""" type: OrchestratorEventType test_id: str = "" @@ -93,8 +94,7 @@ class OrchestratorEvent: message: str = "" -# Тип callback для ответа оператора: -# TUI вызывает его с True (OK) или False (FAIL) после показа prompt +# Тип callback для ответа оператора OperatorConfirmCallback = Callable[[bool], Coroutine] @@ -107,7 +107,6 @@ class Orchestrator: orch = Orchestrator(firmware_client, m5_client) async for event in orch.run_tests(["sdram", "opto", "display"]): if event.type == OrchestratorEventType.CONFIRM_NEEDED: - # TUI показывает prompt, получает ответ оператора confirmed = await tui.ask_operator(event.confirm) await orch.resolve_operator_confirm(confirmed) elif event.type == OrchestratorEventType.TEST_RESULT: @@ -121,7 +120,6 @@ class Orchestrator: ) -> None: self._fw = firmware self._m5 = m5 - # Queue для ответов оператора на CONFIRM_NEEDED self._operator_queue: asyncio.Queue[bool] = asyncio.Queue(maxsize=1) async def resolve_operator_confirm(self, confirmed: bool) -> None: @@ -151,12 +149,11 @@ class Orchestrator: ) elif event_type == "test_result": - status_str = raw.get("status", "fail") status = { "pass": TestStatus.PASS, "fail": TestStatus.FAIL, "skip": TestStatus.SKIP, - }.get(status_str, TestStatus.FAIL) + }.get(raw.get("status", "fail"), TestStatus.FAIL) result = TestResult( id=raw.get("id", ""), @@ -176,8 +173,8 @@ class Orchestrator: prompt=raw.get("prompt", ""), timeout_ms=raw.get("timeout_ms", 30000), ) - async for orch_event in self._handle_confirm(confirm): - yield orch_event + async for ev in self._handle_confirm(confirm): + yield ev elif event_type == "summary": yield OrchestratorEvent( @@ -185,10 +182,10 @@ class Orchestrator: summary=raw, ) - elif event_type in ("error", None): + else: yield OrchestratorEvent( type=OrchestratorEventType.ERROR, - message=raw.get("error", "unknown error"), + message=raw.get("error", f"unknown event: {event_type}"), ) # ── Маршрутизация confirm ──────────────────────────────────────────── @@ -199,25 +196,21 @@ class Orchestrator: """Определить тип confirm и обработать соответственно.""" cid = confirm.id - # HIL: opto-тест if cid in _OPTO_RELAY_MAP: async for ev in self._handle_hil_opto(confirm): yield ev return - # HIL: CAN RX if cid == "can_rx_ready": async for ev in self._handle_hil_can_rx(confirm): yield ev return - # HIL: CAN TX verify if cid == "can_tx_verify": async for ev in self._handle_hil_can_tx(confirm): yield ev return - # buttons: нет JSON-confirm, только инструкция оператору if cid.startswith("btn"): yield OrchestratorEvent( type=OrchestratorEventType.BUTTONS_PROMPT, @@ -225,7 +218,6 @@ class Orchestrator: ) return - # standalone interactive: показать оператору async for ev in self._handle_operator_confirm(confirm): yield ev @@ -248,7 +240,6 @@ class Orchestrator: settle_s = _RELAY_ON_S if relay_state else _RELAY_OFF_S ok = await self._m5.relay_set(relay_num, relay_state) await asyncio.sleep(settle_s) - await self._fw.send_confirm(confirm.id, ok) yield OrchestratorEvent( @@ -270,7 +261,6 @@ class Orchestrator: ) return - # M5 отправляет CAN-фрейм, таргет принимает после confirmed:true ok = await self._m5.can_send(_CAN_RX_ID, _CAN_RX_DATA) await self._fw.send_confirm(confirm.id, ok) @@ -293,7 +283,6 @@ class Orchestrator: ) return - # Таргет уже отправил фрейм, M5 принимает и верифицирует frame = await self._m5.can_recv(timeout_ms=500) verified = ( frame is not None @@ -322,7 +311,6 @@ class Orchestrator: Передать confirm оператору. Блокируется до resolve_operator_confirm(). TUI должен показать prompt и дать возможность ответить. """ - # очистить очередь на случай устаревшего ответа while not self._operator_queue.empty(): self._operator_queue.get_nowait() @@ -331,7 +319,6 @@ class Orchestrator: confirm=confirm, ) - # ждём ответа от оператора (TUI вызовет resolve_operator_confirm) timeout_s = confirm.timeout_ms / 1000.0 try: confirmed = await asyncio.wait_for( @@ -342,4 +329,4 @@ class Orchestrator: logger.warning("Operator confirm timeout: %s", confirm.id) confirmed = False - await self._fw.send_confirm(confirm.id, confirmed) \ No newline at end of file + await self._fw.send_confirm(confirm.id, confirmed) diff --git a/tools/production/app/screens/__init__.py b/tools/production/app/screens/__init__.py index dfe4305..50c15c8 100644 --- a/tools/production/app/screens/__init__.py +++ b/tools/production/app/screens/__init__.py @@ -2,6 +2,7 @@ from .diag import DiagScreen from .flash import FlashScreen +from .post_flash import PostFlashScreen from .waiting import WaitingScreen -__all__ = ["WaitingScreen", "FlashScreen", "DiagScreen"] +__all__ = ["WaitingScreen", "FlashScreen", "PostFlashScreen", "DiagScreen"] diff --git a/tools/production/app/screens/diag/__init__.py b/tools/production/app/screens/diag/__init__.py index 3e3e1ef..b75f691 100644 --- a/tools/production/app/screens/diag/__init__.py +++ b/tools/production/app/screens/diag/__init__.py @@ -17,7 +17,7 @@ from typing import Optional from textual import on, work from textual.app import ComposeResult from textual.binding import Binding -from textual.containers import Horizontal +from textual.containers import Horizontal, Vertical from textual.css.query import NoMatches from textual.message import Message from textual.screen import Screen @@ -27,6 +27,7 @@ from ...firmware_client import FirmwareClient from ...m5_client import M5Client from ...models import SessionState from ...orchestrator import Orchestrator, OrchestratorEvent, OrchestratorEventType +from ...widgets import AppFrame from .confirm_panel import ConfirmPanel from .results import ResultsPanel from .test_list import TestListPanel @@ -55,48 +56,55 @@ class DiagScreen(Screen): self, firmware: FirmwareClient, m5: Optional[M5Client] = None, + fw_version: str = "", **kwargs, ) -> None: super().__init__(**kwargs) self._fw = firmware self._m5 = m5 self._orchestrator = Orchestrator(firmware, m5) - self._session = SessionState() - self._running = False + self._session = SessionState(fw_version=fw_version) + self._tests_running = False def compose(self) -> ComposeResult: - # Шапка - with Horizontal(id="diag-header"): - yield Static("fw: —", id="diag-header-fw") - yield Static("UID: —", id="diag-header-uid") - yield Static("M5: —", id="diag-header-m5") + with AppFrame(id="diag-frame"): + # Шапка — фиксированная высота 3 + with Horizontal(id="diag-header"): + yield Static("firmware_test: —\nFw: —", id="diag-header-fw") + yield Static("MCU ID: —", id="diag-header-uid") + yield Static("M5: —", id="diag-header-m5") - # Рабочая зона: список тестов + результаты - with Horizontal(id="diag-main"): - yield TestListPanel(id="diag-test-list") - yield ResultsPanel(id="diag-results") + # Рабочая зона: список тестов + результаты — занимает всё + # оставшееся место (1fr), сама прокручивается при переполнении + with Horizontal(id="diag-main"): + yield TestListPanel(id="diag-test-list") + yield ResultsPanel(id="diag-results") - # Прогресс - yield ProgressBar(id="diag-progress-bar", show_eta=False) - yield Label("", id="diag-progress-label") + # Прогресс — скрыт пока нет активного прогона. Контейнер + # имеет classes="hidden" по умолчанию — height: auto + display: + # none даёт нулевую высоту, не отнимая место у остального layout. + with Vertical(id="diag-progress-row", classes="hidden"): + yield ProgressBar(id="diag-progress-bar", show_eta=False) + yield Label("", id="diag-progress-label") - # Кнопки запуска - with Horizontal(id="diag-btn-row"): - yield Button( - "▶ Запустить выбранные", - id="diag-btn-run-selected", - variant="primary", - disabled=True, - ) - yield Button( - "▶▶ Все тесты", - id="diag-btn-run-all", - variant="default", - disabled=True, - ) + # Кнопки запуска — фиксированная высота 3 (под border Button) + with Horizontal(id="diag-btn-row"): + yield Button( + "▶ Запустить выбранные", + id="diag-btn-run-selected", + variant="primary", + disabled=True, + ) + yield Button( + "▶▶ Все тесты", + id="diag-btn-run-all", + variant="default", + disabled=True, + ) + yield Button("✕ Выйти", id="diag-btn-quit", variant="default") - # Панель confirm - yield ConfirmPanel(id="diag-confirm", classes="hidden") + # Панель confirm — auto-высота, видна только когда есть запрос + yield ConfirmPanel(id="diag-confirm", classes="hidden") def on_mount(self) -> None: self._init_session() @@ -108,7 +116,6 @@ class DiagScreen(Screen): try: tests = await self._fw.list_tests() uid = await self._fw.get_uid() - version = await self._fw.get_version() except Exception as exc: logger.error("Session init failed: %s", exc) self.post_message(self.DiagDone()) @@ -116,7 +123,6 @@ class DiagScreen(Screen): self._session.tests = tests self._session.chip_uid = uid - self._session.fw_version = version self._session.m5_connected = self._m5 is not None self._update_header() @@ -129,17 +135,17 @@ class DiagScreen(Screen): def _update_header(self) -> None: self.query_one("#diag-header-fw", Static).update( - f"fw: {self._session.fw_version or '?'}" + f"firmware_test: {self._session.fw_version or '?'}" ) - uid_short = self._session.chip_uid[:16] if self._session.chip_uid else "—" - self.query_one("#diag-header-uid", Static).update(f"UID: {uid_short}") + uid = self._session.chip_uid or "—" + self.query_one("#diag-header-uid", Static).update(f"MCU ID: {uid}") m5_widget = self.query_one("#diag-header-m5", Static) if self._session.m5_connected: m5_widget.update("M5: ✓ подключён") m5_widget.remove_class("m5-absent") else: - m5_widget.update("M5: — нет") + m5_widget.update("M5: ✕ не подключен") m5_widget.add_class("m5-absent") # ── Кнопки ─────────────────────────────────────────────────────────────── @@ -156,8 +162,13 @@ class DiagScreen(Screen): if ids: self._start_run(ids) + @on(Button.Pressed, "#diag-btn-quit") + def _on_quit_pressed(self) -> None: + if not self._tests_running: + self.app.exit() + def action_go_back(self) -> None: - if not self._running: + if not self._tests_running: self.post_message(self.DiagDone()) def action_run_all(self) -> None: @@ -181,9 +192,11 @@ class DiagScreen(Screen): results = self.query_one("#diag-results", ResultsPanel) results.reset() self._session.results.clear() + self._show_progress(True) self._update_progress(0, len(test_ids), "") self._set_run_buttons(enabled=False) - self._running = True + self.query_one("#diag-test-list", TestListPanel).set_enabled(False) + self._tests_running = True self._run_worker(test_ids) @work(exclusive=True, thread=False) @@ -200,8 +213,9 @@ class DiagScreen(Screen): except Exception as exc: logger.error("run_worker error: %s", exc) finally: - self._running = False + self._tests_running = False self._set_run_buttons(enabled=True) + self.query_one("#diag-test-list", TestListPanel).set_enabled(True) try: self.query_one("#diag-confirm", ConfirmPanel).hide() except NoMatches: @@ -251,9 +265,21 @@ class DiagScreen(Screen): # ── Утилиты ─────────────────────────────────────────────────────────────── + def _show_progress(self, visible: bool) -> None: + """Показать/скрыть строку прогресса. Скрыта в простое — без анимации.""" + row = self.query_one("#diag-progress-row") + bar = self.query_one("#diag-progress-bar", ProgressBar) + if visible: + bar.update(total=100, progress=0) + row.remove_class("hidden") + else: + row.add_class("hidden") + def _update_progress(self, done: int, total: int, msg: str) -> None: pct = int(done / total * 100) if total else 0 - self.query_one("#diag-progress-bar", ProgressBar).update(progress=pct) + self.query_one("#diag-progress-bar", ProgressBar).update( + total=100, progress=pct + ) self.query_one("#diag-progress-label", Label).update(msg) def _set_run_buttons(self, enabled: bool) -> None: diff --git a/tools/production/app/screens/diag/confirm_panel.py b/tools/production/app/screens/diag/confirm_panel.py index 0570a91..d694f43 100644 --- a/tools/production/app/screens/diag/confirm_panel.py +++ b/tools/production/app/screens/diag/confirm_panel.py @@ -28,7 +28,7 @@ class ConfirmPanel(Widget): Использование:: panel = ConfirmPanel() - panel.show(prompt="Экран залит красным?", timeout_ms=30000) + panel.show_operator(prompt="Экран залит красным?", timeout_ms=30000) # Слушать ConfirmPanel.Confirmed в родительском экране """ @@ -39,13 +39,10 @@ class ConfirmPanel(Widget): super().__init__() self.confirmed = confirmed - DEFAULT_CSS = "" # стили в diag.tcss - def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._timer: Timer | None = None self._remaining: int = 0 - self._operator_mode: bool = True # False → режим buttons (нет кнопок) def compose(self) -> ComposeResult: yield Static("", id="confirm-prompt") @@ -57,9 +54,8 @@ class ConfirmPanel(Widget): def show_operator(self, prompt: str, timeout_ms: int) -> None: """Показать панель с кнопками OK/Нет и countdown.""" - self._operator_mode = True self._remaining = timeout_ms // 1000 - self._set_prompt(prompt) + self._set_prompt(f"⚠ {prompt}") self._set_countdown(self._remaining) self._show_buttons(True) self.remove_class("hidden") @@ -70,7 +66,6 @@ class ConfirmPanel(Widget): Показать инструкцию для теста кнопок. Без кнопок OK/Нет — оператор только читает, нажимает физическую кнопку. """ - self._operator_mode = False self._stop_timer() self._set_prompt(f"⌨ {prompt}") self._set_countdown("") @@ -117,7 +112,7 @@ class ConfirmPanel(Widget): def _set_prompt(self, text: str) -> None: try: - self.query_one("#confirm-prompt", Static).update(f"⚠ {text}") + self.query_one("#confirm-prompt", Static).update(text) except NoMatches: pass diff --git a/tools/production/app/screens/diag/results.py b/tools/production/app/screens/diag/results.py index 5045b79..5879bd3 100644 --- a/tools/production/app/screens/diag/results.py +++ b/tools/production/app/screens/diag/results.py @@ -1,115 +1,212 @@ """ results.py — виджет правой колонки DiagScreen. -Отображает строки результатов тестов: id | статус | detail. -Обновляется по событиям от Orchestrator через DiagScreen. +DataTable с колонками: Тест | HIL | Статус | Время | Детали. +До первого запуска показывает empty-state placeholder вместо таблицы. + +Сортировка: FAIL всегда наверху (естественно бросается в глаза), +внутри групп статусов — исходный порядок реестра firmware_test. +Строка с FAIL дополнительно подсвечивается красным фоном целиком. Публичный API: - ResultsPanel.populate(tests) — инициализировать пустые строки - ResultsPanel.set_running(test_id) — показать "выполняется" + ResultsPanel.populate(tests) — показать пустую таблицу (PENDING) + ResultsPanel.set_running(test_id) — пометить тест как выполняющийся ResultsPanel.set_result(result) — показать финальный результат - ResultsPanel.reset() — сбросить все строки + ResultsPanel.reset() — сбросить все строки в PENDING """ from __future__ import annotations +from rich.style import Style +from rich.text import Text from textual.app import ComposeResult -from textual.containers import Horizontal -from textual.css.query import NoMatches +from textual.containers import Center, Middle from textual.widget import Widget -from textual.widgets import Label, Static +from textual.widgets import DataTable, Static -from ...models import TestResult, TestStatus +from ...models import TestInfo, TestResult, TestStatus +# Порядок сортировки: чем меньше число — тем выше строка в таблице. +# FAIL всегда наверху, PASS/SKIP внизу — внутри групп сохраняется +# исходный порядок реестра (стабильная сортировка). +_SORT_RANK: dict[TestStatus, int] = { + TestStatus.FAIL: 0, + TestStatus.RUNNING: 1, + TestStatus.PENDING: 2, + TestStatus.SKIP: 3, + TestStatus.PASS: 4, +} -def _status_display(status: TestStatus) -> tuple[str, str]: - """Вернуть (текст, css-класс) для статуса.""" - return { - TestStatus.PASS: ("✓ PASS", "result-status-pass"), - TestStatus.FAIL: ("✗ FAIL", "result-status-fail"), - TestStatus.RUNNING: ("…", "result-status-running"), - TestStatus.SKIP: ("SKIP", "result-status-skip"), - TestStatus.PENDING: ("", "result-status-skip"), - }[status] +_STATUS_DISPLAY: dict[TestStatus, tuple[str, str]] = { + TestStatus.PASS: ("✓ PASS", "green"), + TestStatus.FAIL: ("✗ FAIL", "bold white"), + TestStatus.RUNNING: ("…", "yellow"), + TestStatus.SKIP: ("SKIP", "grey50"), + TestStatus.PENDING: ("", "grey50"), +} - -# CSS-классы статусов — для очистки перед сменой -_STATUS_CLASSES = ( - "result-status-pass", - "result-status-fail", - "result-status-running", - "result-status-skip", -) +_FAIL_BG = "dark_red" class ResultsPanel(Widget): - """Правая колонка DiagScreen — результаты тестов.""" + """Правая колонка DiagScreen — таблица результатов тестов.""" - DEFAULT_CSS = "" # стили в diag.tcss + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + # test_id → TestInfo, нужно для перестроения строк при сортировке + self._tests: dict[str, TestInfo] = {} + # test_id → текущий статус (для сортировки и повторных update) + self._statuses: dict[str, TestStatus] = {} + self._order: list[str] = [] # исходный порядок реестра def compose(self) -> ComposeResult: - yield Label("Результаты", classes="section-title") + with Center(id="results-empty"): + with Middle(): + yield Static( + "Выберите тесты и нажмите\n«Запустить выбранные»", + id="results-empty-text", + ) + yield DataTable(id="results-table", cursor_type="row", classes="hidden") + + def on_mount(self) -> None: + table = self.query_one("#results-table", DataTable) + table.add_columns("Тест", "M5", "Статус", "Время", "Детали") # ── Public API ──────────────────────────────────────────────────────────── - def populate(self, tests: list) -> None: + def populate(self, tests: list[TestInfo]) -> None: """ - Инициализировать пустые строки результатов. + Заполнить таблицу тестами в состоянии PENDING. + До первого вызова populate() с непустым списком виден empty-state. + """ + table = self.query_one("#results-table", DataTable) + table.clear() + self._tests.clear() + self._statuses.clear() + self._order = [t.id for t in tests] - :param tests: list[TestInfo] - """ - for child in list(self.children): - if not child.has_class("section-title"): - child.remove() + if not tests: + self._show_empty(True) + return for test in tests: - row = Horizontal(classes="result-row", id=f"result-row-{test.id}") - id_lbl = Static(test.id, classes="result-id") - status_lbl = Static( - "", classes="result-status-skip", id=f"result-status-{test.id}" - ) - detail_lbl = Static( - "", classes="result-detail", id=f"result-detail-{test.id}" - ) - self.mount(row) - row.mount(id_lbl) - row.mount(status_lbl) - row.mount(detail_lbl) + self._tests[test.id] = test + self._statuses[test.id] = TestStatus.PENDING + self._add_row(test, TestStatus.PENDING, duration_ms=0, detail="") + + self._show_empty(False) def set_running(self, test_id: str) -> None: """Пометить тест как выполняющийся.""" - self._update(test_id, TestStatus.RUNNING, "") + self._update_row(test_id, TestStatus.RUNNING, duration_ms=0, detail="") def set_result(self, result: TestResult) -> None: """Показать финальный результат теста.""" detail = result.detail if result.status == TestStatus.FAIL else "" - self._update(result.id, result.status, detail) + self._update_row(result.id, result.status, result.duration_ms, detail) def reset(self) -> None: - """Сбросить все строки в пустое состояние.""" - for widget in self.query(Static): - wid = widget.id or "" - if wid.startswith("result-status-"): - for cls in _STATUS_CLASSES: - widget.remove_class(cls) - widget.add_class("result-status-skip") - widget.update("") - elif wid.startswith("result-detail-"): - widget.update("") + """Сбросить все строки в PENDING (перед повторным запуском).""" + for test_id in list(self._tests.keys()): + self._update_row(test_id, TestStatus.PENDING, duration_ms=0, detail="") - # ── Internal ────────────────────────────────────────────────────────────── + # ── Internal: empty state ─────────────────────────────────────────────── - def _update(self, test_id: str, status: TestStatus, detail: str) -> None: - try: - status_w = self.query_one(f"#result-status-{test_id}", Static) - detail_w = self.query_one(f"#result-detail-{test_id}", Static) - except NoMatches: + def _show_empty(self, visible: bool) -> None: + empty = self.query_one("#results-empty") + table = self.query_one("#results-table", DataTable) + if visible: + empty.remove_class("hidden") + table.add_class("hidden") + else: + empty.add_class("hidden") + table.remove_class("hidden") + + # ── Internal: строки таблицы ──────────────────────────────────────────── + + def _add_row( + self, test: TestInfo, status: TestStatus, duration_ms: int, detail: str + ) -> None: + table = self.query_one("#results-table", DataTable) + cells = self._row_cells(test, status, duration_ms, detail) + table.add_row(*cells, key=test.id) + + def _update_row( + self, test_id: str, status: TestStatus, duration_ms: int, detail: str + ) -> None: + test = self._tests.get(test_id) + if test is None: return + self._statuses[test_id] = status - for cls in _STATUS_CLASSES: - status_w.remove_class(cls) + table = self.query_one("#results-table", DataTable) + cells = self._row_cells(test, status, duration_ms, detail) + col_keys = [c.key for c in table.ordered_columns] + for col_key, value in zip(col_keys, cells): + table.update_cell(test_id, col_key, value) - text, css = _status_display(status) - status_w.update(text) - status_w.add_class(css) - detail_w.update(detail) + self._resort(table) + + def _row_cells( + self, test: TestInfo, status: TestStatus, duration_ms: int, detail: str + ) -> tuple: + """Собрать пять ячеек строки с учётом подсветки FAIL.""" + status_text, status_color = _STATUS_DISPLAY[status] + is_fail = status == TestStatus.FAIL + + bg = _FAIL_BG if is_fail else None + fg = "white" if is_fail else status_color + + name_style = Style(bgcolor=bg, bold=is_fail) + hil_style = Style(bgcolor=bg, color="white" if is_fail else "cyan") + status_style = Style(bgcolor=bg, color=fg, bold=True) + time_style = Style(bgcolor=bg, color="white" if is_fail else "grey70") + detail_style = Style(bgcolor=bg, color="white" if is_fail else "grey50") + + time_text = f"{duration_ms / 1000:.1f}с" if duration_ms > 0 else "" + hil_text = "[*]" if test.requires_hil else "[-]" + + return ( + Text(test.name, style=name_style), + Text(hil_text, style=hil_style), + Text(status_text, style=status_style), + Text(time_text, style=time_style), + Text(detail, style=detail_style), + ) + + def _resort(self, table: DataTable) -> None: + """ + Пересортировать: FAIL наверх, дальше RUNNING/PENDING/SKIP/PASS. + Внутри групп — исходный порядок реестра (стабильная сортировка). + + DataTable.sort(*columns, key=...) передаёт в key() кортеж значений + ЯЧЕЕК (не row_key) для указанных columns — поэтому сортируем по + содержимому самой ячейки "Тест" (используем как индекс в self._order) + и по тексту статуса, который мы сами туда пишем и полностью + контролируем — никаких приватных атрибутов DataTable не трогаем. + """ + name_col = table.ordered_columns[0].key + status_col = table.ordered_columns[2].key + + # text -> status enum, обратное к _STATUS_DISPLAY + status_by_text = {text: status for status, (text, _) in _STATUS_DISPLAY.items()} + # имя теста -> индекс в исходном реестре (для стабильности внутри группы) + name_to_order = { + self._tests[tid].name: idx + for idx, tid in enumerate(self._order) + if tid in self._tests + } + + def sort_key(cells: tuple) -> tuple: + name_cell, status_cell = cells + name_plain = ( + name_cell.plain if hasattr(name_cell, "plain") else str(name_cell) + ) + status_plain = ( + status_cell.plain if hasattr(status_cell, "plain") else str(status_cell) + ) + status = status_by_text.get(status_plain, TestStatus.PENDING) + order_idx = name_to_order.get(name_plain, 0) + return (_SORT_RANK.get(status, 99), order_idx) + + table.sort(name_col, status_col, key=sort_key) diff --git a/tools/production/app/screens/diag/test_list.py b/tools/production/app/screens/diag/test_list.py index 14bcad8..f3dfd49 100644 --- a/tools/production/app/screens/diag/test_list.py +++ b/tools/production/app/screens/diag/test_list.py @@ -12,46 +12,63 @@ test_list.py — виджет списка тестов с чекбоксами. from __future__ import annotations +from textual import on from textual.app import ComposeResult from textual.containers import Horizontal from textual.widget import Widget -from textual.widgets import Checkbox, Label +from textual.widgets import Button, Checkbox, Label class TestListPanel(Widget): """Левая колонка DiagScreen — список тестов с чекбоксами.""" - DEFAULT_CSS = "" # стили в diag.tcss - def __init__(self, **kwargs) -> None: super().__init__(**kwargs) # test_id → Checkbox для быстрого доступа self._checkboxes: dict[str, Checkbox] = {} + # test_id → True если HIL-тест недоступен без M5 (постоянное состояние, + # не зависящее от прогона). Отдельно от Checkbox.disabled, который + # временно перещёлкивается на время прогона тестов через set_enabled(). + self._hil_unavailable: dict[str, bool] = {} def compose(self) -> ComposeResult: yield Label("Тесты", classes="section-title") + with Horizontal(id="test-list-select-row"): + yield Button( + "Выбрать все", id="test-list-select-all", classes="-textual-compact" + ) + yield Button( + "Снять все", id="test-list-select-none", classes="-textual-compact" + ) # ── Public API ──────────────────────────────────────────────────────────── def populate(self, tests: list, m5_connected: bool) -> None: """ - Заполнить список тестами. + Заполнить список тестами. Изначально все тесты НЕ выбраны + (сервисник выбирает явно что запускать) — кроме HIL без M5, + которые всегда disabled. :param tests: list[TestInfo] :param m5_connected: True если M5StampPLC подключён """ - # Удалить старые строки (кроме заголовка) + # Удалить старые строки (кроме заголовка и select-row) for child in list(self.children): - if not child.has_class("section-title"): + if child.id != "test-list-select-row" and not child.has_class( + "section-title" + ): child.remove() self._checkboxes.clear() + self._hil_unavailable.clear() + rows = [] for test in tests: hil_unavailable = test.requires_hil and not m5_connected + self._hil_unavailable[test.id] = hil_unavailable cb = Checkbox( test.name, - value=not hil_unavailable, + value=False, disabled=hil_unavailable, id=f"cb-{test.id}", classes="-textual-compact", @@ -65,10 +82,16 @@ class TestListPanel(Widget): ) badge_text = "[HIL]" if test.requires_hil else "" - row = Horizontal(classes="test-row") - self.mount(row) - row.mount(cb) - row.mount(Label(badge_text, classes=hil_css)) + rows.append( + Horizontal( + cb, + Label(badge_text, classes=hil_css), + classes="test-row", + ) + ) + + if rows: + self.mount_all(rows) def get_selected_ids(self) -> list[str]: """Вернуть список id выбранных (checked + not disabled) тестов.""" @@ -77,7 +100,31 @@ class TestListPanel(Widget): ] def set_enabled(self, enabled: bool) -> None: - """Разрешить/запретить изменение чекбоксов во время прогона.""" - for cb in self._checkboxes.values(): - if not cb.disabled: # не трогать серые HIL-без-M5 + """ + Разрешить/запретить изменение чекбоксов во время прогона. + + HIL-чекбоксы без M5 остаются disabled всегда — их постоянное + состояние хранится в self._hil_unavailable и не зависит от прогона. + """ + for tid, cb in self._checkboxes.items(): + if self._hil_unavailable.get(tid, False): + cb.disabled = True # HIL без M5 — всегда недоступен + else: cb.disabled = not enabled + + self.query_one("#test-list-select-all", Button).disabled = not enabled + self.query_one("#test-list-select-none", Button).disabled = not enabled + + # ── Обработчики ─────────────────────────────────────────────────────────── + + @on(Button.Pressed, "#test-list-select-all") + def _on_select_all(self) -> None: + for tid, cb in self._checkboxes.items(): + if not self._hil_unavailable.get(tid, False): + cb.value = True + + @on(Button.Pressed, "#test-list-select-none") + def _on_select_none(self) -> None: + for tid, cb in self._checkboxes.items(): + if not self._hil_unavailable.get(tid, False): + cb.value = False diff --git a/tools/production/app/screens/flash.py b/tools/production/app/screens/flash.py index a2cce04..c5b0e92 100644 --- a/tools/production/app/screens/flash.py +++ b/tools/production/app/screens/flash.py @@ -30,6 +30,7 @@ from textual.widgets import ( from ..flasher import Flasher from ..models import FlashProgress, FlashTarget +from ..widgets import AppFrame logger = logging.getLogger(__name__) @@ -39,7 +40,7 @@ class FlashScreen(Screen): Экран прошивки. Messages: - FlashDone(success) — прошивка завершена + FlashDone(success, target) — прошивка завершена """ BINDINGS = [ @@ -47,9 +48,10 @@ class FlashScreen(Screen): ] class FlashDone(Message): - def __init__(self, success: bool) -> None: + def __init__(self, success: bool, target: Optional[FlashTarget] = None) -> None: super().__init__() self.success = success + self.target = target def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @@ -57,48 +59,42 @@ class FlashScreen(Screen): self._flashing = False def compose(self) -> ComposeResult: - with Vertical(): + with AppFrame(id="flash-frame"): yield Label( - "⚡ Прошивка платы — BootROM SDP обнаружен", + "⚡ Загрузка прошивки на плату (BootROM SDP обнаружен)", id="flash-title", ) with Vertical(id="flash-target-group"): - yield Label("Что прошить?", classes="section-title") + yield Label("Выберите файл для загрузки", classes="section-title") with RadioSet(id="flash-radio"): yield RadioButton( - "firmware_test (диагностическая прошивка)", + "Диагностическая прошивка (firmware_test)", id="radio-fw-test", value=True, ) yield RadioButton( - "Production (bootloader + tft_app)", + "Серийная прошивка (bootloader + tft_app)", id="radio-production", ) yield RadioButton( - "Кастомный бинарь...", + "Другое (подготовленный бинарный файл)", id="radio-custom", ) with Horizontal(id="flash-custom-path", classes="hidden"): yield Input( - placeholder="Путь к HAB-бинарю (.bin)", + placeholder="Путь к бинарному файлу (.bin)", id="flash-custom-input", ) with Horizontal(id="flash-btn-row"): + yield Button("▶ Загрузить ПО", id="flash-btn-flash", variant="warning") + yield Button("⚠ Очистить память", id="flash-btn-erase", variant="error") yield Button( - "▶ Прошить", - id="flash-btn-flash", - variant="warning", - ) - yield Button( - "⚠ Chip Erase", - id="flash-btn-erase", - variant="error", + "✕ Выйти из программы", id="flash-btn-quit", variant="default" ) - yield ProgressBar(id="flash-progress-bar", show_eta=False) - yield Label("", id="flash-progress-label") + yield ProgressBar(id="flash-progress-bar", show_eta=False, classes="hidden") yield Log(id="flash-log", auto_scroll=True) # ── Обработчики ─────────────────────────────────────────────────────────── @@ -118,15 +114,19 @@ class FlashScreen(Screen): return target, bin_path = self._resolve_target() if target is None: - self._log("⚠ Укажите путь к бинарю") + self._log("⚠ Укажите путь к бинарному файлу!") return self._do_flash(target, bin_path) @on(Button.Pressed, "#flash-btn-erase") def _on_erase_pressed(self) -> None: - if self._flashing: - return - self._do_erase() + if not self._flashing: + self._do_erase() + + @on(Button.Pressed, "#flash-btn-quit") + def _on_quit_pressed(self) -> None: + if not self._flashing: + self.app.exit() def action_go_back(self) -> None: if not self._flashing: @@ -137,6 +137,7 @@ class FlashScreen(Screen): @work(exclusive=True, thread=False) async def _do_flash(self, target: FlashTarget, bin_path: Optional[Path]) -> None: self._set_busy(True) + self._show_progress(True) self._log(f"▶ Прошивка: {target.value}") ok = await self._flasher.flash( target=target, @@ -144,16 +145,23 @@ class FlashScreen(Screen): progress_cb=self._on_progress, ) self._set_busy(False) + self._finish_progress(ok) self._log("✅ Готово" if ok else "❌ Ошибка") - self.post_message(self.FlashDone(success=ok)) + self.post_message(self.FlashDone(success=ok, target=target)) @work(exclusive=True, thread=False) async def _do_erase(self) -> None: self._set_busy(True) - self._log("⚠ Chip erase (~30 с)...") + self._show_progress(True) + self._log("⚠ Очистка флеш памяти (~30 с)...") ok = await self._flasher.erase_chip(progress_cb=self._on_progress) self._set_busy(False) - self._log("✅ Chip erase завершён" if ok else "❌ Chip erase: ошибка") + self._finish_progress(ok) + self._log( + "✅ Очистка флеш памяти завершена" + if ok + else "❌ Очистка флеш памяти: ошибка" + ) # ── Вспомогательные ─────────────────────────────────────────────────────── @@ -177,18 +185,29 @@ class FlashScreen(Screen): return None, None async def _on_progress(self, progress: FlashProgress) -> None: - self.query_one("#flash-progress-bar", ProgressBar).update( - progress=progress.percent - ) - self.query_one("#flash-progress-label", Label).update( - f"{progress.phase} {progress.percent}%" - ) + bar = self.query_one("#flash-progress-bar", ProgressBar) + bar.update(total=100, progress=progress.percent) self._log(progress.message) + def _show_progress(self, visible: bool) -> None: + """Показать/скрыть прогресс-бар. Скрыт в простое — без анимации.""" + bar = self.query_one("#flash-progress-bar", ProgressBar) + if visible: + bar.update(total=100, progress=0) + bar.remove_class("hidden") + else: + bar.add_class("hidden") + + def _finish_progress(self, success: bool) -> None: + """Зафиксировать прогресс-бар на 100% с финальным статусом.""" + bar = self.query_one("#flash-progress-bar", ProgressBar) + bar.update(total=100, progress=100) + def _set_busy(self, busy: bool) -> None: self._flashing = busy self.query_one("#flash-btn-flash", Button).disabled = busy self.query_one("#flash-btn-erase", Button).disabled = busy + self.query_one("#flash-btn-quit", Button).disabled = busy def _log(self, msg: str) -> None: try: diff --git a/tools/production/app/screens/post_flash.py b/tools/production/app/screens/post_flash.py new file mode 100644 index 0000000..891ba3e --- /dev/null +++ b/tools/production/app/screens/post_flash.py @@ -0,0 +1,88 @@ +""" +post_flash.py — экран-промпт после успешной прошивки firmware_test. + +Показывается только для FlashTarget.FIRMWARE_TEST (диагностическая прошивка) — +после неё плата должна быть переведена в нормальный режим (BOOT_MOD → GND) +чтобы попасть в DiagScreen. Без этого промпта TUI зацикливался: SDP всё ещё +виден → WaitingScreen снова детектит SDP → снова FlashScreen. + +Для PRODUCTION/CUSTOM (либо когда прошивка не удалась) промпт не нужен — +сразу WaitingScreen с тем же общим поведением автодетекта. + +Три способа покинуть экран: + 1. Оператор успел сменить BootMode и нажал "Готово" → WaitingScreen + (далее автодетект сам поймает CDC, если плата уже перезагружена) + 2. Оператор нажал "Выйти" → app.exit() + 3. Таймаут (по умолчанию 40 с) → автоматически WaitingScreen +""" + +from __future__ import annotations + +from textual.app import ComposeResult +from textual.containers import Horizontal +from textual.message import Message +from textual.screen import Screen +from textual.timer import Timer +from textual.widgets import Button, Label, Static + +from ..widgets import AppFrame + +_AUTO_TIMEOUT_S = 40 +_TICK_S = 1.0 + + +class PostFlashScreen(Screen): + """ + Промпт смены BootMode после прошивки firmware_test. + + Messages: + Done() — оператор подтвердил или истёк таймаут → пора в WaitingScreen + """ + + class Done(Message): + """Готово к переходу на WaitingScreen.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._remaining: int = _AUTO_TIMEOUT_S + self._timer: Timer | None = None + + def compose(self) -> ComposeResult: + with AppFrame(id="post-flash-frame"): + yield Label("✅ firmware_test успешно записан", id="post-flash-title") + + yield Static( + "Переведите плату в нормальный режим:\nBOOT_MOD_1 → GND → Reset", + id="post-flash-instruction", + ) + + yield Static("", id="post-flash-countdown") + + with Horizontal(id="post-flash-btn-row"): + yield Button("✓ Готово", id="post-flash-btn-ok", variant="success") + yield Button("✕ Выйти", id="post-flash-btn-quit", variant="default") + + def on_mount(self) -> None: + self._update_countdown() + self._timer = self.set_interval(_TICK_S, self._tick) + + def on_unmount(self) -> None: + if self._timer is not None: + self._timer.stop() + + def _tick(self) -> None: + self._remaining -= 1 + self._update_countdown() + if self._remaining <= 0: + self.post_message(self.Done()) + + def _update_countdown(self) -> None: + self.query_one("#post-flash-countdown", Static).update( + f"Автопереход через: {self._remaining}с" + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "post-flash-btn-ok": + self.post_message(self.Done()) + elif event.button.id == "post-flash-btn-quit": + self.app.exit() diff --git a/tools/production/app/screens/waiting.py b/tools/production/app/screens/waiting.py index 379eb1a..fbd9c42 100644 --- a/tools/production/app/screens/waiting.py +++ b/tools/production/app/screens/waiting.py @@ -9,12 +9,14 @@ from __future__ import annotations from textual.app import ComposeResult from textual.css.query import NoMatches +from textual.message import Message from textual.screen import Screen from textual.timer import Timer from textual.widgets import Static -from textual.message import Message + from ..flasher import Flasher from ..models import AppMode +from ..widgets import AppFrame _DETECT_INTERVAL_S = 1.5 _SPIN_INTERVAL_S = 0.1 @@ -43,19 +45,17 @@ class WaitingScreen(Screen): self._spin_timer: Timer | None = None def compose(self) -> ComposeResult: - yield Static("TFT Indicator Board\nService Tool", id="waiting-logo") - yield Static("Подключите плату к USB...", id="waiting-hint") - yield Static(_SPINNER_FRAMES[0], id="waiting-spinner") + with AppFrame(id="waiting-frame"): + yield Static("TFT Indicator Board\nService Tool", id="waiting-logo") + yield Static("Подключите плату к USB...", id="waiting-hint") + yield Static(_SPINNER_FRAMES[0], id="waiting-spinner") def on_mount(self) -> None: self._detect_timer = self.set_interval(_DETECT_INTERVAL_S, self._poll_usb) self._spin_timer = self.set_interval(_SPIN_INTERVAL_S, self._spin) def on_unmount(self) -> None: - if self._detect_timer: - self._detect_timer.stop() - if self._spin_timer: - self._spin_timer.stop() + self._stop_timers() # ── Internal ────────────────────────────────────────────────────────────── diff --git a/tools/production/app/widgets/__init__.py b/tools/production/app/widgets/__init__.py new file mode 100644 index 0000000..9957b8c --- /dev/null +++ b/tools/production/app/widgets/__init__.py @@ -0,0 +1,5 @@ +"""widgets — общие переиспользуемые виджеты TUI.""" + +from .app_frame import AppFrame + +__all__ = ["AppFrame"] diff --git a/tools/production/app/widgets/app_frame.py b/tools/production/app/widgets/app_frame.py new file mode 100644 index 0000000..11e6bf6 --- /dev/null +++ b/tools/production/app/widgets/app_frame.py @@ -0,0 +1,40 @@ +""" +app_frame.py — общий контейнер фиксированного размера для всех экранов. + +Решает две задачи: +1. UX: визуальная рамка фиксированного размера (как в ratatui-приложениях, + напр. binsider), центрированная в терминале вне зависимости от размера окна. +2. Баг Textual 8.x: AssertionError при mouse drag, когда Screen выступает + content_widget напрямую (assert isinstance(content_widget.parent, Widget) + падает, потому что в этом сценарии родитель не является валидным Widget). + AppFrame как промежуточный контейнер между Screen и содержимым устраняет + этот сценарий — content_widget при drag теперь всегда AppFrame или его + потомок, у которых .parent валиден. + +Использование в экране:: + + def compose(self) -> ComposeResult: + with AppFrame(): + yield Static("...") + yield Button("...") +""" + +from __future__ import annotations + +from textual.containers import Container + + +class AppFrame(Container): + """Контейнер фиксированного размера, центрированный в Screen.""" + + DEFAULT_CSS = """ + AppFrame { + width: 100%; + height: 100%; + max-width: 160; + max-height: 50; + border: heavy $primary; + background: $surface; + padding: 1 2; + } + """ diff --git a/tools/production/service_tui.log b/tools/production/service_tui.log index ead3ed2..8e48d2f 100644 --- a/tools/production/service_tui.log +++ b/tools/production/service_tui.log @@ -660,3 +660,1186 @@ 2026-06-29 19:36:47,180 DEBUG asyncio Using selector: KqueueSelector 2026-06-29 19:41:53,210 INFO app.m5_client M5StampPLC не найден 2026-06-29 19:41:56,267 WARNING app.firmware_client _recv_until timeout after 3.0 s +2026-06-30 11:07:04,658 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:07:32,037 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:07:38,901 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 11:07:38,923 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 11:07:38,923 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-30 11:07:38,925 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-30 11:07:39,117 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv +2026-06-30 11:07:39,117 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv +2026-06-30 11:07:39,247 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 100ms +2026-06-30 11:07:43,943 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:07:43,943 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 11:07:44,116 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 11:07:44,135 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:07:45,522 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 11:07:45,698 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:08:20,418 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:08:20,670 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 11:09:41,208 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 11:09:41,239 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 11:09:41,805 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:09:41,805 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 11:09:41,995 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:43,425 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 11:09:43,598 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:44,216 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:44,426 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:44,610 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:44,789 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 11:09:44,957 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:44,957 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 11:09:45,207 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 11:10:12,371 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:10:16,917 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:10:57,370 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 11:12:15,443 WARNING app.orchestrator Operator confirm timeout: display_red +2026-06-30 11:12:25,456 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 11:14:57,478 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:14:59,027 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:16:37,862 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:17:04,481 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 11:17:04,488 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 11:17:05,064 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:17:05,064 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 11:17:05,238 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:06,649 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 11:17:06,832 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:17:41,515 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:17:41,755 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: +2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 11:17:45,454 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 11:17:45,472 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 11:17:45,888 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:17:45,888 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 11:17:46,064 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:47,468 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 11:17:47,641 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:48,263 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:48,472 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:48,646 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:48,815 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 11:17:48,986 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:48,986 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 11:17:49,175 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 11:17:49,195 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 11:18:16,295 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:18:18,011 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:20:23,814 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 11:22:00,580 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:22:02,131 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:24:08,449 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:24:09,999 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:24:39,035 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:24:40,580 INFO app.m5_client M5StampPLC не найден +2026-06-30 11:25:23,410 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:25:58,243 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:26:30,720 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 11:26:58,255 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:12:49,218 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:12:57,749 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 12:12:57,771 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:12:58,350 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:12:58,350 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:12:58,522 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:12:59,916 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:13:00,092 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:13:08,189 DEBUG app.flasher flash_usb [erase]: Response status = 10004 (0x2714) No response packet from target device. +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: [ERROR] Команда завершилась с кодом 1 +2026-06-30 12:13:12,578 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:13:12,604 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:13:13,005 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:13:13,005 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:13:13,178 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:14,579 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:13:14,753 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:15,335 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:15,546 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:15,722 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:15,893 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:13:16,067 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:16,067 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:13:16,282 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:13:46,755 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:13:48,420 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:14:17,920 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured +2026-06-30 12:14:50,713 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:14:52,420 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:15:19,761 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:15:21,306 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:15:23,620 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:15:31,588 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:16:53,965 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:16:56,990 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 12:16:57,017 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:16:57,543 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:16:57,543 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:16:57,715 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:16:57,736 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:16:59,225 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:16:59,405 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:17:34,744 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:17:35,036 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 12:17:38,393 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:17:38,415 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:17:38,820 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:17:38,821 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:17:38,996 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:40,400 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:17:40,575 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:41,209 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:41,417 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:41,589 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:41,758 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:17:41,926 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:41,926 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:17:42,112 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:18:16,089 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:18:17,817 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:19:51,239 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 12:20:11,226 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:20:12,767 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:20:46,816 WARNING app.orchestrator Operator confirm timeout: mqs_tone +2026-06-30 12:23:08,021 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:23:09,570 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:24:19,465 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 12:28:29,368 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:28:32,469 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 12:28:32,491 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:28:33,043 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:28:33,043 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:28:33,212 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:28:34,621 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:28:34,792 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:29:10,122 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:29:10,362 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 12:29:42,254 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:29:42,274 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:29:42,779 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:29:42,779 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:29:42,951 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:44,350 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:29:44,519 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:45,125 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:45,328 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:45,502 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:45,675 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:29:45,847 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:45,847 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:29:46,038 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:30:13,179 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:30:57,597 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:31:07,114 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 12:31:07,127 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:31:07,633 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:31:07,634 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:31:07,805 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:31:07,825 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:31:07,825 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:09,209 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:31:09,387 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:31:43,890 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:31:44,130 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: +2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 12:31:54,530 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:31:54,544 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:31:54,994 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:31:54,994 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:31:55,169 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:56,575 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:31:56,748 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:57,367 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:57,577 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:57,752 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:57,923 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:31:58,094 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:58,094 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:31:58,282 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:32:39,880 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:34:28,288 WARNING app.orchestrator Operator confirm timeout: mqs_tone +2026-06-30 12:36:10,339 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:36:12,274 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:36:16,715 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:36:17,666 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 12:36:20,059 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:36:21,609 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:37:39,767 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:37:42,638 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:37:42,653 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:37:43,208 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:37:43,208 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:37:43,385 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:37:43,404 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:37:43,404 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:44,787 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:37:44,965 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:45,567 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:45,775 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:45,949 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:46,123 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:37:46,295 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:46,295 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:37:46,484 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:38:28,112 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:39:00,162 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:39:01,711 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:39:16,105 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:39:29,578 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:39:29,591 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:39:30,119 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:39:30,119 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:39:30,293 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:39:30,313 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:31,690 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:39:31,867 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:32,499 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:32,710 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:32,885 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:33,057 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:39:33,232 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:33,232 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:39:33,420 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:39:42,605 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:39:45,866 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:39:45,892 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:39:46,402 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:39:46,402 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:39:46,575 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:39:46,594 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:39:46,594 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:47,978 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:39:48,156 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:48,777 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:48,988 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:49,163 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:49,340 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:39:49,511 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:49,511 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:39:49,695 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:40:34,546 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 12:40:34,566 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 12:40:35,182 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:40:35,182 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 12:40:35,411 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 12:40:35,452 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 12:40:35,452 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:36,857 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 12:40:37,029 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:37,659 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:37,878 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:38,054 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:38,226 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 12:40:38,396 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:38,396 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) +2026-06-30 12:40:38,581 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 12:41:20,289 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:41:45,632 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:41:47,180 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:50:05,155 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:50:06,706 INFO app.m5_client M5StampPLC не найден +2026-06-30 12:53:12,550 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:53:19,153 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 +2026-06-30 12:53:34,236 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 12:53:35,947 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:00:39,178 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:00:40,865 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:32:56,441 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:32:57,988 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:35:28,536 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:35:45,185 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 13:35:45,204 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 13:35:45,204 DEBUG app.flasher flash_usb [firmware_test]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-30 13:35:45,206 DEBUG app.flasher flash_usb [firmware_test]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-30 13:35:45,381 DEBUG app.flasher flash_usb [firmware_test]: Removed virtual environment at: .venv +2026-06-30 13:35:45,381 DEBUG app.flasher flash_usb [firmware_test]: Creating virtual environment at: .venv +2026-06-30 13:35:45,517 DEBUG app.flasher flash_usb [firmware_test]: Installed 66 packages in 104ms +2026-06-30 13:35:50,049 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 13:35:50,049 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 13:35:50,221 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:51,617 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 13:35:51,786 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:52,392 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:52,603 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:52,777 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:52,951 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 13:35:53,121 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:53,122 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68364 (0x10b0c) +2026-06-30 13:35:53,309 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68364 байт +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 13:36:18,782 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:40:43,635 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 13:40:50,292 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:40:51,840 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:42:37,034 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 13:42:42,356 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:42:43,901 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:43:43,721 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 13:43:48,120 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 13:43:50,861 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:43:52,409 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:44:36,067 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured +2026-06-30 13:48:46,897 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:49:26,682 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:50:01,537 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:50:49,483 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 13:50:51,216 INFO app.m5_client M5StampPLC не найден +2026-06-30 13:56:22,896 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 14:13:37,172 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:13:38,722 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:16:04,892 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:16:11,539 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 +2026-06-30 14:16:18,139 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 +2026-06-30 14:16:20,826 ERROR app.app CDC connect failed: read failed: [Errno 6] Device not configured +2026-06-30 14:16:23,904 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:17:25,582 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 14:18:32,310 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:18:33,929 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:19:27,641 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured +2026-06-30 14:19:57,182 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:19:58,730 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:36:34,945 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:36:36,495 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:38:19,642 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:38:21,192 INFO app.m5_client M5StampPLC не найден +2026-06-30 14:38:26,849 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 14:38:58,972 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:43:53,430 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:45:10,133 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:45:51,286 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:55:19,262 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:55:56,114 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:56:46,050 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 14:57:28,802 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip +2026-06-30 14:57:28,820 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 14:57:28,822 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python +2026-06-30 14:57:28,825 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 +2026-06-30 14:57:29,003 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv +2026-06-30 14:57:29,003 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv +2026-06-30 14:57:29,120 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 86ms +2026-06-30 14:57:33,713 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 14:57:33,713 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 14:57:33,888 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: Операция: chip erase +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 14:57:33,907 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 14:57:33,907 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:57:35,315 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 14:57:35,485 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 14:58:10,903 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 14:58:11,146 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: OK (1с) +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: Reset +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 14:58:11,167 DEBUG app.flasher flash_usb [erase]: +2026-06-30 14:58:11,167 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён +2026-06-30 14:58:17,157 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release +2026-06-30 14:58:17,178 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead +2026-06-30 14:58:17,661 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 14:58:17,661 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. +2026-06-30 14:58:17,832 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 +2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:19,246 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. +2026-06-30 14:58:19,423 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,055 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,265 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,440 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,618 DEBUG app.flasher flash_usb [firmware_test]: Writing memory +2026-06-30 14:58:20,789 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,789 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68364 (0x10b0c) +2026-06-30 14:58:20,971 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68364 байт +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: Reset +2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── +2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset +2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: +2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно +2026-06-30 14:58:47,116 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:02:20,779 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:02:22,328 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:03:07,785 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:03:09,333 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:03:56,019 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:03:57,569 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:05:47,811 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:05:49,358 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:07:27,853 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:07:29,401 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:08:24,771 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:08:26,321 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:08:55,013 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:08:56,561 INFO app.m5_client M5StampPLC не найден From 0b16f5cf19c23974d0e7068a36b27f745f6f5c08 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Tue, 30 Jun 2026 16:28:48 +0300 Subject: [PATCH 4/9] =?UTF-8?q?#=20bsp:=20usd=20-=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=B0?= =?UTF-8?q?=D0=B3=D0=B0=20=D1=81=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bsp/sd/src/sd.c | 37 +++++++++++++++++++++++++ firmware/test/src/tests/test_buttons.c | 10 +++---- firmware/test/src/tests/test_can.c | 2 +- firmware/test/src/tests/test_display.c | 14 +++++----- firmware/test/src/tests/test_mqs.c | 6 ++-- firmware/test/src/tests/test_opto.c | 2 +- firmware/test/src/tests/test_usd.c | 4 +-- tools/production/app/app.tcss | 4 +++ tools/production/app/firmware_client.py | 4 +-- tools/production/app/m5_client.py | 4 +-- tools/production/service_tui.log | 22 +++++++++++++++ 11 files changed, 86 insertions(+), 23 deletions(-) diff --git a/bsp/sd/src/sd.c b/bsp/sd/src/sd.c index d2ecb45..05c5df2 100644 --- a/bsp/sd/src/sd.c +++ b/bsp/sd/src/sd.c @@ -5,8 +5,10 @@ #include "bsp/sd.h" #include "fsl_sd.h" +#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */ #include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */ +#include /* memset */ /* --------------------------------------------------------------------------- * Глобальный дескриптор карты — нужен SDK-стеку (передаётся по указателю * в BOARD_SD_Config и sd_disk_initialize через g_sd). @@ -51,6 +53,33 @@ bsp_status_t bsp_sd_init(void) return BSP_OK; } + /* + * Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД + * повторной инициализацией. Без этого non-blocking host driver SDK + * (fsl_sdmmc_host.c) может остаться в состоянии "ожидание завершения + * предыдущей транзакции" после SD_HostDeinit() на прошлом прогоне — + * физическая транзакция уже умерла вместе с deinit, но внутренний + * флаг ожидания interrupt остаётся выставленным, и следующий f_mount() + * блокируется навсегда в ожидании события, которое никогда не придёт. + * + * USDHC_Reset с маской kUSDHC_ResetAll сбрасывает контроллер на + * регистровом уровне, не полагаясь на состояние, оставленное + * предыдущей сессией. Безопасно вызывать даже при первом запуске — + * базовый адрес уже доступен через BOARD_SDMMC_SD_HOST_BASEADDR + * (clock на этот момент должен быть включён, см. ниже). + */ + CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */ + USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U); + + /* + * Обнулить g_sd целиком перед повторной конфигурацией. BOARD_SD_Config() + * перезаписывает только часть полей — указатели на callback-структуры + * non-blocking host driver и внутренние DMA-дескрипторы могут остаться + * от предыдущей сессии, если их явно не сбросить. + */ + (void) memset(&g_sd, 0, sizeof(g_sd)); + g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */ + ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */ /* @@ -72,6 +101,14 @@ bsp_status_t bsp_sd_deinit(void) SD_HostDeinit(&g_sd); SD_SetCardPower(&g_sd, false); + /* + * Дополнительный аппаратный сброс сразу после deinit — гарантирует, + * что FIFO и state machine USDHC не останутся в промежуточном + * состоянии независимо от того, что делает (или не делает) + * SD_HostDeinit() из SDK на уровне регистров. + */ + USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U); + g_s_initialized = false; g_s_host_configured = false; return BSP_OK; diff --git a/firmware/test/src/tests/test_buttons.c b/firmware/test/src/tests/test_buttons.c index c5764b4..acdd6ef 100644 --- a/firmware/test/src/tests/test_buttons.c +++ b/firmware/test/src/tests/test_buttons.c @@ -143,14 +143,14 @@ static void buttons_test_init(void) */ static test_result_t buttons_test_run(void) { - if (!wait_button_press(BSP_BUTTON_1, "btn1_press", "Press Test_But_1", BTN_PRESS_TIMEOUT_MS)) + if (!wait_button_press(BSP_BUTTON_1, "btn1_press", "Нажмите кнопку 1", BTN_PRESS_TIMEOUT_MS)) { - return make_skip("btn1_press timeout"); + return make_skip("Кнопка 1 - таймаут"); } - if (!wait_button_press(BSP_BUTTON_2, "btn2_press", "Press Test_But_2", BTN_PRESS_TIMEOUT_MS)) + if (!wait_button_press(BSP_BUTTON_2, "btn2_press", "Нажмите кнопку 2", BTN_PRESS_TIMEOUT_MS)) { - return make_skip("btn2_press timeout"); + return make_skip("Кнопка 2 - таймаут"); } return (test_result_t){ @@ -165,7 +165,7 @@ static test_result_t buttons_test_run(void) /** @brief Дескриптор тест-модуля кнопок для реестра test_runner. */ const test_module_t K_TEST_BUTTONS = { .id = "buttons", - .name = "Buttons", + .name = "Тактовые кнопки", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_can.c b/firmware/test/src/tests/test_can.c index 02aed4a..1ca7484 100644 --- a/firmware/test/src/tests/test_can.c +++ b/firmware/test/src/tests/test_can.c @@ -197,7 +197,7 @@ static test_result_t can_run(void) /** @brief Дескриптор для регистрации в test_runner. */ const test_module_t K_TEST_CAN = { .id = "can", - .name = "CAN Network", + .name = "CAN шина", .critical = false, .requires_hil = true, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_display.c b/firmware/test/src/tests/test_display.c index 9af17ac..1a1a3ad 100644 --- a/firmware/test/src/tests/test_display.c +++ b/firmware/test/src/tests/test_display.c @@ -222,11 +222,11 @@ static bool step_rot_apply_and_confirm(bsp_display_rotation_t rotation, const ch static bool step_rotation(test_result_t *p_out) { bool ok = step_rot_apply_and_confirm(BSP_DISPLAY_ROTATE_0, "display_rot0", - "Screen: left RED, right BLUE?", p_out); + "Дисплей: слева - красный, справа - синий?", p_out); if (ok) { ok = step_rot_apply_and_confirm(BSP_DISPLAY_FLIP_HORIZONTAL, "display_rot_base", - "Left RED and right BLUE swapped sides?", p_out); + "Дисплей: слева - синий, справа - красный?", p_out); } /* Восстановить ROTATE_0 в любом исходе */ @@ -264,19 +264,19 @@ static test_result_t display_test_run(void) test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U, .detail = { 0 } }; /* Этап 1: цвет */ - if (!step_color(DISPLAY_COLOR_RED, "display_red", "Screen is solid red?", &fail_result)) + if (!step_color(DISPLAY_COLOR_RED, "display_red", "Дисплей залит красным?", &fail_result)) { return fail_result; } - if (!step_color(DISPLAY_COLOR_GREEN, "display_green", "Screen is solid green?", &fail_result)) + if (!step_color(DISPLAY_COLOR_GREEN, "display_green", "Дисплей залит зеленым?", &fail_result)) { return fail_result; } - if (!step_color(DISPLAY_COLOR_BLUE, "display_blue", "Screen is solid blue?", &fail_result)) + if (!step_color(DISPLAY_COLOR_BLUE, "display_blue", "Дисплей залит синим?", &fail_result)) { return fail_result; } - if (!step_color(DISPLAY_COLOR_WHITE, "display_white", "Screen is solid white?", &fail_result)) + if (!step_color(DISPLAY_COLOR_WHITE, "display_white", "Дисплей залит белым?", &fail_result)) { return fail_result; } @@ -308,7 +308,7 @@ static void display_test_deinit(void) const test_module_t K_TEST_DISPLAY = { .id = "display", - .name = "TFT Display", + .name = "TFT дисплей", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_mqs.c b/firmware/test/src/tests/test_mqs.c index aefb362..67976a7 100644 --- a/firmware/test/src/tests/test_mqs.c +++ b/firmware/test/src/tests/test_mqs.c @@ -190,7 +190,7 @@ static test_result_t test_mqs_run(void) */ const confirm_params_t K_PARAMS = { .id = "mqs_tone", - .prompt = "Do you hear a tone?", + .prompt = "Звук слышен?", .timeout_ms = 15000U, }; @@ -200,7 +200,7 @@ static test_result_t test_mqs_run(void) { return (test_result_t){ .status = TEST_STATUS_FAIL, - .detail = "operator: no sound", + .detail = "Звук не был услышан", }; } @@ -220,7 +220,7 @@ static void test_mqs_deinit(void) const test_module_t K_TEST_MQS = { .id = "mqs", - .name = "Audio Out", + .name = "Аудио-выход", .critical = false, .requires_hil = false, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_opto.c b/firmware/test/src/tests/test_opto.c index 858a566..1b300b1 100644 --- a/firmware/test/src/tests/test_opto.c +++ b/firmware/test/src/tests/test_opto.c @@ -182,7 +182,7 @@ static test_result_t opto_run(void) /** @brief Дескриптор для регистрации в test_runner. */ const test_module_t K_TEST_OPTO = { .id = "opto", - .name = "Opto Inputs", + .name = "Оптопары", .critical = false, .requires_hil = true, .pre_confirm_prompt = NULL, diff --git a/firmware/test/src/tests/test_usd.c b/firmware/test/src/tests/test_usd.c index f136f6d..2eeb7da 100644 --- a/firmware/test/src/tests/test_usd.c +++ b/firmware/test/src/tests/test_usd.c @@ -343,10 +343,10 @@ static void usd_deinit(void) const test_module_t K_TEST_USD = { .id = "usd", - .name = "microSD Card", + .name = "microSD карта", .critical = false, .requires_hil = false, - .pre_confirm_prompt = "Insert microSD card and press OK", + .pre_confirm_prompt = "Карта вставлена в слот?", .init = usd_init, .run = usd_run, .deinit = usd_deinit, diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss index 53bf7f0..b967115 100644 --- a/tools/production/app/app.tcss +++ b/tools/production/app/app.tcss @@ -287,6 +287,10 @@ ResultsPanel .section-title { width: 100%; } +#results-empty.hidden { + display: none; +} + #results-empty-text { color: $text-muted; text-align: center; diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py index 342869a..b872bf6 100644 --- a/tools/production/app/firmware_client.py +++ b/tools/production/app/firmware_client.py @@ -134,7 +134,7 @@ class FirmwareClient: """Сериализовать dict в JSON и отправить строку (blocking).""" assert self._ser is not None line = json.dumps(obj, separators=(",", ":")) + "\n" - self._ser.write(line.encode("ascii")) + self._ser.write(line.encode("utf-8")) self._ser.flush() def _read_line(self) -> Optional[dict]: @@ -143,7 +143,7 @@ class FirmwareClient: raw = self._ser.readline() if not raw: return None - return _parse_event(raw.decode("ascii", errors="replace")) + return _parse_event(raw.decode("utf-8", errors="replace")) async def _send(self, obj: dict) -> None: """Отправить JSON-команду (async wrapper).""" diff --git a/tools/production/app/m5_client.py b/tools/production/app/m5_client.py index 99fe3e8..03ea043 100644 --- a/tools/production/app/m5_client.py +++ b/tools/production/app/m5_client.py @@ -121,13 +121,13 @@ class M5Client: """Отправить команду, прочитать ответ (blocking).""" assert self._ser is not None line = json.dumps(cmd, separators=(",", ":")) + "\n" - self._ser.write(line.encode("ascii")) + self._ser.write(line.encode("utf-8")) self._ser.flush() raw = self._ser.readline() if not raw: return None try: - return json.loads(raw.decode("ascii", errors="replace").strip()) + return json.loads(raw.decode("utf-8", errors="replace").strip()) except json.JSONDecodeError: return None diff --git a/tools/production/service_tui.log b/tools/production/service_tui.log index 8e48d2f..7ef503c 100644 --- a/tools/production/service_tui.log +++ b/tools/production/service_tui.log @@ -1843,3 +1843,25 @@ 2026-06-30 15:08:26,321 INFO app.m5_client M5StampPLC не найден 2026-06-30 15:08:55,013 DEBUG asyncio Using selector: KqueueSelector 2026-06-30 15:08:56,561 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:09:59,613 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:10:01,161 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:12:33,081 WARNING app.firmware_client _recv_until timeout after 120.0 s +2026-06-30 15:14:40,777 WARNING app.firmware_client _recv_until timeout after 120.0 s +2026-06-30 15:33:29,207 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:33:54,602 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:33:56,280 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:37:25,364 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:37:27,091 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:42:36,030 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:42:37,715 INFO app.m5_client M5StampPLC не найден +2026-06-30 15:43:19,205 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 15:43:22,852 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 15:43:23,345 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 15:43:23,701 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured +2026-06-30 15:43:28,104 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 15:43:29,654 INFO app.m5_client M5StampPLC не найден +2026-06-30 16:19:09,052 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 16:19:10,697 INFO app.m5_client M5StampPLC не найден +2026-06-30 16:20:10,951 ERROR app.orchestrator HIL confirm без M5: opto_in1_active +2026-06-30 16:27:42,558 DEBUG asyncio Using selector: KqueueSelector +2026-06-30 16:27:44,211 INFO app.m5_client M5StampPLC не найден From e2f00eca99a7069c2eede83b457bd18d761498b6 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Tue, 30 Jun 2026 17:31:05 +0300 Subject: [PATCH 5/9] # firmware_test: TUI app current state --- .gitignore | 1 + tools/production/app/app.py | 14 +- tools/production/app/app.tcss | 13 + tools/production/app/firmware_client.py | 7 + tools/production/app/orchestrator.py | 80 +- .../app/screens/connection_watcher.py | 78 + tools/production/app/screens/diag/__init__.py | 71 +- tools/production/app/screens/diag/results.py | 65 +- .../production/app/screens/diag/test_list.py | 2 +- tools/production/app/screens/flash.py | 56 +- tools/production/app/screens/post_flash.py | 4 +- tools/production/app/screens/waiting.py | 35 +- tools/production/docs/INITIAL_PLAN.md | 488 +++++ tools/production/docs/TUI_PLAN.md | 7 + .../{TUI_REPORT.md => docs/TUI_REPORT_1.md} | 0 tools/production/docs/TUI_REPORT_2.md | 8 + tools/production/docs/TUI_SESSION.md | 121 ++ tools/production/docs/pics/FLASH.png | Bin 0 -> 56806 bytes tools/production/docs/pics/POST_FLASH.png | Bin 0 -> 45998 bytes tools/production/docs/pics/TEST_SCREEN.png | Bin 0 -> 88874 bytes tools/production/docs/pics/WAITING.png | Bin 0 -> 35099 bytes .../docs/pics/WAITING_AFTER_LOSS.png | Bin 0 -> 39786 bytes tools/production/service_tui.log | 1867 ----------------- 23 files changed, 1005 insertions(+), 1912 deletions(-) create mode 100644 tools/production/app/screens/connection_watcher.py create mode 100644 tools/production/docs/INITIAL_PLAN.md create mode 100644 tools/production/docs/TUI_PLAN.md rename tools/production/{TUI_REPORT.md => docs/TUI_REPORT_1.md} (100%) create mode 100644 tools/production/docs/TUI_REPORT_2.md create mode 100644 tools/production/docs/TUI_SESSION.md create mode 100644 tools/production/docs/pics/FLASH.png create mode 100644 tools/production/docs/pics/POST_FLASH.png create mode 100644 tools/production/docs/pics/TEST_SCREEN.png create mode 100644 tools/production/docs/pics/WAITING.png create mode 100644 tools/production/docs/pics/WAITING_AFTER_LOSS.png delete mode 100644 tools/production/service_tui.log diff --git a/.gitignore b/.gitignore index 36a74bd..342ec63 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ build* Testing/ !docs/testing !build.just +service_tui.log # Файлы настройки среды разработки .project diff --git a/tools/production/app/app.py b/tools/production/app/app.py index 51d95b5..f8085ca 100644 --- a/tools/production/app/app.py +++ b/tools/production/app/app.py @@ -59,8 +59,16 @@ class ServiceApp(App): """ После прошивки: - firmware_test + успех → PostFlashScreen (промпт смены BootMode) - - всё остальное (production/custom, либо ошибка) → сразу WaitingScreen + - всё остальное (production/custom, либо ошибка) → WaitingScreen + - target=None — маркер потери соединения (см. ConnectionWatcherMixin) + → WaitingScreen с явной причиной возврата """ + if event.target is None and not event.success: + self.switch_screen( + WaitingScreen(disconnect_reason="Соединение с платой потеряно") + ) + return + if event.success and event.target == FlashTarget.FIRMWARE_TEST: self.switch_screen(PostFlashScreen()) else: @@ -72,10 +80,10 @@ class ServiceApp(App): self.switch_screen(WaitingScreen()) @on(DiagScreen.DiagDone) - def _on_diag_done(self) -> None: + def _on_diag_done(self, event: DiagScreen.DiagDone) -> None: """После диагностики — отключиться, вернуться в Waiting.""" self._disconnect() - self.switch_screen(WaitingScreen()) + self.switch_screen(WaitingScreen(disconnect_reason=event.reason)) # ── Подключение к firmware_test ─────────────────────────────────────────── diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss index b967115..969a812 100644 --- a/tools/production/app/app.tcss +++ b/tools/production/app/app.tcss @@ -38,6 +38,19 @@ AppFrame { text-style: bold; } +#waiting-reason { + margin-top: 1; + padding: 1 2; + border: round $warning; + color: $warning; + text-style: bold; + content-align: center middle; +} + +#waiting-reason.hidden { + display: none; +} + #waiting-hint { margin-top: 2; color: $text-muted; diff --git a/tools/production/app/firmware_client.py b/tools/production/app/firmware_client.py index b872bf6..aa01df0 100644 --- a/tools/production/app/firmware_client.py +++ b/tools/production/app/firmware_client.py @@ -159,6 +159,12 @@ class FirmwareClient: """ Читать события до получения одного из stop_types или таймаута. Генератор — yield каждого полученного события. + + При таймауте yield-ит синтетическое событие + {"type": "_timeout", "timeout_s": ...} перед завершением — вызывающий + код (Orchestrator) должен явно обработать этот тип и не путать его + с обрывом связи без объяснения причины. Префикс "_" отличает это + от реальных событий протокола firmware_test. """ loop = asyncio.get_running_loop() deadline = loop.time() + timeout_s @@ -171,6 +177,7 @@ class FirmwareClient: if event.get("type") in stop_types: return logger.warning("_recv_until timeout after %.1f s", timeout_s) + yield {"type": "_timeout", "timeout_s": timeout_s} # ── Public API ────────────────────────────────────────────────────────── diff --git a/tools/production/app/orchestrator.py b/tools/production/app/orchestrator.py index bb18905..95a1d06 100644 --- a/tools/production/app/orchestrator.py +++ b/tools/production/app/orchestrator.py @@ -28,6 +28,11 @@ orchestrator.py — оркестратор confirm_request для TUI. "btn*" → buttons (нет confirm, ждём test_result) всё остальное → operator (показать prompt) +Помимо confirm_request, протокол v2 определяет "progress" — внутришаговые +информационные события долгих тестов (сейчас только usd: card_detect, +mount, write, read_compare — см. docs/testing/PROTOCOL.md). Эти события +не требуют ответа, только отображаются как текущая фаза прогресса. + Публичный API: Orchestrator.run_tests(test_ids) — запустить тесты, yield OrchestratorEvent """ @@ -67,6 +72,9 @@ _CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF] _CAN_TX_ID = 0x200 _CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE] +# Тип события от FirmwareClient._recv_until() сигнализирующий таймаут чтения +_TIMEOUT_EVENT_TYPE = "_timeout" + # ── Типы событий оркестратора ──────────────────────────────────────────── @@ -74,11 +82,12 @@ _CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE] class OrchestratorEventType(Enum): TEST_BEGIN = auto() # тест начался TEST_RESULT = auto() # тест завершился + TEST_PROGRESS = auto() # внутришаговый прогресс долгого теста (usd и т.п.) CONFIRM_NEEDED = auto() # нужен ответ оператора (standalone) CONFIRM_RESOLVED = auto() # HIL confirm выполнен автоматически BUTTONS_PROMPT = auto() # показать инструкцию для buttons (без confirm) SUMMARY = auto() # итог всей сессии - ERROR = auto() # ошибка протокола или M5 + ERROR = auto() # ошибка протокола, M5, или обрыв по таймауту @dataclass @@ -137,14 +146,24 @@ class Orchestrator: Yields OrchestratorEvent в порядке поступления событий от firmware_test. Блокируется на CONFIRM_NEEDED до вызова resolve_operator_confirm(). + + Гарантия для вызывающего кода: генератор ВСЕГДА заканчивается событием + SUMMARY — либо настоящим (от firmware), либо синтетическим при обрыве + потока (таймаут чтения порта истёк раньше, чем пришёл summary). + Без этой гарантии TUI не может надёжно понять, что прогон завершился + (именно это вызывало зависание кнопок при таймауте — см. отчёт). """ + current_test_id = "" + got_summary = False + async for raw in self._fw.run_selected(test_ids): event_type = raw.get("type", "") if event_type == "test_begin": + current_test_id = raw.get("id", "") yield OrchestratorEvent( type=OrchestratorEventType.TEST_BEGIN, - test_id=raw.get("id", ""), + test_id=current_test_id, test_name=raw.get("name", ""), ) @@ -161,12 +180,24 @@ class Orchestrator: duration_ms=raw.get("ms", 0), detail=raw.get("detail", ""), ) + current_test_id = "" yield OrchestratorEvent( type=OrchestratorEventType.TEST_RESULT, test_id=result.id, result=result, ) + elif event_type == "progress": + # Внутришаговый прогресс долгого теста (сейчас только usd). + # Не ошибка — информационное событие, не требует ответа. + step = raw.get("step", "") + pstat = raw.get("status", "") + yield OrchestratorEvent( + type=OrchestratorEventType.TEST_PROGRESS, + test_id=raw.get("test", current_test_id), + message=f"{step}: {pstat}" if step else "progress", + ) + elif event_type == "confirm_request": confirm = ConfirmRequest( id=raw.get("id", ""), @@ -177,16 +208,57 @@ class Orchestrator: yield ev elif event_type == "summary": + got_summary = True yield OrchestratorEvent( type=OrchestratorEventType.SUMMARY, summary=raw, ) - else: + elif event_type == _TIMEOUT_EVENT_TYPE: + # Чтение порта оборвалось по таймауту раньше, чем пришёл + # summary. Завершаем текущий тест (если он был в RUNNING) + # синтетическим FAIL, чтобы таблица результатов не осталась + # с тестом, навечно подвисшим в "выполняется". + timeout_s = raw.get("timeout_s", 0) + logger.error( + "Test run aborted: read timeout after %.0fs, current_test=%r", + timeout_s, + current_test_id, + ) + if current_test_id: + yield OrchestratorEvent( + type=OrchestratorEventType.TEST_RESULT, + test_id=current_test_id, + result=TestResult( + id=current_test_id, + status=TestStatus.FAIL, + duration_ms=0, + detail=f"таймаут связи ({timeout_s:.0f}с)", + ), + ) yield OrchestratorEvent( type=OrchestratorEventType.ERROR, - message=raw.get("error", f"unknown event: {event_type}"), + message=f"Связь с платой прервана (таймаут {timeout_s:.0f}с)", ) + # break, не return — нужно дойти до synthetic summary ниже + break + + else: + # Неизвестный, но не критичный тип события — логируем для + # разработчика, не показываем оператору как ошибку (см. + # отчёт замечание №2: ранее "progress" ошибочно считался + # неизвестным событием до того как протокол был сверен). + logger.debug("Unhandled protocol event: %r", raw) + + if not got_summary: + # Поток событий оборвался (таймаут или обрыв соединения) без + # настоящего summary от firmware — синтезируем его, чтобы + # вызывающий код (DiagScreen._run_worker) гарантированно вышел + # из ожидания и разблокировал кнопки/чекбоксы. + yield OrchestratorEvent( + type=OrchestratorEventType.SUMMARY, + summary={"overall": "fail", "passed": 0, "failed": 0, "aborted": True}, + ) # ── Маршрутизация confirm ──────────────────────────────────────────── diff --git a/tools/production/app/screens/connection_watcher.py b/tools/production/app/screens/connection_watcher.py new file mode 100644 index 0000000..742fcfb --- /dev/null +++ b/tools/production/app/screens/connection_watcher.py @@ -0,0 +1,78 @@ +""" +connection_watcher.py — мониторинг USB-соединения для экранов FlashScreen +и DiagScreen. + +Периодически (каждые _WATCH_INTERVAL_S) проверяет, виден ли таргет ещё +на шине USB. При потере соединения — экран обязан немедленно прекратить +любое взаимодействие (кроме кнопки "Выйти") и вернуться на WaitingScreen. +Решение архитектурно: при разрыве сессия считается недостоверной и +не восстанавливается — экран не пытается определить "вернулась ли та же +самая плата", просто стартует заново с нуля. + +Используется как миксин: класс экрана наследует ConnectionWatcherMixin +вторым родителем после Screen, вызывает self._start_connection_watch(check_fn) +в on_mount(), и переопределяет _on_connection_lost() для специфичной +очистки экрана перед уходом на WaitingScreen. +""" + +from __future__ import annotations + +from typing import Callable + +from textual.message import Message +from textual.timer import Timer + +_WATCH_INTERVAL_S = 1.5 + + +class ConnectionLost(Message): + """ + Соединение с платой потеряно во время нахождения на экране. + Экран должен прекратить взаимодействие и вернуться на WaitingScreen. + """ + + +class ConnectionWatcherMixin: + """ + Миксин периодической проверки USB-соединения. + + Использование в экране:: + + class FlashScreen(Screen, ConnectionWatcherMixin): + def on_mount(self) -> None: + self._start_connection_watch(Flasher.detect_sdp) + + def on_unmount(self) -> None: + self._stop_connection_watch() + """ + + _connection_watch_timer: Timer | None = None + _connection_lost: bool = False + + def _start_connection_watch(self, check_fn: Callable[[], bool]) -> None: + """ + Запустить периодическую проверку. check_fn должна вернуть True + пока устройство видно на шине (Flasher.detect_sdp / detect_cdc). + """ + self._connection_lost = False + self._connection_check_fn = check_fn + self._connection_watch_timer = self.set_interval( # type: ignore[attr-defined] + _WATCH_INTERVAL_S, self._check_connection + ) + + def _stop_connection_watch(self) -> None: + if self._connection_watch_timer is not None: + self._connection_watch_timer.stop() + self._connection_watch_timer = None + + def _check_connection(self) -> None: + if self._connection_lost: + return + try: + still_present = self._connection_check_fn() + except Exception: + still_present = False + if not still_present: + self._connection_lost = True + self._stop_connection_watch() + self.post_message(ConnectionLost()) # type: ignore[attr-defined] diff --git a/tools/production/app/screens/diag/__init__.py b/tools/production/app/screens/diag/__init__.py index b75f691..27cc051 100644 --- a/tools/production/app/screens/diag/__init__.py +++ b/tools/production/app/screens/diag/__init__.py @@ -7,6 +7,13 @@ DiagScreen координирует три виджета: ConfirmPanel — confirm_request оператора (нижняя панель) и Orchestrator — маршрутизатор confirm_request. + +Мониторинг соединения (см. отчёт, замечание №4): пока тесты не запущены, +каждые 1.5с проверяется наличие CDC-порта на шине. Во время активного +прогона мониторинг приостановлен — таймаут чтения порта внутри Orchestrator +уже детектирует обрыв связи надёжнее (видит реальную остановку потока +данных, а не просто исчезновение устройства из списка портов) и сам +формирует понятный результат для прерванного теста. """ from __future__ import annotations @@ -24,10 +31,12 @@ from textual.screen import Screen from textual.widgets import Button, Label, ProgressBar, Static from ...firmware_client import FirmwareClient +from ...flasher import Flasher from ...m5_client import M5Client from ...models import SessionState from ...orchestrator import Orchestrator, OrchestratorEvent, OrchestratorEventType from ...widgets import AppFrame +from ..connection_watcher import ConnectionLost, ConnectionWatcherMixin from .confirm_panel import ConfirmPanel from .results import ResultsPanel from .test_list import TestListPanel @@ -35,7 +44,7 @@ from .test_list import TestListPanel logger = logging.getLogger(__name__) -class DiagScreen(Screen): +class DiagScreen(Screen, ConnectionWatcherMixin): """ Экран диагностики. @@ -52,6 +61,10 @@ class DiagScreen(Screen): class DiagDone(Message): """Сессия диагностики завершена.""" + def __init__(self, reason: Optional[str] = None) -> None: + super().__init__() + self.reason = reason + def __init__( self, firmware: FirmwareClient, @@ -70,9 +83,9 @@ class DiagScreen(Screen): with AppFrame(id="diag-frame"): # Шапка — фиксированная высота 3 with Horizontal(id="diag-header"): - yield Static("firmware_test: —\nFw: —", id="diag-header-fw") + yield Static("fw: —", id="diag-header-fw") yield Static("MCU ID: —", id="diag-header-uid") - yield Static("M5: —", id="diag-header-m5") + yield Static("M5 Bench: —", id="diag-header-m5") # Рабочая зона: список тестов + результаты — занимает всё # оставшееся место (1fr), сама прокручивается при переполнении @@ -84,30 +97,50 @@ class DiagScreen(Screen): # имеет classes="hidden" по умолчанию — height: auto + display: # none даёт нулевую высоту, не отнимая место у остального layout. with Vertical(id="diag-progress-row", classes="hidden"): - yield ProgressBar(id="diag-progress-bar", show_eta=False) + yield ProgressBar( + id="diag-progress-bar", show_eta=False, show_percentage=False + ) yield Label("", id="diag-progress-label") # Кнопки запуска — фиксированная высота 3 (под border Button) with Horizontal(id="diag-btn-row"): yield Button( - "▶ Запустить выбранные", + "▶ Запустить выбранные тесты", id="diag-btn-run-selected", variant="primary", disabled=True, ) yield Button( - "▶▶ Все тесты", + "▶▶ Запустить все тесты", id="diag-btn-run-all", variant="default", disabled=True, ) - yield Button("✕ Выйти", id="diag-btn-quit", variant="default") + yield Button( + "✕ Выйти из приложения", id="diag-btn-quit", variant="default" + ) # Панель confirm — auto-высота, видна только когда есть запрос yield ConfirmPanel(id="diag-confirm", classes="hidden") def on_mount(self) -> None: self._init_session() + self._start_connection_watch(self._check_cdc_present) + + def on_unmount(self) -> None: + self._stop_connection_watch() + + def _check_cdc_present(self) -> bool: + # Не считаем потерей соединения во время активного прогона — + # Orchestrator сам детектирует обрыв через таймаут чтения порта + # (надёжнее: видит остановку потока данных, не просто список USB). + if self._tests_running: + return True + return Flasher.detect_cdc() + + @on(ConnectionLost) + def _on_connection_lost(self) -> None: + self.post_message(self.DiagDone(reason="Соединение с платой потеряно")) # ── Инициализация сессии ───────────────────────────────────────────────── @@ -118,7 +151,7 @@ class DiagScreen(Screen): uid = await self._fw.get_uid() except Exception as exc: logger.error("Session init failed: %s", exc) - self.post_message(self.DiagDone()) + self.post_message(self.DiagDone(reason="Не удалось получить список тестов")) return self._session.tests = tests @@ -135,17 +168,17 @@ class DiagScreen(Screen): def _update_header(self) -> None: self.query_one("#diag-header-fw", Static).update( - f"firmware_test: {self._session.fw_version or '?'}" + f"fw: {self._session.fw_version or '?'}" ) uid = self._session.chip_uid or "—" self.query_one("#diag-header-uid", Static).update(f"MCU ID: {uid}") m5_widget = self.query_one("#diag-header-m5", Static) if self._session.m5_connected: - m5_widget.update("M5: ✓ подключён") + m5_widget.update("M5 Bench: ✓ подключён") m5_widget.remove_class("m5-absent") else: - m5_widget.update("M5: ✕ не подключен") + m5_widget.update("M5 Bench: ✕ нет связи") m5_widget.add_class("m5-absent") # ── Кнопки ─────────────────────────────────────────────────────────────── @@ -234,6 +267,13 @@ class DiagScreen(Screen): self._session.set_result(event.result) results.set_result(event.result) + elif event.type == OrchestratorEventType.TEST_PROGRESS: + # Внутришаговый прогресс долгого теста (сейчас только usd): + # card_detect, mount, write, read_compare — см. PROTOCOL.md. + self._update_progress( + done, total, f"Тест: {event.test_id} — {event.message}" + ) + elif event.type == OrchestratorEventType.CONFIRM_NEEDED: assert event.confirm is not None confirm.show_operator( @@ -242,7 +282,7 @@ class DiagScreen(Screen): ) elif event.type == OrchestratorEventType.CONFIRM_RESOLVED: - self._update_progress(done, total, f"HIL: {event.message}") + self._update_progress(done, total, f"M5 Bench: {event.message}") elif event.type == OrchestratorEventType.BUTTONS_PROMPT: assert event.confirm is not None @@ -255,6 +295,13 @@ class DiagScreen(Screen): self._update_progress(done, total, f"⚠ {event.message}") def _on_summary(self, summary: dict) -> None: + aborted = summary.get("aborted", False) + if aborted: + self.query_one("#diag-progress-label", Label).update( + "⚠ Прогон прерван: связь с платой потеряна" + ) + return + overall = summary.get("overall", "fail") passed = summary.get("passed", 0) failed = summary.get("failed", 0) diff --git a/tools/production/app/screens/diag/results.py b/tools/production/app/screens/diag/results.py index 5879bd3..fadeecd 100644 --- a/tools/production/app/screens/diag/results.py +++ b/tools/production/app/screens/diag/results.py @@ -8,6 +8,10 @@ DataTable с колонками: Тест | HIL | Статус | Время | Д внутри групп статусов — исходный порядок реестра firmware_test. Строка с FAIL дополнительно подсвечивается красным фоном целиком. +Колонка "Детали" переносит длинный текст на несколько строк внутри ячейки +(а не обрезает) — высота строки для FAIL вычисляется по длине detail +относительно ширины колонки. Остальные статусы всегда однострочные. + Публичный API: ResultsPanel.populate(tests) — показать пустую таблицу (PENDING) ResultsPanel.set_running(test_id) — пометить тест как выполняющийся @@ -17,6 +21,8 @@ DataTable с колонками: Тест | HIL | Статус | Время | Д from __future__ import annotations +import math + from rich.style import Style from rich.text import Text from textual.app import ComposeResult @@ -47,6 +53,12 @@ _STATUS_DISPLAY: dict[TestStatus, tuple[str, str]] = { _FAIL_BG = "dark_red" +# Ширина колонки "Детали" в символах — должна совпадать с шириной, +# заданной явно в on_mount() через table.add_column("Детали", width=...). +# Используется для расчёта высоты строки под перенос текста. +_DETAIL_COL_WIDTH = 21 +_MAX_ROW_HEIGHT = 4 # не даём одной FAIL-строке занять весь экран + class ResultsPanel(Widget): """Правая колонка DiagScreen — таблица результатов тестов.""" @@ -70,7 +82,16 @@ class ResultsPanel(Widget): def on_mount(self) -> None: table = self.query_one("#results-table", DataTable) - table.add_columns("Тест", "M5", "Статус", "Время", "Детали") + # add_columns() (множественное число) не принимает width — без явной + # ширины колонка "Детали" сжимается до длины заголовка ("Детали" = 6 + # символов) и обрезает любой более длинный текст, даже однострочный. + # Используем add_column() по одной с явной шириной под каждую, + # рассчитанной под ResultsPanel { width: 70 } (см. app.tcss). + table.add_column("Тест", width=22) + table.add_column("M5 Bench", width=8) + table.add_column("Статус", width=8) + table.add_column("Время", width=6) + table.add_column("Детали", width=21) # ── Public API ──────────────────────────────────────────────────────────── @@ -124,12 +145,25 @@ class ResultsPanel(Widget): # ── Internal: строки таблицы ──────────────────────────────────────────── + def _row_height(self, detail: str) -> int: + """ + Сколько строк нужно ячейке "Детали" под перенос текста. + 1 строка по умолчанию; растёт пропорционально длине detail, + ограничено _MAX_ROW_HEIGHT чтобы один FAIL не съел весь экран + (очень длинный detail в этом случае обрежется — лучше, чем + одна строка съедает половину видимой таблицы). + """ + if not detail: + return 1 + needed = math.ceil(len(detail) / _DETAIL_COL_WIDTH) + return max(1, min(needed, _MAX_ROW_HEIGHT)) + def _add_row( self, test: TestInfo, status: TestStatus, duration_ms: int, detail: str ) -> None: table = self.query_one("#results-table", DataTable) cells = self._row_cells(test, status, duration_ms, detail) - table.add_row(*cells, key=test.id) + table.add_row(*cells, key=test.id, height=self._row_height(detail)) def _update_row( self, test_id: str, status: TestStatus, duration_ms: int, detail: str @@ -141,16 +175,28 @@ class ResultsPanel(Widget): table = self.query_one("#results-table", DataTable) cells = self._row_cells(test, status, duration_ms, detail) - col_keys = [c.key for c in table.ordered_columns] - for col_key, value in zip(col_keys, cells): - table.update_cell(test_id, col_key, value) + new_height = self._row_height(detail) + + # DataTable не предоставляет публичный API для изменения высоты + # уже добавленной строки (update_cell меняет только содержимое). + # Когда нужная высота отличается от текущей (например, тест перешёл + # в FAIL с многострочным detail) — пересоздаём строку: remove + add. + # Иначе — точечный update_cell, дешевле и не теряет курсор/scroll. + current_height = table.get_row_height(test_id) + if current_height != new_height: + table.remove_row(test_id) + table.add_row(*cells, key=test_id, height=new_height) + else: + col_keys = [c.key for c in table.ordered_columns] + for col_key, value in zip(col_keys, cells): + table.update_cell(test_id, col_key, value) self._resort(table) def _row_cells( self, test: TestInfo, status: TestStatus, duration_ms: int, detail: str ) -> tuple: - """Собрать пять ячеек строки с учётом подсветки FAIL.""" + """Собрать пять ячеек строки с учётом подсветки FAIL и переноса текста.""" status_text, status_color = _STATUS_DISPLAY[status] is_fail = status == TestStatus.FAIL @@ -166,12 +212,17 @@ class ResultsPanel(Widget): time_text = f"{duration_ms / 1000:.1f}с" if duration_ms > 0 else "" hil_text = "[*]" if test.requires_hil else "[-]" + # overflow="fold" — перенос по символам на границе ячейки вместо + # обрезания с многоточием (Textual default), detail виден целиком + # на нескольких строках, если высота строки это позволяет. + detail_cell = Text(detail, style=detail_style, overflow="fold") + return ( Text(test.name, style=name_style), Text(hil_text, style=hil_style), Text(status_text, style=status_style), Text(time_text, style=time_style), - Text(detail, style=detail_style), + detail_cell, ) def _resort(self, table: DataTable) -> None: diff --git a/tools/production/app/screens/diag/test_list.py b/tools/production/app/screens/diag/test_list.py index f3dfd49..068f8f9 100644 --- a/tools/production/app/screens/diag/test_list.py +++ b/tools/production/app/screens/diag/test_list.py @@ -32,7 +32,7 @@ class TestListPanel(Widget): self._hil_unavailable: dict[str, bool] = {} def compose(self) -> ComposeResult: - yield Label("Тесты", classes="section-title") + yield Label("Доступные тесты", classes="section-title") with Horizontal(id="test-list-select-row"): yield Button( "Выбрать все", id="test-list-select-all", classes="-textual-compact" diff --git a/tools/production/app/screens/flash.py b/tools/production/app/screens/flash.py index c5b0e92..96c3782 100644 --- a/tools/production/app/screens/flash.py +++ b/tools/production/app/screens/flash.py @@ -31,16 +31,24 @@ from textual.widgets import ( from ..flasher import Flasher from ..models import FlashProgress, FlashTarget from ..widgets import AppFrame +from .connection_watcher import ConnectionLost, ConnectionWatcherMixin logger = logging.getLogger(__name__) -class FlashScreen(Screen): +class FlashScreen(Screen, ConnectionWatcherMixin): """ Экран прошивки. Messages: FlashDone(success, target) — прошивка завершена + + Мониторинг соединения: пока плата не прошивается (self._flashing + == False), каждые 1.5с проверяется наличие BootROM SDP на шине. + Если плата физически отключена в простое — сессия считается + недостоверной, экран сразу уходит на WaitingScreen (см. замечание + №4 отчёта). Во время самой прошивки/erase мониторинг приостановлен — + обрыв в этом случае обнаружит и обработает сам flash_usb.py subprocess. """ BINDINGS = [ @@ -61,12 +69,12 @@ class FlashScreen(Screen): def compose(self) -> ComposeResult: with AppFrame(id="flash-frame"): yield Label( - "⚡ Загрузка прошивки на плату (BootROM SDP обнаружен)", + "⚡ Загрузка прошивки на плату индикатора (режим BootROM)", id="flash-title", ) with Vertical(id="flash-target-group"): - yield Label("Выберите файл для загрузки", classes="section-title") + yield Label("Выбор загружаемой прошивки", classes="section-title") with RadioSet(id="flash-radio"): yield RadioButton( "Диагностическая прошивка (firmware_test)", @@ -78,25 +86,47 @@ class FlashScreen(Screen): id="radio-production", ) yield RadioButton( - "Другое (подготовленный бинарный файл)", + "Другое", id="radio-custom", ) with Horizontal(id="flash-custom-path", classes="hidden"): yield Input( - placeholder="Путь к бинарному файлу (.bin)", + placeholder="Имя бинарного файла в custom_binaries/ (.bin)", id="flash-custom-input", ) with Horizontal(id="flash-btn-row"): - yield Button("▶ Загрузить ПО", id="flash-btn-flash", variant="warning") + yield Button("▶ Загрузить", id="flash-btn-flash", variant="warning") yield Button("⚠ Очистить память", id="flash-btn-erase", variant="error") yield Button( - "✕ Выйти из программы", id="flash-btn-quit", variant="default" + "✕ Выйти из приложения", id="flash-btn-quit", variant="default" ) - yield ProgressBar(id="flash-progress-bar", show_eta=False, classes="hidden") + yield ProgressBar( + id="flash-progress-bar", + show_eta=False, + show_percentage=False, + classes="hidden", + ) yield Log(id="flash-log", auto_scroll=True) + def on_mount(self) -> None: + self._start_connection_watch(self._check_sdp_present) + + def on_unmount(self) -> None: + self._stop_connection_watch() + + def _check_sdp_present(self) -> bool: + # Не считаем потерей соединения, если идёт активная операция — + # flash_usb.py сам обработает реальный обрыв через subprocess. + if self._flashing: + return True + return Flasher.detect_sdp() + + @on(ConnectionLost) + def _on_connection_lost(self) -> None: + self.post_message(self.FlashDone(success=False, target=None)) + # ── Обработчики ─────────────────────────────────────────────────────────── @on(RadioSet.Changed, "#flash-radio") @@ -114,7 +144,7 @@ class FlashScreen(Screen): return target, bin_path = self._resolve_target() if target is None: - self._log("⚠ Укажите путь к бинарному файлу!") + self._log("⚠ Укажите корректное имя бинарного файла") return self._do_flash(target, bin_path) @@ -153,15 +183,11 @@ class FlashScreen(Screen): async def _do_erase(self) -> None: self._set_busy(True) self._show_progress(True) - self._log("⚠ Очистка флеш памяти (~30 с)...") + self._log("⚠ Очистка памяти (~30 с)...") ok = await self._flasher.erase_chip(progress_cb=self._on_progress) self._set_busy(False) self._finish_progress(ok) - self._log( - "✅ Очистка флеш памяти завершена" - if ok - else "❌ Очистка флеш памяти: ошибка" - ) + self._log("✅ Очистка памяти завершена" if ok else "❌ Очистка памяти: ошибка") # ── Вспомогательные ─────────────────────────────────────────────────────── diff --git a/tools/production/app/screens/post_flash.py b/tools/production/app/screens/post_flash.py index 891ba3e..aaf103d 100644 --- a/tools/production/app/screens/post_flash.py +++ b/tools/production/app/screens/post_flash.py @@ -60,7 +60,9 @@ class PostFlashScreen(Screen): with Horizontal(id="post-flash-btn-row"): yield Button("✓ Готово", id="post-flash-btn-ok", variant="success") - yield Button("✕ Выйти", id="post-flash-btn-quit", variant="default") + yield Button( + "✕ Выйти из приложения", id="post-flash-btn-quit", variant="default" + ) def on_mount(self) -> None: self._update_countdown() diff --git a/tools/production/app/screens/waiting.py b/tools/production/app/screens/waiting.py index fbd9c42..12818c4 100644 --- a/tools/production/app/screens/waiting.py +++ b/tools/production/app/screens/waiting.py @@ -3,10 +3,19 @@ waiting.py — экран ожидания подключения платы. Опрашивает USB каждые _DETECT_INTERVAL_S секунд через Flasher. При обнаружении SDP или CDC отправляет DeviceDetected message в App. + +Принимает опциональный disconnect_reason — короткое сообщение о причине +возврата на этот экран (например, "Соединение с платой потеряно"), +показывается несколько секунд поверх обычной подсказки, затем исчезает +само. Нужно, чтобы потеря USB во время прошивки/диагностики (см. отчёт, +замечание №4) не выглядела как необъяснимый скачок экрана — сервисник +должен понимать, что это осознанное поведение, а не баг. """ from __future__ import annotations +from typing import Optional + from textual.app import ComposeResult from textual.css.query import NoMatches from textual.message import Message @@ -20,6 +29,7 @@ from ..widgets import AppFrame _DETECT_INTERVAL_S = 1.5 _SPIN_INTERVAL_S = 0.1 +_REASON_DISPLAY_S = 4.0 _SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] @@ -38,27 +48,46 @@ class WaitingScreen(Screen): super().__init__() self.mode = mode - def __init__(self, **kwargs) -> None: + def __init__(self, disconnect_reason: Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) + self._disconnect_reason = disconnect_reason self._spinner_idx: int = 0 self._detect_timer: Timer | None = None self._spin_timer: Timer | None = None + self._reason_timer: Timer | None = None def compose(self) -> ComposeResult: with AppFrame(id="waiting-frame"): yield Static("TFT Indicator Board\nService Tool", id="waiting-logo") - yield Static("Подключите плату к USB...", id="waiting-hint") + yield Static("", id="waiting-reason", classes="hidden") + yield Static("Подключите плату индикатора к USB...", id="waiting-hint") yield Static(_SPINNER_FRAMES[0], id="waiting-spinner") def on_mount(self) -> None: self._detect_timer = self.set_interval(_DETECT_INTERVAL_S, self._poll_usb) self._spin_timer = self.set_interval(_SPIN_INTERVAL_S, self._spin) + if self._disconnect_reason: + self._show_reason(self._disconnect_reason) def on_unmount(self) -> None: self._stop_timers() # ── Internal ────────────────────────────────────────────────────────────── + def _show_reason(self, reason: str) -> None: + try: + self.query_one("#waiting-reason", Static).update(f"⚠ {reason}") + self.query_one("#waiting-reason").remove_class("hidden") + except NoMatches: + return + self._reason_timer = self.set_timer(_REASON_DISPLAY_S, self._hide_reason) + + def _hide_reason(self) -> None: + try: + self.query_one("#waiting-reason").add_class("hidden") + except NoMatches: + pass + def _spin(self) -> None: self._spinner_idx = (self._spinner_idx + 1) % len(_SPINNER_FRAMES) try: @@ -81,3 +110,5 @@ class WaitingScreen(Screen): self._detect_timer.stop() if self._spin_timer: self._spin_timer.stop() + if self._reason_timer: + self._reason_timer.stop() diff --git a/tools/production/docs/INITIAL_PLAN.md b/tools/production/docs/INITIAL_PLAN.md new file mode 100644 index 0000000..7fa9d46 --- /dev/null +++ b/tools/production/docs/INITIAL_PLAN.md @@ -0,0 +1,488 @@ +# firmware_test — План разработки + +> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified). + +--- + +## Контекст проекта + +**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации). +Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика. + +**Стенд:** + +- Хост подключается через USB CDC ACM — единственный канал firmware_test +- HIL-тесты управляются через M5StampPLC (опционально) +- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно + +--- + +## Текущий статус + +| Компонент | Статус | Примечание | +| ------------------------------ | ------ | ------------------------------------------------ | +| `bsp_usb_cdc` | ✅ | HIL тест пройден | +| firmware_test скелет | ✅ | `main.c` + `cli.c` | +| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | +| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | +| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | +| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | +| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | +| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | +| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified | +| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified | +| `test_opto` | ✅ | Этап 6б, hardware-verified | +| `test_can` | ✅ | Этап 6в, hardware-verified | +| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | +| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | +| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | +| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified | +| Provisioning | ⬜ | Этап 7 | +| TUI сервисного инженера | ⬜ | Этап 8 | + +--- + +## Матрица тестов — итоговая + +| ID | Название | Critical | HIL | Тип | BSP | Статус | +| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ | +| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ | +| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ | +| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ | +| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | +| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | +| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ | +| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | +| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | + +**Убранные тесты (закрытые решения):** + +- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется +- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно + +--- + +## Закрытые архитектурные решения + +> Не пересматривать без явного запроса. + +### Этапы 1–5 (ранее зафиксированные) + +- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test. +- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`. +- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует. +- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`. +- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7). +- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`. +- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL. +- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP. + +### Этап 6 (новые решения) + +- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL). + TUI фильтрует HIL-тесты если M5StampPLC не подключён. +- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста. + TUI строит UI динамически, не хардкодит список тестов. +- **`run_selected`:** запуск произвольного подмножества тестов по списку ID. + Порядок выполнения — как в реестре таргета, не как в запросе. + Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI. +- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request` + от HIL-теста TUI командует M5, получает результат, отправляет confirm. +- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты + недоступны (серые в UI, не входят в `run_selected`). +- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`. +- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен + один канал. Буфер всегда стерео (L+R идентичны). +- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая), + `confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL. + `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`. +- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`. + Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M. + Нарушение порядка приводит к щелчку при старте или отсутствию звука. +- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking), + параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с. +- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно + выставлять в `true`. При инициализации через designated initializers без явного + указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит + на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого + прогона). Воспроизводится только при cold reset. +- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на + `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate + может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` + перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым — + закрывать не нужно, LPUART1 тактируется с минимальным потреблением. + `bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`. +- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при + старте, может быть пропущено). Проверяет живость через `ping → pong`. +- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина + без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется + в `test_opto.c` после settle — обходит race condition когда чётное число ISR + при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`. +- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm, + но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`. +- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`. + Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`, + `bsp_opto_force_read()` читает финальное состояние пина напрямую. + +### Этап 8 (TUI решения) + +- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost). + Оператор сам переставляет перемычку BOOT — это ок, документируется. +- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130) + или CDC firmware_test (session_start) — и показывает соответствующий экран. +- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты, + работает в SSH-сессии, вписывается в uv-экосистему. +- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/` + и `tools/production/`. + +--- + +## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН + +### 6а — Расширение протокола ✅ + +**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md` + +#### Новая команда `list_tests` + +```json +→ {"type":"cmd","cmd":"list_tests"} +← {"type":"test_list","tests":[ + {"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false}, + {"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false}, + {"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false}, + {"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false}, + {"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false}, + {"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false}, + {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true}, + {"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true} + ]} +``` + +#### Новая команда `run_selected` + +```json +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""} +← {"type":"test_begin","id":"qspi",...} +← {"type":"test_result","id":"qspi",...} +← {"type":"test_begin","id":"display",...} +← {"type":"test_result","id":"display",...} +← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"} +``` + +Если хотя бы один ID не найден в реестре: + +```json +← {"ok":false,"error":"UNKNOWN_TEST"} +``` + +**Реализация в `test_runner.c`:** + +- Новый режим `RUNNER_MODE_SELECTED` +- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc +- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция + +### 6б — test_opto.c ✅ + +**Файл:** `firmware/test/src/tests/test_opto.c` + +6 шагов, попарно ACTIVE/INACTIVE для трёх каналов: + +| Шаг | confirm_request id | M5 действие | Проверка | +| --- | ------------------- | ----------- | -------------------------------- | +| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` | +| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` | +| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` | +| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` | +| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` | +| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` | + +- Init: `bsp_opto_init()` единым вызовом для всех каналов +- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`) +- FAIL при несоответствии: `detail = " state mismatch: expected ACTIVE got INACTIVE"` +- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг + +### 6в — test_can.c ✅ + +**Файл:** `firmware/test/src/tests/test_can.c` + +2 шага, оба направления независимо: + +**Шаг 1 — RX (M5 → таргет):** + +```bash +confirm_request("can_rx_ready") +→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF]) +→ TUI: confirm(true) +→ таргет: bsp_can_receive(&frame, 500 мс) +→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF] +→ FAIL если timeout или несовпадение +``` + +**Шаг 2 — TX (таргет → M5):** + +```bash +bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс) +confirm_request("can_tx_verify") +→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data +→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет +→ FAIL если confirmed=false или timeout +``` + +- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует +- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()` + +### 6г — bsp_mqs + test_mqs.c ✅ + +**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c` + +**bsp_mqs:** + +- SAI3 + eDMA (DMA0 канал 0) + MQS периферия +- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT` +- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358 +- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`, + `bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume` + +**test_mqs:** + +- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида +- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле +- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL +- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг) +- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL` + +### 6д — HIL pytest для firmware_test ✅ + +**Файлы:** + +``` +tools/hil/conftest.py ← фикстура firmware_cdc +tools/hil/06_test_firmware_opto.py +tools/hil/06_test_firmware_can.py +``` + +**Фикстура `firmware_cdc`:** + +```python +@pytest.fixture(scope="module") +def firmware_cdc(m5): + """ + Открывает USB CDC порт firmware_test. + firmware_test уже прошит в Flash (не загружается pyOCD). + Проверяет живость через ping → pong. + """ +``` + +**`FirmwareCdcClient`** — тонкий клиент: + +- `send_cmd(cmd_dict)` — отправить JSON команду +- `wait_event(type, timeout_s)` — ждать события нужного типа +- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}` +- `run_test(id)` — запустить тест, вернуть test_result dict + +**Justfile:** + +```bash +hil-firmware-opto → pytest 06_test_firmware_opto.py -v +hil-firmware-can → pytest 06_test_firmware_can.py -v +``` + +--- + +## Этап 7 — Provisioning + +### Что нужно + +1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp` +2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary` +3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста +4. Записывать статус в Flash (первый сектор после прошивки, вне XIP) + +### BSP (предварительно) + +```c +/* bsp/provisioning/include/bsp/provisioning.h */ +bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */ +``` + +### Открытые вопросы — Этап 7 + +- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID? +- [ ] Нужна ли защита от повторного provisioning (write-once)? + +--- + +## Этап 8 — TUI сервисного инженера + +### Стек технологий + +| Компонент | Выбор | Обоснование | +| ------------- | ------------- | ----------------------------------------------------- | +| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема | +| Serial | pyserial | Уже в стеке (tools/hil) | +| Прошивка | spsdk | sdphost + blhost, уже в tools/host | +| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом | + +### Структура приложения + +```bash +tools/production/ +├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv +├── uv.lock +├── main.py ← точка входа +├── app/ +│ ├── tui.py ← Textual App, экраны, layout +│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test +│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py) +│ ├── flasher.py ← USB SDP обёртка над spsdk +│ ├── orchestrator.py ← confirm_request → M5 action → confirm response +│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses) +└── README.md + +tools/shared/ +└── m5_agent.py ← общая M5-логика для hil/ и production/ +``` + +### Два режима работы + +**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP) + +``` +┌─ Прошивка платы ─────────────────────────────────┐ +│ Обнаружен BootROM (SDP режим) │ +│ │ +│ Что прошить? │ +│ ◉ firmware_test (диагностика) │ +│ ○ Production (bootloader + tft_app) │ +│ │ +│ Файл: [/path/to/firmware_test_hab.bin ···] │ +│ │ +│ [ Прошить ] │ +│ │ +│ ████████████░░░░░░ 64% Запись во Flash... │ +└────────────────────────────────────────────────────┘ +``` + +**Режим B — Диагностика** (триггер: session_start получен по CDC) + +``` +┌─ Диагностика платы fw:0.1.0 ─────────────────────┐ +│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │ +├────────────────────────────────────────────────────┤ +│ Выбор тестов: │ Результаты: │ +│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ +│ ☑ QSPI Flash │ qspi ✓ PASS │ +│ ☑ microSD │ usd ✗ FAIL │ +│ ☑ TFT Display │ mount failed: 5 │ +│ ☑ Кнопки │ display ✓ PASS │ +│ ☑ MQS Audio │ buttons ✓ PASS │ +│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │ +│ ☑ Оптовходы [HIL] │ ... │ +├────────────────────────────────────────────────────┤ +│ [ Запустить выбранные ] [ Все тесты ] │ +│ ████████████████░░░░ 80% Тест: display │ +├────────────────────────────────────────────────────┤ +│ ⚠ Экран залит красным цветом? │ +│ [ ✓ Да ] [ ✗ Нет ] │ +└────────────────────────────────────────────────────┘ +``` + +### Поведение confirm_request в TUI + +| Тип теста | Источник confirm | Действие TUI | +| -------------------- | ------------------ | --------------------------------------------- | +| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown | +| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown | +| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result | +| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) | + +HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt. + +### Конфигурация (.env) + +```ini +# Существующие переменные (tools/hil/.env): +HIL_VCOM_PORT=/dev/ttyACM0 +HIL_M5_PORT=/dev/ttyACM1 + +# Новые переменные для production TUI: +SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start +SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5 +FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin +PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin +PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin +``` + +### Запуск + +```bash +just host::service-tui # запустить TUI сервисного инженера +just host::service-flash # прошить без TUI (для автоматизации) +``` + +### Процесс работы сервисника + +**Диагностика (firmware_test уже в Flash):** + +```bash +1. Плата в нормальном режиме (BOOT_MOD_1 → GND) +2. Подключить USB к сервисному ПК +3. just host::service-tui → TUI обнаружил session_start → Режим B +4. Выбрать тесты → Запустить → Смотреть результаты +``` + +**Перепрошивка (нужна новая версия firmware_test или production):** + +```bash +1. Перемычка BOOT_MOD_1 → 3V3 +2. Reset, подключить USB +3. TUI обнаружил 1FC9:0130 → Режим A +4. Выбрать бинарь → Прошить +5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B +``` + +--- + +## Порядок реализации + +``` +✅ Этап 1 протокол v2 + runner +✅ Этап 2 bsp_sdram + test_sdram +✅ Этап 3 bsp_qspi_flash + test_qspi +✅ Этап 4 bsp_sd + test_usd +✅ Этап 5 display + buttons +✅ Этап 6а протокол: list_tests + run_selected +✅ Этап 6б test_opto.c + hardware верификация +✅ Этап 6в test_can.c + hardware верификация +✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) +✅ Этап 6е HIL pytest: 06_test_firmware_opto.py +✅ Этап 6ж HIL pytest: 06_test_firmware_can.py +✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация + +⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ + +⬜ Этап 8а tools/production/ скелет + models + clients +⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты) +⬜ Этап 8в Экран прошивки (flasher + SDP автодетект) +⬜ Этап 8г Provisioning в TUI +⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики) + +⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру +``` + +--- + +## Зависимости между этапами + +``` +✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs) + ↓ + ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) + ↓ + ⬜ 7 (provisioning) + ↓ + ⬜ 8 (TUI) +``` \ No newline at end of file diff --git a/tools/production/docs/TUI_PLAN.md b/tools/production/docs/TUI_PLAN.md new file mode 100644 index 0000000..1d996b6 --- /dev/null +++ b/tools/production/docs/TUI_PLAN.md @@ -0,0 +1,7 @@ +# TUI Service Tool Plan + +1. Навести порядок с экранами "USB-POLL", "TRANSFER", возможно сделать bootlogo с версией программы и лого компании +2. Уменьшить размер активной области до предела и сделать его равным для всех экранов +3. Добавить обратный отчет в тесте buttons (таймаут) +4. Выгрузка результатов тестов в json файл с привязкой к UID микроконтроллера +5. Возможно ли копировать UID с экрана терминала? \ No newline at end of file diff --git a/tools/production/TUI_REPORT.md b/tools/production/docs/TUI_REPORT_1.md similarity index 100% rename from tools/production/TUI_REPORT.md rename to tools/production/docs/TUI_REPORT_1.md diff --git a/tools/production/docs/TUI_REPORT_2.md b/tools/production/docs/TUI_REPORT_2.md new file mode 100644 index 0000000..68dae8d --- /dev/null +++ b/tools/production/docs/TUI_REPORT_2.md @@ -0,0 +1,8 @@ +# 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.md new file mode 100644 index 0000000..a01ae9b --- /dev/null +++ b/tools/production/docs/TUI_SESSION.md @@ -0,0 +1,121 @@ +# Отчёт: разработка и стабилизация service-tui + +**Период:** один рабочий тред, от architecture-планирования до прод-готовности +**Объект:** `tools/production/` — TUI-приложение для сервисного инженера (диагностика и прошивка платы MIMXRT1052CVJ5B) + +--- + +## 1. Архитектура и первичная разработка (Этап 8) + +Построено с нуля на Python + Textual, монорепо `tools/production/`: + +``` +tools/production/ +├── main.py — точка входа (10 строк) +├── pyproject.toml — textual, pyserial, python-dotenv, pyinstaller, pyusb +└── app/ + ├── app.py — ServiceApp, роутинг экранов + ├── app.tcss — единый файл стилей + ├── models.py — AppMode, TestStatus, FlashTarget, TestInfo, TestResult, SessionState, ConfirmRequest, FlashProgress + ├── firmware_client.py — async USB CDC клиент firmware_test + ├── m5_client.py — async M5StampPLC клиент (HIL) + ├── flasher.py — subprocess-обёртка над tools/host/flash_usb.py + ├── orchestrator.py — маршрутизация confirm_request, обработка progress/timeout + ├── widgets/ + │ └── app_frame.py — общий адаптивный контейнер для всех экранов + └── screens/ + ├── waiting.py — WaitingScreen (USB autodetect) + ├── flash.py — FlashScreen (прошивка/chip erase) + ├── post_flash.py — PostFlashScreen (промпт смены BootMode) + ├── connection_watcher.py — миксин мониторинга обрыва USB + └── diag/ + ├── __init__.py — DiagScreen (координатор) + ├── test_list.py — TestListPanel (чекбоксы тестов) + ├── results.py — ResultsPanel (DataTable результатов) + └── confirm_panel.py — ConfirmPanel (prompt + countdown) +``` + +### Ключевые архитектурные решения +- **Оркестрация confirm_request** по `id`: HIL (opto/CAN) → автоматически через M5, `btn*` → инструкция без JSON-ответа, остальное → оператор с countdown +- **Flasher** не дублирует spsdk-окружение — вызывает `tools/host/flash_usb.py` как subprocess +- **Standalone-сборка** через PyInstaller для сервисников без Python +- **Три режима прошивки**: firmware_test / Production / кастомный бинарь + chip erase + +--- + +## 2. Расширения протокола и инструментария + +| Что | Где | +| ------------------------------------------------------------------------------ | --------------------------------------------------------- | +| `--bin-path`, `--erase-chip` в `flash_usb.py` | `tools/host/flash_usb.py` | +| `get_version` команда + CMake-версионирование (`version.h.in`) | firmware (`protocol.c/h`, `cli.c`, `CMakeLists.txt`) | +| `FIRMWARE_BUILD_TYPE` из `.env` (Debug по умолчанию — Release пока нестабилен) | `flasher.py` | +| Поддержка кириллицы в названиях тестов/промптах | `firmware_client.py`, `m5_client.py` (UTF-8 вместо ASCII) | + +--- + +## 3. Найденные и исправленные баги (хронологически) + +### Линкер и сборка +- **`${PROJECT_SOURCE_DIR}` → `${CMAKE_SOURCE_DIR}`** в `firmware/test/CMakeLists.txt` — после добавления `project(VERSION)` путь к линкер-скрипту стал резолвиться неверно. + +### USB-детект +- На macOS BootROM SDP (`1FC9:0130`) не создаёт serial-порт → невидим через `pyserial.list_ports`. Добавлен **pyusb** как primary метод детекта с fallback на `list_ports`. + +### Textual-специфичные баги +- `Screen.Message` не существует в Textual 8.x → заменено на `from textual.message import Message`. +- `MountError` в `populate()` — `row.mount(child)` до прикрепления `row` к DOM → исправлено передачей детей в конструктор + `mount_all()`. +- `CSS_PATH` на каждом экране резолвился относительно файла класса и постоянно расходился с реальным расположением `.tcss` → **унифицировано**: один `app.tcss` с `CSS_PATH` только на `ServiceApp`. +- **Краш при drag мыши** (`assert isinstance(content_widget.parent, Widget)`) — `Screen` выступал `content_widget` напрямую → решено введением общего `AppFrame`-контейнера между `Screen` и содержимым. +- **`self._running`** в `DiagScreen` случайно совпало с приватным полем `MessagePump._running` из самого Textual → кнопка "Выйти" была перманентно заблокирована. Переименовано в `_tests_running`. +- `DataTable.sort(*columns, key=fn)` передаёт в `key()` кортеж **значений ячеек**, не `row_key` — пришлось сортировать по содержимому ячейки "Статус", которое сами полностью контролируем. +- `table.add_columns()` (множественное число) не принимает `width=` → колонка "Детали" обрезалась по длине заголовка. Исправлено через `add_column()` по одной с явной шириной. + +### Layout +- `AppFrame` с жёстким `width: 140; height: 44` обрезал контент на терминалах меньшего размера → сделан **адаптивным** (`width/height: 100%` с потолком `max-width: 160; max-height: 50`). +- `#diag-frame` на `layout: grid` с ручным расчётом `grid-rows` рассинхронизировался с реальным числом/высотой children (кнопки с `border: tall` не помещались в выделенную строку) → переход на `layout: vertical` с единственным `1fr` на рабочую зону. +- `#results-empty.hidden` не имел CSS-правила `display: none` → пустой контейнер с `height: 1fr` продолжал выталкивать таблицу результатов вниз даже будучи скрытым. + +### Логика приложения +- **`TestListPanel.set_enabled()`** путал постоянное состояние "HIL без M5" с временной блокировкой на время прогона → чекбоксы навсегда залипали disabled после первого запуска. Исправлено отдельным словарём `_hil_unavailable`. +- **Таймаут чтения порта** (`_recv_until`) тихо завершался без сигнала → зависший тест навсегда оставался в RUNNING, кнопки "Выйти"/запуска блокировались навсегда. Теперь генератор **гарантированно** завершается одним `SUMMARY` (настоящим или синтетическим), зависший тест получает `FAIL` с понятным detail. +- **`progress`-событие** протокола (документированное в PROTOCOL.md, используется тестом USD) ошибочно считалось неизвестным/ошибочным → теперь явно обрабатывается как `TEST_PROGRESS`. + +### Firmware (USD-тест) +- **USD зависает намертво на втором прогоне.** Причина: non-blocking USDHC host driver SDK оставался в состоянии "ожидание завершения транзакции" после `SD_HostDeinit()`, плюс структура `g_sd` не обнулялась между прогонами. Фикс: `USDHC_Reset(..., kUSDHC_ResetAll, ...)` + `memset(&g_sd, 0, ...)` в `bsp_sd_init()`/`bsp_sd_deinit()`. + +--- + +## 4. UX-доработки (по согласованному плану итераций) + +| Итерация | Что сделано | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Стабильность | `AppFrame`, скрытие ProgressBar в простое, кнопка "Выйти" на всех экранах | +| Workflow | `PostFlashScreen` (промпт смены BootMode после прошивки firmware_test — только для этого сценария), тесты изначально не выбраны + кнопки "Выбрать все"/"Снять все", полный UID в шапке | +| Polish | `ResultsPanel` переведён на `DataTable`: сортировка FAIL-наверх (стабильная внутри группы), подсветка FAIL-строки целиком, перенос длинных `detail` на несколько строк без обрезания, empty-state с подсказкой | +| Отчёт №2 | Убраны проценты в статус-баре (полоса осталась), мониторинг обрыва USB (`ConnectionWatcherMixin`) на `FlashScreen`/`DiagScreen` с разрывом сессии и понятным баннером причины на `WaitingScreen` | + +--- + +## 5. Текущее состояние + +**Готово и протестировано (headless):** +- Полный цикл: WaitingScreen → Flash/Diag → результат → возврат +- Прошивка (3 варианта) + chip erase + версионирование +- Диагностика: список тестов, выборочный/полный запуск, HIL через M5, кириллица +- Обработка обрывов: таймаут теста, потеря USB, повторные прогоны +- Адаптивная вёрстка на диапазоне терминалов 80×24 → 220×60 + +**Известные открытые вопросы / не доделано:** +- Release-сборка firmware нестабильна (медленное мигание — подозрение на проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно форсирует Debug через `FIRMWARE_BUILD_TYPE` +- `docs/DEV_ARCH.md`, `CHANGELOG.md`, `PLAN.md` — подготовлены диффы для финализации документации, но не применялись по твоему решению ("пока не буду обновлять документацию, нужно всё проверить") +- `tools/shared/m5_agent.py` — сознательно не делался: pytest HIL-окружение и TUI используют независимые M5-клиенты, признано правильным архитектурным решением, а не техдолгом + +--- + +## 6. Рекомендации для следующего треда + +1. Перед стартом — синхронизировать единую копию репозитория со всеми патчами из этого треда (было замечено расхождение версий файлов между чатом и локальной копией один раз, см. эпизод с TUI_REPORT.md). +2. Дальнейшее тестирование на реальном железе: полный цикл diagnostics с HIL (M5 подключён), повторные циклы прошивки/chip erase, граничные случаи USB-отключения во время разных операций. +3. Когда стабильность подтверждена — вернуться к обновлению `DEV_ARCH.md`/`CHANGELOG.md`/`PLAN.md` под финальную архитектуру. +4. Разобрать Release-сборку firmware (сравнить `hab_firmware_test_debug.yaml` vs `hab_firmware_test_release.yaml`). \ No newline at end of file diff --git a/tools/production/docs/pics/FLASH.png b/tools/production/docs/pics/FLASH.png new file mode 100644 index 0000000000000000000000000000000000000000..19c5216f6c7037c9be6bbbe0f80c1b91d0758d02 GIT binary patch literal 56806 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%0ASlw_F^& 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 literal 0 HcmV?d00001 diff --git a/tools/production/docs/pics/POST_FLASH.png b/tools/production/docs/pics/POST_FLASH.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c5998a3cc2da0fc383527046d75f0432c9b83c GIT binary patch literal 45998 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 F{ttK2x+VYs literal 0 HcmV?d00001 diff --git a/tools/production/docs/pics/TEST_SCREEN.png b/tools/production/docs/pics/TEST_SCREEN.png new file mode 100644 index 0000000000000000000000000000000000000000..32c74c02c22b458aefaa1980fe63166a12b6d07e GIT binary patch literal 88874 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

jn0;@#0L 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 literal 0 HcmV?d00001 diff --git a/tools/production/docs/pics/WAITING.png b/tools/production/docs/pics/WAITING.png new file mode 100644 index 0000000000000000000000000000000000000000..f13b869a32eec1774c7e40a13b55c17f798306f2 GIT binary patch literal 35099 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 literal 0 HcmV?d00001 diff --git a/tools/production/docs/pics/WAITING_AFTER_LOSS.png b/tools/production/docs/pics/WAITING_AFTER_LOSS.png new file mode 100644 index 0000000000000000000000000000000000000000..52544add14705af095a5f5878c73927e5b5c616d GIT binary patch literal 39786 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 python -2026-06-29 18:36:18,046 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-29 18:36:18,224 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv -2026-06-29 18:36:18,224 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv -2026-06-29 18:36:18,344 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 93ms -2026-06-29 18:36:23,001 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:36:23,001 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 18:36:23,177 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:23,197 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 18:36:23,198 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 18:36:23,198 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:24,586 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 18:36:24,760 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 18:36:59,315 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 18:36:59,571 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 18:36:59,591 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: -2026-06-29 18:36:59,592 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-29 18:37:20,822 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-29 18:37:20,836 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 18:37:21,381 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:37:21,381 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 18:37:21,559 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:21,582 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 18:37:21,583 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 18:37:21,583 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:22,972 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 18:37:23,148 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:23,979 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:24,183 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:24,357 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:24,534 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 18:37:24,594 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:24,594 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 22616 (0x5858) -2026-06-29 18:37:24,779 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,798 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Размер: 22616 байт -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +28672 байт -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 28672 0 -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 18:37:24,799 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:24,800 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 18:37:46,867 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-29 18:37:46,883 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 18:37:47,377 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:37:47,377 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 18:37:47,548 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:47,568 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 18:37:47,569 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 18:37:47,569 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:48,972 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 18:37:49,154 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:49,987 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:50,206 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:50,386 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:50,560 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 18:37:50,621 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:50,621 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 22616 (0x5858) -2026-06-29 18:37:50,800 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,818 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Размер: 22616 байт -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +28672 байт -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 28672 0 -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,819 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:37:50,820 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 18:38:46,907 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:40:26,915 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:40:28,552 INFO app.m5_client M5StampPLC не найден -2026-06-29 18:41:26,213 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-29 18:44:38,127 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:44:39,837 INFO app.m5_client M5StampPLC не найден -2026-06-29 18:45:28,221 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-29 18:46:57,397 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:46:58,957 INFO app.m5_client M5StampPLC не найден -2026-06-29 18:50:48,270 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:51:01,877 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-29 18:51:01,900 DEBUG app.flasher flash_usb [firmware_test]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-29 18:51:02,032 DEBUG app.flasher flash_usb [firmware_test]: Removed virtual environment at: .venv -2026-06-29 18:51:02,032 DEBUG app.flasher flash_usb [firmware_test]: Creating virtual environment at: .venv -2026-06-29 18:51:02,130 DEBUG app.flasher flash_usb [firmware_test]: Installed 66 packages in 75ms -2026-06-29 18:51:06,727 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:51:06,727 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 18:51:06,903 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 18:51:06,923 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 18:51:06,924 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:08,331 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 18:51:08,506 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:09,215 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:09,429 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:09,621 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:09,804 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 18:51:09,985 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:09,985 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 72116 (0x119b4) -2026-06-29 18:51:10,173 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 18:51:10,191 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Размер: 72116 байт -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +77824 байт -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 77824 0 -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,192 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 18:51:10,193 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 18:51:36,969 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:57:30,698 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 18:57:32,409 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:00:37,411 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:00:38,953 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:04:04,441 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:04:12,903 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-29 19:04:12,918 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:04:12,919 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-29 19:04:12,921 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-29 19:04:13,106 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv -2026-06-29 19:04:13,106 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv -2026-06-29 19:04:13,225 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 93ms -2026-06-29 19:04:17,826 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:04:17,826 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:04:18,001 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:04:18,019 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:18,019 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:04:18,020 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:19,412 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:04:19,586 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:04:54,910 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:04:55,150 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,168 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-29 19:04:55,169 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:04:55,170 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-29 19:04:58,515 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug -2026-06-29 19:04:58,542 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:04:58,955 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:04:58,955 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:04:59,131 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:04:59,150 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:04:59,151 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:00,537 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:05:00,713 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:01,631 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:01,832 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:02,011 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:02,191 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 19:05:02,527 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:02,527 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) -2026-06-29 19:05:02,734 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:05:02,752 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:05:02,753 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:05:02,754 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 19:05:23,969 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:05:25,545 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:07:57,549 WARNING app.firmware_client _recv_until timeout after 120.0 s -2026-06-29 19:17:54,908 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:18:31,735 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-29 19:18:31,752 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:18:31,752 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-29 19:18:31,755 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-29 19:18:31,934 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv -2026-06-29 19:18:31,935 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv -2026-06-29 19:18:32,066 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 98ms -2026-06-29 19:18:36,600 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:18:36,600 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:18:36,776 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:18:36,796 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:18:36,797 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:18:38,238 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:18:38,414 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:19:13,145 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:19:13,403 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,422 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,423 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:19:13,424 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-29 19:20:01,708 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug -2026-06-29 19:20:01,724 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:20:02,300 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:20:02,300 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:20:02,473 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:20:02,493 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:03,887 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:20:04,068 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:04,968 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:05,182 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:05,355 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:05,534 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 19:20:05,873 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:05,873 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) -2026-06-29 19:20:06,062 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 19:20:06,081 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:20:06,082 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 19:20:59,572 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug -2026-06-29 19:20:59,595 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:21:00,152 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:21:00,152 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:21:00,330 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:21:00,350 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:21:00,351 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:01,734 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:21:01,904 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:02,832 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:03,049 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:03,224 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:03,447 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 19:21:03,786 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:03,787 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) -2026-06-29 19:21:04,008 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,028 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:21:04,029 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 19:21:20,423 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-29 19:21:20,437 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:21:20,936 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:21:20,936 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:21:21,112 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:21,133 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:21:21,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:21:21,134 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:22,510 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:21:22,685 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:21:57,783 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:21:58,044 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:58,064 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: -2026-06-29 19:21:58,065 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-29 19:31:03,319 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:31:12,383 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Debug -2026-06-29 19:31:12,399 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-29 19:31:12,928 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:31:12,928 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-29 19:31:13,096 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Debug] -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:13,117 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-29 19:31:13,118 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-29 19:31:13,118 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:14,509 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-29 19:31:14,679 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:15,624 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:15,830 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:16,009 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:16,183 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-29 19:31:16,518 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:16,518 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 135240 (0x21048) -2026-06-29 19:31:16,724 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Размер: 135240 байт -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +143360 байт -2026-06-29 19:31:16,744 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 143360 0 -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Debug/firmware_test_hab.bin 0 -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-29 19:31:16,745 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-29 19:31:45,595 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:31:54,684 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:31:57,741 WARNING app.firmware_client _recv_until timeout after 3.0 s -2026-06-29 19:34:42,219 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-29 19:36:35,663 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:36:37,208 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:36:40,255 WARNING app.firmware_client _recv_until timeout after 3.0 s -2026-06-29 19:36:47,180 DEBUG asyncio Using selector: KqueueSelector -2026-06-29 19:41:53,210 INFO app.m5_client M5StampPLC не найден -2026-06-29 19:41:56,267 WARNING app.firmware_client _recv_until timeout after 3.0 s -2026-06-30 11:07:04,658 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:07:32,037 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:07:38,901 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 11:07:38,923 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 11:07:38,923 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-30 11:07:38,925 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-30 11:07:39,117 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv -2026-06-30 11:07:39,117 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv -2026-06-30 11:07:39,247 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 100ms -2026-06-30 11:07:43,943 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:07:43,943 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 11:07:44,116 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 11:07:44,134 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 11:07:44,135 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:07:45,522 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 11:07:45,698 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:08:20,418 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:08:20,670 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,689 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:08:20,690 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 11:09:41,208 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 11:09:41,239 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 11:09:41,805 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:09:41,805 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 11:09:41,995 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 11:09:42,014 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:43,425 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 11:09:43,598 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:44,216 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:44,426 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:44,610 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:44,789 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 11:09:44,957 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:44,957 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 11:09:45,207 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,229 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:09:45,230 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:09:45,231 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 11:10:12,371 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:10:16,917 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:10:57,370 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 11:12:15,443 WARNING app.orchestrator Operator confirm timeout: display_red -2026-06-30 11:12:25,456 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 11:14:57,478 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:14:59,027 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:16:37,862 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:17:04,481 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 11:17:04,488 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 11:17:05,064 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:17:05,064 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 11:17:05,238 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:05,257 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 11:17:05,258 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:06,649 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 11:17:06,832 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:17:41,515 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:17:41,755 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,774 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: -2026-06-30 11:17:41,775 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 11:17:45,454 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 11:17:45,472 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 11:17:45,888 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:17:45,888 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 11:17:46,064 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 11:17:46,084 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:47,468 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 11:17:47,641 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:48,263 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:48,472 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:48,646 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:48,815 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 11:17:48,986 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:48,986 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 11:17:49,175 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 11:17:49,195 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 11:17:49,196 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 11:17:49,197 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 11:18:16,295 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:18:18,011 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:20:23,814 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 11:22:00,580 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:22:02,131 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:24:08,449 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:24:09,999 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:24:39,035 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:24:40,580 INFO app.m5_client M5StampPLC не найден -2026-06-30 11:25:23,410 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:25:58,243 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:26:30,720 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 11:26:58,255 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:12:49,218 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:12:57,749 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 12:12:57,771 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:12:58,350 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:12:58,350 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:12:58,522 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:12:58,543 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:12:59,916 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:13:00,092 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:13:08,189 DEBUG app.flasher flash_usb [erase]: Response status = 10004 (0x2714) No response packet from target device. -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:13:08,242 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:13:08,243 DEBUG app.flasher flash_usb [erase]: [ERROR] Команда завершилась с кодом 1 -2026-06-30 12:13:12,578 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:13:12,604 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:13:13,005 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:13:13,005 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:13:13,178 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:13:13,198 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:14,579 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:13:14,753 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:15,335 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:15,546 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:15,722 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:15,893 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:13:16,067 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:16,067 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:13:16,282 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:13:16,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,302 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:13:16,303 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:13:46,755 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:13:48,420 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:14:17,920 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured -2026-06-30 12:14:50,713 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:14:52,420 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:15:19,761 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:15:21,306 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:15:23,620 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:15:31,588 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:16:53,965 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:16:56,990 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 12:16:57,017 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:16:57,543 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:16:57,543 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:16:57,715 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:16:57,735 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:16:57,736 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:16:59,225 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:16:59,405 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:17:34,744 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:17:35,036 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,055 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:17:35,056 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:17:35,057 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 12:17:38,393 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:17:38,415 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:17:38,820 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:17:38,821 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:17:38,996 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:39,015 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:17:39,016 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:40,400 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:17:40,575 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:41,209 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:41,417 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:41,589 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:41,758 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:17:41,926 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:41,926 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:17:42,112 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,130 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:17:42,131 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:18:16,089 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:18:17,817 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:19:51,239 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 12:20:11,226 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:20:12,767 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:20:46,816 WARNING app.orchestrator Operator confirm timeout: mqs_tone -2026-06-30 12:23:08,021 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:23:09,570 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:24:19,465 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 12:28:29,368 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:28:32,469 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 12:28:32,491 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:28:33,043 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:28:33,043 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:28:33,212 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:28:33,230 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:28:33,231 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:28:34,621 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:28:34,792 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:29:10,122 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:29:10,362 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:29:10,381 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:29:10,382 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 12:29:42,254 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:29:42,274 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:29:42,779 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:29:42,779 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:29:42,951 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:29:42,971 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:44,350 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:29:44,519 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:45,125 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:45,328 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:45,502 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:45,675 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:29:45,847 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:45,847 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:29:46,038 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,057 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:29:46,058 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:29:46,059 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:30:13,179 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:30:57,597 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:31:07,114 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 12:31:07,127 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:31:07,633 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:31:07,634 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:31:07,805 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:07,824 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:31:07,825 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:31:07,825 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:09,209 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:31:09,387 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:31:43,890 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:31:44,130 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,148 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:44,149 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: -2026-06-30 12:31:44,150 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 12:31:54,530 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:31:54,544 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:31:54,994 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:31:54,994 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:31:55,169 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:31:55,189 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:56,575 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:31:56,748 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:57,367 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:57,577 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:57,752 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:57,923 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:31:58,094 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:58,094 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:31:58,282 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:31:58,301 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:31:58,302 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:32:39,880 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:34:28,288 WARNING app.orchestrator Operator confirm timeout: mqs_tone -2026-06-30 12:36:10,339 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:36:12,274 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:36:16,715 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:36:17,666 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 12:36:20,059 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:36:21,609 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:37:39,767 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:37:42,638 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:37:42,653 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:37:43,208 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:37:43,208 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:37:43,385 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:43,403 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:37:43,404 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:37:43,404 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:44,787 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:37:44,965 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:45,567 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:45,775 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:45,949 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:46,123 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:37:46,295 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:46,295 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:37:46,484 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,502 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:37:46,503 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:37:46,504 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:38:28,112 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:39:00,162 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:39:01,711 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:39:16,105 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:39:29,578 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:39:29,591 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:39:30,119 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:39:30,119 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:39:30,293 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:39:30,312 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:39:30,313 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:31,690 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:39:31,867 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:32,499 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:32,710 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:32,885 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:33,057 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:39:33,232 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:33,232 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:39:33,420 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,440 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:39:33,441 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:33,442 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:39:42,605 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:39:45,866 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:39:45,892 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:39:46,402 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:39:46,402 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:39:46,575 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:46,593 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:39:46,594 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:39:46,594 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:47,978 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:39:48,156 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:48,777 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:48,988 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:49,163 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:49,340 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:39:49,511 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:49,511 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:39:49,695 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,713 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:39:49,714 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:40:34,546 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 12:40:34,566 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 12:40:35,182 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:40:35,182 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 12:40:35,411 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 12:40:35,450 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:35,451 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 12:40:35,452 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 12:40:35,452 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:36,857 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 12:40:37,029 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:37,659 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:37,878 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:38,054 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:38,226 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 12:40:38,396 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:38,396 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68380 (0x10b1c) -2026-06-30 12:40:38,581 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:40:38,599 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68380 байт -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 12:40:38,600 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 12:41:20,289 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:41:45,632 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:41:47,180 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:50:05,155 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:50:06,706 INFO app.m5_client M5StampPLC не найден -2026-06-30 12:53:12,550 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:53:19,153 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 -2026-06-30 12:53:34,236 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 12:53:35,947 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:00:39,178 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:00:40,865 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:32:56,441 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:32:57,988 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:35:28,536 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:35:45,185 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 13:35:45,204 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 13:35:45,204 DEBUG app.flasher flash_usb [firmware_test]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-30 13:35:45,206 DEBUG app.flasher flash_usb [firmware_test]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-30 13:35:45,381 DEBUG app.flasher flash_usb [firmware_test]: Removed virtual environment at: .venv -2026-06-30 13:35:45,381 DEBUG app.flasher flash_usb [firmware_test]: Creating virtual environment at: .venv -2026-06-30 13:35:45,517 DEBUG app.flasher flash_usb [firmware_test]: Installed 66 packages in 104ms -2026-06-30 13:35:50,049 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 13:35:50,049 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 13:35:50,221 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 13:35:50,241 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:51,617 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 13:35:51,786 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:52,392 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:52,603 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:52,777 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:52,951 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 13:35:53,121 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:53,122 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68364 (0x10b0c) -2026-06-30 13:35:53,309 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68364 байт -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 13:35:53,327 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 13:35:53,328 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 13:36:18,782 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:40:43,635 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 13:40:50,292 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:40:51,840 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:42:37,034 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 13:42:42,356 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:42:43,901 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:43:43,721 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 13:43:48,120 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 13:43:50,861 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:43:52,409 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:44:36,067 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured -2026-06-30 13:48:46,897 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:49:26,682 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:50:01,537 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:50:49,483 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 13:50:51,216 INFO app.m5_client M5StampPLC не найден -2026-06-30 13:56:22,896 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 14:13:37,172 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:13:38,722 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:16:04,892 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:16:11,539 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 -2026-06-30 14:16:18,139 ERROR app.app CDC connect failed: firmware_test не отвечает на ping: /dev/cu.usbmodem11301 -2026-06-30 14:16:20,826 ERROR app.app CDC connect failed: read failed: [Errno 6] Device not configured -2026-06-30 14:16:23,904 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:17:25,582 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 14:18:32,310 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:18:33,929 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:19:27,641 ERROR app.screens.diag run_worker error: read failed: [Errno 6] Device not configured -2026-06-30 14:19:57,182 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:19:58,730 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:36:34,945 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:36:36,495 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:38:19,642 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:38:21,192 INFO app.m5_client M5StampPLC не найден -2026-06-30 14:38:26,849 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 14:38:58,972 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:43:53,430 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:45:10,133 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:45:51,286 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:55:19,262 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:55:56,114 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:56:46,050 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 14:57:28,802 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --erase-chip -2026-06-30 14:57:28,820 DEBUG app.flasher flash_usb [erase]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 14:57:28,822 DEBUG app.flasher flash_usb [erase]: warning: Ignoring existing virtual environment linked to non-existent Python interpreter: .venv/bin/python3 -> python -2026-06-30 14:57:28,825 DEBUG app.flasher flash_usb [erase]: Using CPython 3.10.11 interpreter at: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10 -2026-06-30 14:57:29,003 DEBUG app.flasher flash_usb [erase]: Removed virtual environment at: .venv -2026-06-30 14:57:29,003 DEBUG app.flasher flash_usb [erase]: Creating virtual environment at: .venv -2026-06-30 14:57:29,120 DEBUG app.flasher flash_usb [erase]: Installed 66 packages in 86ms -2026-06-30 14:57:33,713 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 14:57:33,713 DEBUG app.flasher flash_usb [erase]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 14:57:33,888 DEBUG app.flasher flash_usb [erase]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: Операция: chip erase -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: SDP USB: 0x1FC9,0x130 -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: BL USB: 0x15A2,0x73 -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ════════════════════════════════════════════════════════════ -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:57:33,906 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 14:57:33,907 DEBUG app.flasher flash_usb [erase]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 14:57:33,907 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:57:35,315 DEBUG app.flasher flash_usb [erase]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 14:57:35,485 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 14:58:10,903 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 14:58:11,146 DEBUG app.flasher flash_usb [erase]: Response status = 0 (0x0) Success. -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: OK (1с) -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,165 DEBUG app.flasher flash_usb [erase]: Полная очистка Flash (chip erase, ~30 с) -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ⚠️ После chip erase BootROM не сможет загрузить прошивку. -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ⚠️ Используй flash_usb.py для восстановления. -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: $ blhost -t 100000 -u 0x15A2,0x73 -- flash-erase-all 9 -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: Reset -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:11,166 DEBUG app.flasher flash_usb [erase]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 14:58:11,167 DEBUG app.flasher flash_usb [erase]: -2026-06-30 14:58:11,167 DEBUG app.flasher flash_usb [erase]: ✅ Chip erase завершён -2026-06-30 14:58:17,157 INFO app.flasher Running: uv run --directory /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host python /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/flash_usb.py --firmware firmware_test --build-type Release -2026-06-30 14:58:17,178 DEBUG app.flasher flash_usb [firmware_test]: warning: `VIRTUAL_ENV=/Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead -2026-06-30 14:58:17,661 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 14:58:17,661 DEBUG app.flasher flash_usb [firmware_test]: Response status = 2290649224 (0x88888888) Write File Success. -2026-06-30 14:58:17,832 DEBUG app.flasher flash_usb [firmware_test]: Status (HAB mode) = 1450735702 (0x56787856) Hab Is Disabled (Unlocked). -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: MIMXRT1052 Flash Tool (USB SDP) -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Прошивка: firmware_test [Release] -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: SDP USB: 0x1FC9,0x130 -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: BL USB: 0x15A2,0x73 -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ════════════════════════════════════════════════════════════ -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: Загрузка Flashloader через SDP (1FC9:0130) -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- write-file 0x20001C00 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/host/dcd/ivt_flashloader.bin -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: $ sdphost -u 0x1FC9,0x130 -j -- jump-address 0x20001C00 -2026-06-30 14:58:17,851 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:19,246 DEBUG app.flasher flash_usb [firmware_test]: Ожидание Flashloader (до 10с)...Response status = 0 (0x0) Success. -2026-06-30 14:58:19,423 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,055 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,265 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,440 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,618 DEBUG app.flasher flash_usb [firmware_test]: Writing memory -2026-06-30 14:58:20,789 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,789 DEBUG app.flasher flash_usb [firmware_test]: Response word 1 = 68364 (0x10b0c) -2026-06-30 14:58:20,971 DEBUG app.flasher flash_usb [firmware_test]: Response status = 0 (0x0) Success. -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: OK (1с) -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: Конфигурация FlexSPI NOR (инициализация контроллера) -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,995 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xC0000007 word -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Прошивка Flash: firmware_test_hab.bin -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Образ: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Размер: 68364 байт -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Адрес: 0x60001000 -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Стирание: 0x60000000 .. +73728 байт -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- flash-erase-region 0x60000000 73728 0 -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: Запись FCB в Flash[0x60000000] -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- fill-memory 0x2000 4 0xF000000F word -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- configure-memory 9 0x2000 -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- write-memory 0x60001000 /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/build/Release/firmware_test_hab.bin 0 -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:20,996 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: Reset -2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: ──────────────────────────────────────────────────────────── -2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: $ blhost -u 0x15A2,0x73 -- reset -2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: -2026-06-30 14:58:20,997 DEBUG app.flasher flash_usb [firmware_test]: ✅ Прошивка завершена успешно -2026-06-30 14:58:47,116 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:02:20,779 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:02:22,328 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:03:07,785 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:03:09,333 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:03:56,019 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:03:57,569 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:05:47,811 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:05:49,358 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:07:27,853 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:07:29,401 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:08:24,771 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:08:26,321 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:08:55,013 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:08:56,561 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:09:59,613 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:10:01,161 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:12:33,081 WARNING app.firmware_client _recv_until timeout after 120.0 s -2026-06-30 15:14:40,777 WARNING app.firmware_client _recv_until timeout after 120.0 s -2026-06-30 15:33:29,207 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:33:54,602 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:33:56,280 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:37:25,364 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:37:27,091 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:42:36,030 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:42:37,715 INFO app.m5_client M5StampPLC не найден -2026-06-30 15:43:19,205 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 15:43:22,852 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 15:43:23,345 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 15:43:23,701 ERROR app.screens.diag run_worker error: write failed: [Errno 6] Device not configured -2026-06-30 15:43:28,104 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 15:43:29,654 INFO app.m5_client M5StampPLC не найден -2026-06-30 16:19:09,052 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 16:19:10,697 INFO app.m5_client M5StampPLC не найден -2026-06-30 16:20:10,951 ERROR app.orchestrator HIL confirm без M5: opto_in1_active -2026-06-30 16:27:42,558 DEBUG asyncio Using selector: KqueueSelector -2026-06-30 16:27:44,211 INFO app.m5_client M5StampPLC не найден From 86df36fab7bce6cb67eae2114f589945dc477965 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Wed, 1 Jul 2026 10:15:03 +0300 Subject: [PATCH 6/9] # firmware_test: TUI app m5_client fixed --- tools/production/README.md | 634 ++++++++++-------------------- tools/production/app/m5_client.py | 24 +- tools/production/docs/TUI_PLAN.md | 3 +- 3 files changed, 225 insertions(+), 436 deletions(-) diff --git a/tools/production/README.md b/tools/production/README.md index 2d4c0cc..ec6d72e 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -1,6 +1,6 @@ # service-tui — TUI сервисного инженера -TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. +TUI-приложение для диагностики и прошивки платы **MIMXRT1052CVJ5B** на сервисе. Написано на Python + [Textual](https://textual.textualize.io/). Работает на Linux, macOS, Windows. --- @@ -14,24 +14,26 @@ tools/production/ ├── uv.lock └── app/ ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов + ├── app.tcss ← единый файл стилей для всех экранов ├── models.py ← все типы данных (dataclass/Enum) - ├── firmware_client.py ← async USB CDC клиент firmware_test - ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines) + ├── 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 + ├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты + ├── widgets/ + │ ├── __init__.py + │ └── app_frame.py ← AppFrame — общий адаптивный контейнер всех экранов └── screens/ - ├── __init__.py ← реэкспорт: WaitingScreen, FlashScreen, DiagScreen - ├── waiting.py ← WaitingScreen — ожидание USB + ├── __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 — строки результатов - ├── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown - └── styles/ - ├── waiting.tcss - ├── flash.tcss - └── diag.tcss + ├── test_list.py ← TestListPanel — чекбоксы тестов, Выбрать/Снять все + ├── results.py ← ResultsPanel — DataTable результатов + └── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown ``` --- @@ -43,10 +45,10 @@ graph LR subgraph PC["Сервисный ПК"] TUI["service-tui\n(Textual App)"] subgraph app["app/"] - FC["firmware_client.py\nUSB CDC ACM"] - M5["m5_client.py\nSerial JSON-lines"] - FL["flasher.py\nsubprocess"] - OR["orchestrator.py\nconfirm router"] + 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 @@ -64,18 +66,20 @@ graph LR FU["flash_usb.py\nsdphost + blhost"] end - FC <-->|"JSON-lines\nVID:PID 1996:00AD"| FW + 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. +Состояние определяется автодетектом USB и меняется динамически без перезапуска TUI. При потере соединения сессия разрывается полностью — TUI не пытается восстановить прежнее состояние, а стартует заново с `WaitingScreen`. ```mermaid stateDiagram-v2 @@ -84,16 +88,18 @@ stateDiagram-v2 WAITING --> FLASHING : VID:PID 1FC9:0130\n(BootROM SDP) WAITING --> DIAGNOSING : VID:PID 1996:00AD\n+ ping→pong по CDC - FLASHING --> WAITING : FlashDone / ESC / плата отключена - FLASHING --> DIAGNOSING: плата перезагружена после прошивки + FLASHING --> POST_FLASH : firmware_test прошит успешно + FLASHING --> WAITING : Production/Custom прошит, ошибка,\nили потеря USB в простое - DIAGNOSING --> WAITING : DiagDone / ESC / плата отключена + POST_FLASH --> WAITING : оператор подтвердил / таймаут 40с + + DIAGNOSING --> WAITING : DiagDone / ESC /\nпотеря USB в простое DIAGNOSING --> FLASHING: плата переведена в SDP (перемычка BOOT_MOD) ``` -### Режим A — Прошивка +### Режим A — Прошивка (FlashScreen) -Триггер: BootROM SDP `1FC9:0130` виден в `serial.tools.list_ports`. +Триггер: BootROM SDP `1FC9:0130` обнаружен через `pyusb`. ```bash ┌─ Прошивка платы ─────────────────────────────────┐ @@ -104,9 +110,9 @@ stateDiagram-v2 │ ○ Production (bootloader + tft_app) │ │ ○ Кастомный бинарь... │ │ │ -│ [ ▶ Прошить ] [ ⚠ Chip Erase ] │ +│ [ ▶ Прошить ] [ ⚠ Chip Erase ] [ ✕ Выйти ] │ │ │ -│ ████████████░░░░░░ 64% blhost 64% │ +│ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ │ │ ▶ Прошивка: firmware_test │ │ │ │ $ blhost -u 0x15A2,0x0073 -- write-memory… │ │ @@ -114,368 +120,123 @@ stateDiagram-v2 └────────────────────────────────────────────────────┘ ``` -### Режим B — Диагностика +ProgressBar виден только во время активной операции (скрыт в простое), без числового `%` — только полоса и построчный лог в реальном времени. + +### Промежуточный экран — PostFlashScreen + +Показывается **только** после успешной прошивки `firmware_test` (не для Production/Custom — им этот шаг не нужен). + +```bash +┌─ Прошивка завершена ──────────────────────────────┐ +│ ✅ firmware_test успешно записан │ +│ │ +│ Переведите плату в нормальный режим: │ +│ BOOT_MOD_1 → GND → Reset │ +│ │ +│ [ ✓ Готово, перешёл ] [ ✕ Выйти ] │ +│ Автопереход через: 40с │ +└────────────────────────────────────────────────────┘ +``` + +### Режим B — Диагностика (DiagScreen) Триггер: CDC-порт `1996:00AD` виден + `ping→pong` прошёл. ```bash -┌─ Диагностика fw:0.1.4 UID:A1B2C3D4E5F60011 ────────────────┐ -│ M5: ✓ подключён │ -├────────────────────────────┬───────────────────────────────────┤ -│ Тесты │ Результаты │ -│ ☑ SDRAM 32 MB │ sdram ✓ PASS │ -│ ☑ QSPI Flash │ qspi ✓ PASS │ -│ ☑ microSD │ usd ✗ FAIL mount err: 5 │ -│ ☑ TFT Display │ display ✓ PASS │ -│ ☑ Кнопки │ buttons ✓ PASS │ -│ ☑ MQS Audio │ mqs ✓ PASS │ -│ ☑ CAN loopback [HIL] │ can … running │ -│ ☑ Оптовходы [HIL] │ opto pending │ -├────────────────────────────┴───────────────────────────────────┤ -│ [ ▶ Запустить выбранные ] [ ▶▶ Все тесты ] │ -│ ████████████████░░░░ 80% Тест: can │ -├────────────────────────────────────────────────────────────────┤ -│ ⚠ Экран залит красным цветом? 28с │ -│ [ ✓ Да ] [ ✗ Нет ] │ -└────────────────────────────────────────────────────────────────┘ +┌─ Диагностика 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с │ +│ [ ✓ Да ] [ ✗ Нет ] │ +└─────────────────────────────────────────────────────────────────┘ ``` -HIL-тесты без M5StampPLC отображаются серыми и не выбираются автоматически. - ---- - -## Диаграмма классов - -```mermaid -classDiagram - direction TB - - %% ── Точка входа ────────────────────────────────────────── - class ServiceApp { - -_fw: FirmwareClient - -_m5: M5Client - +on_mount() - +_on_device_detected(event) - +_on_flash_done(event) - +_on_diag_done() - +_connect_and_diagnose() - +_disconnect() - } - - %% ── Экраны ─────────────────────────────────────────────── - class WaitingScreen { - -_spinner_idx: int - -_detect_timer: Timer - -_spin_timer: Timer - +on_mount() - +on_unmount() - -_poll_usb() - -_spin() - -_stop_timers() - } - class WaitingScreen.DeviceDetected { - +mode: AppMode - } - - class FlashScreen { - -_flasher: Flasher - -_flashing: bool - +compose() - -_on_radio_changed(event) - -_on_flash_pressed() - -_on_erase_pressed() - -_do_flash(target, bin_path) - -_do_erase() - -_resolve_target() - -_on_progress(progress) - -_set_busy(busy) - } - class FlashScreen.FlashDone { - +success: bool - } - - class DiagScreen { - -_fw: FirmwareClient - -_m5: M5Client - -_orchestrator: Orchestrator - -_session: SessionState - -_running: bool - +on_mount() - -_init_session() - -_update_header() - -_on_run_selected() - -_on_run_all() - -_on_confirmed(event) - -_start_run(test_ids) - -_run_worker(test_ids) - -_handle_event(event, total, done) - -_on_summary(summary) - } - class DiagScreen.DiagDone - - %% ── Виджеты DiagScreen ─────────────────────────────────── - class TestListPanel { - -_checkboxes: dict - +populate(tests, m5_connected) - +get_selected_ids() list - +set_enabled(enabled) - } - - class ResultsPanel { - +populate(tests) - +set_running(test_id) - +set_result(result) - +reset() - -_update(test_id, status, detail) - } - - class ConfirmPanel { - -_timer: Timer - -_remaining: int - +show_operator(prompt, timeout_ms) - +show_buttons_hint(prompt) - +hide() - -_tick() - -_start_timer() - -_stop_timer() - } - class ConfirmPanel.Confirmed { - +confirmed: bool - } - - %% ── Клиенты ────────────────────────────────────────────── - class FirmwareClient { - -_port: str - -_ser: Serial - -_lock: Lock - +connect() - +disconnect() - +ping() bool - +list_tests() list - +run_selected(test_ids) AsyncGenerator - +send_confirm(id, confirmed) - +get_uid() str - +auto_connect(vid, pid)$ - +find_port(vid, pid)$ - } - - class M5Client { - -_port: str - -_ser: Serial - -_lock: Lock - +connect() - +disconnect() - +ping() bool - +relay_set(relay, state) bool - +relay_get(relay) bool - +can_send(id, data) bool - +can_recv(timeout_ms) dict - +auto_connect()$ - +find_port()$ - } - - class Flasher { - -_proc: Process - +detect_sdp()$ bool - +detect_cdc()$ bool - +flash(target, progress_cb, bin_path) bool - +erase_chip(progress_cb) bool - -_run_flash(firmware, build_type, cb) - -_run_flash_bin(bin_path, cb) - -_run_cmd(cmd, label, cb) - } - - %% ── Оркестратор ────────────────────────────────────────── - class Orchestrator { - -_fw: FirmwareClient - -_m5: M5Client - -_operator_queue: Queue - +run_tests(test_ids) AsyncGenerator - +resolve_operator_confirm(confirmed) - -_handle_confirm(confirm) - -_handle_hil_opto(confirm) - -_handle_hil_can_rx(confirm) - -_handle_hil_can_tx(confirm) - -_handle_operator_confirm(confirm) - } - - %% ── Модели ─────────────────────────────────────────────── - class SessionState { - +fw_version: str - +chip_uid: str - +m5_connected: bool - +tests: list - +results: dict - +get_result(id) - +set_result(result) - } - - class OrchestratorEvent { - +type: OrchestratorEventType - +test_id: str - +result: TestResult - +confirm: ConfirmRequest - +summary: dict - +message: str - } - - class TestInfo { - +id: str - +name: str - +critical: bool - +requires_hil: bool - } - - class TestResult { - +id: str - +status: TestStatus - +duration_ms: int - +detail: str - } - - class ConfirmRequest { - +id: str - +prompt: str - +timeout_ms: int - } - - class FlashProgress { - +phase: str - +percent: int - +message: str - } - - %% ── Enum ───────────────────────────────────────────────── - class AppMode { - <> - WAITING - FLASHING - DIAGNOSING - } - - class TestStatus { - <> - PENDING - RUNNING - PASS - FAIL - SKIP - } - - class FlashTarget { - <> - FIRMWARE_TEST - PRODUCTION - CUSTOM - } - - class OrchestratorEventType { - <> - TEST_BEGIN - TEST_RESULT - CONFIRM_NEEDED - CONFIRM_RESOLVED - BUTTONS_PROMPT - SUMMARY - ERROR - } - - %% ── Связи ──────────────────────────────────────────────── - - %% App → экраны - ServiceApp --> WaitingScreen : push/switch - ServiceApp --> FlashScreen : switch - ServiceApp --> DiagScreen : switch - ServiceApp --> FirmwareClient : создаёт - ServiceApp --> M5Client : создаёт - - %% Сообщения от экранов - WaitingScreen ..> WaitingScreen.DeviceDetected : posts - FlashScreen ..> FlashScreen.FlashDone : posts - DiagScreen ..> DiagScreen.DiagDone : posts - ConfirmPanel ..> ConfirmPanel.Confirmed : posts - - %% App слушает сообщения - ServiceApp ..> WaitingScreen.DeviceDetected : on() - ServiceApp ..> FlashScreen.FlashDone : on() - ServiceApp ..> DiagScreen.DiagDone : on() - - %% Экраны → компоненты - FlashScreen --> Flasher : использует - DiagScreen --> Orchestrator : создаёт - DiagScreen --> TestListPanel : монтирует - DiagScreen --> ResultsPanel : монтирует - DiagScreen --> ConfirmPanel : монтирует - DiagScreen --> SessionState : владеет - DiagScreen ..> ConfirmPanel.Confirmed : on() - - %% Оркестратор - Orchestrator --> FirmwareClient : вызывает - Orchestrator --> M5Client : вызывает - Orchestrator ..> OrchestratorEvent : yields - - %% Flasher детект - WaitingScreen --> Flasher : detect_sdp/cdc - - %% Модели - Flasher --> FlashProgress - Orchestrator --> ConfirmRequest - Orchestrator --> TestResult - DiagScreen --> OrchestratorEvent - SessionState --> TestInfo - SessionState --> TestResult - - %% Enum использование - ServiceApp --> AppMode - WaitingScreen.DeviceDetected --> AppMode - TestResult --> TestStatus - Flasher --> FlashTarget - Orchestrator --> OrchestratorEventType - OrchestratorEvent --> OrchestratorEventType -``` +Ключевые отличия от ранних версий TUI: +- **Тесты изначально не выбраны** — сервисник выбирает явно, либо кнопками "Выбрать все"/"Снять все" +- **Результаты — `DataTable`**, не текстовые строки: сортировка FAIL-наверх (стабильная внутри группы по порядку реестра), FAIL-строка подсвечена красным фоном целиком, длинные `detail`-сообщения переносятся на несколько строк без обрезания (явная ширина колонок, см. раздел «Известные грабли Textual») +- **`progress`-события** теста USD (`card_detect`, `mount`, `write`, `read_compare`) отображаются в прогресс-строке как текущая фаза +- HIL-тесты без M5StampPLC — серые, недоступны для выбора (постоянное состояние, не путается с временной блокировкой во время прогона) --- ## Обработка confirm_request -Маршрутизация реализована в `Orchestrator._handle_confirm()` по значению `confirm_request.id`: +Маршрутизация реализована в `Orchestrator._handle_confirm()` по значению `confirm_request.id`. Помимо confirm, протокол v2 определяет `progress` — внутришаговые информационные события долгих тестов (сейчас только `usd`), не требующие ответа. ```mermaid flowchart TD - CR["confirm_request\nот firmware_test"] - CR --> R{confirm_request.id} + EV["Событие от firmware_test"] + EV --> T{type} - R -->|"opto_in1_active\nopto_in1_inactive\nopto_in2_active\nopto_in2_inactive\nopto_rs_active\nopto_rs_inactive"| HIL_OPTO - R -->|"can_rx_ready"| HIL_CAN_RX - R -->|"can_tx_verify"| HIL_CAN_TX - R -->|"btn*"| BTN - R -->|"всё остальное"| OP + 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не показывается оператору"] - HIL_OPTO["M5: relay_set(rly, state)\nsleep(settle)\nsend_confirm(true/false)"] - HIL_CAN_RX["M5: can_send(0x100, data)\nsend_confirm(ok)"] - HIL_CAN_TX["M5: can_recv()\nverify id+data\nsend_confirm(verified)"] - BTN["DiagScreen: show_buttons_hint()\nНЕ отправлять confirm\nтаргет сам детектирует нажатие"] - OP["DiagScreen: show_operator()\nCountdown таймер\nОжидать resolve_operator_confirm()"] - - HIL_OPTO --> RAUTO["CONFIRM_RESOLVED → прогресс"] - HIL_CAN_RX --> RAUTO - HIL_CAN_TX --> RAUTO - OP --> RNEED["CONFIRM_NEEDED → UI панель\nОператор нажимает OK/Нет"] - RNEED --> RESOLVE["ConfirmPanel.Confirmed\n→ resolve_operator_confirm()\n→ send_confirm()"] + 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` | Кто отвечает | Действие TUI | Реле M5 | -| -------------------- | ------------ | -------------------- | ---------- | -| `opto_in1_active` | M5 авто | прогресс | RLY3 ON | -| `opto_in1_inactive` | M5 авто | прогресс | RLY3 OFF | -| `opto_in2_active` | M5 авто | прогресс | RLY4 ON | -| `opto_in2_inactive` | M5 авто | прогресс | RLY4 OFF | -| `opto_rs_active` | M5 авто | прогресс | RLY2 ON | -| `opto_rs_inactive` | M5 авто | прогресс | RLY2 OFF | -| `can_rx_ready` | M5 авто | прогресс | — (CAN TX) | -| `can_tx_verify` | M5 авто | прогресс | — (CAN RX) | -| `btn*` | физика | инструкция оператору | — | -| всё остальное | оператор | prompt + countdown | — | +| `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`) — гарантирует, что элементы управления (кнопки, таблицы) никогда не обрезаются на маленьком терминале и не расползаются на огромном мониторе. --- @@ -487,12 +248,13 @@ graph TB direction LR WS["WaitingScreen"] FS["FlashScreen"] + PF["PostFlashScreen"] DS["DiagScreen"] end subgraph DiagInternals["DiagScreen (screens/diag/)"] TL["TestListPanel\ntest_list.py"] - RP["ResultsPanel\nresults.py"] + RP["ResultsPanel (DataTable)\nresults.py"] CP["ConfirmPanel\nconfirm_panel.py"] OR["Orchestrator\norchestrator.py"] end @@ -505,8 +267,10 @@ graph TB WS -->|"DeviceDetected(FLASHING)"| FS WS -->|"DeviceDetected(DIAGNOSING)"| DS - FS -->|"FlashDone"| WS - DS -->|"DiagDone"| WS + FS -->|"FlashDone(success=True, target=FIRMWARE_TEST)"| PF + FS -->|"FlashDone(остальное)"| WS + PF -->|"Done"| WS + DS -->|"DiagDone(reason)"| WS FS --> FL DS --> OR @@ -517,11 +281,14 @@ graph TB OR --> FC OR --> M5 WS --> FL + + FS -.->|"ConnectionWatcherMixin"| FL + DS -.->|"ConnectionWatcherMixin"| FL ``` --- -## Жизненный цикл сессии +## Жизненный цикл диагностической сессии ```mermaid sequenceDiagram @@ -534,30 +301,36 @@ sequenceDiagram OP->>TUI: запустить service_tui TUI->>WS: push_screen() - WS->>WS: poll USB каждые 1.5 с + 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() + TUI->>DS: switch_screen(fw_version=...) - DS->>FW: list_tests() → TestInfo×8 - DS->>FW: get_uid() → "A1B2C3D4..." - DS->>DS: populate TestListPanel + ResultsPanel + DS->>FW: list_tests() → TestInfo×N + DS->>FW: get_uid() + DS->>DS: populate (тесты НЕ выбраны по умолчанию) + DS->>DS: ConnectionWatcherMixin: старт мониторинга - OP->>DS: выбрать тесты → Запустить + 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) - DS->>DS: прогресс CONFIRM_RESOLVED else Оператор (display / mqs) FW-->>DS: confirm_request DS->>DS: ConfirmPanel.show_operator() @@ -570,21 +343,26 @@ sequenceDiagram end FW-->>DS: test_result - DS->>DS: ResultsPanel.set_result() + DS->>DS: ResultsPanel.set_result() + сортировка FAIL-наверх end FW-->>DS: summary DS->>DS: показать итог PASS / FAIL - OP->>DS: ESC → DiagDone - TUI->>WS: switch_screen() + + 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`) -Файл `.env` в корне репозитория — единый источник. Загружается через `python-dotenv` в `main.py` до импорта app-модулей. - ```ini # USB VID:PID — BootROM SDP (константы NXP, не менять) BOOTROM_VID=1fc9 @@ -598,11 +376,19 @@ FLASHLOADER_PID=0073 SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad +# Тип сборки firmware_test для прошивки (Debug | Release). +# Release временно нестабилен — по умолчанию Debug. +FIRMWARE_BUILD_TYPE=Debug + # Опционально: путь к директории лога TUI # SERVICE_LOG_DIR=/tmp ``` -Пути к бинарям `flash_usb.py` вычисляет автоматически из `BUILD_DIR` (также из `.env`). +--- + +## Версионирование firmware + +`firmware_test` версионируется через CMake (`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через `configure_file`. Команда протокола `get_version` (по аналогии с `get_uid`) запрашивается один раз при подключении в `ServiceApp._connect_and_diagnose()` и передаётся в `DiagScreen` параметром конструктора — версия не запрашивается повторно внутри самого экрана. --- @@ -611,25 +397,18 @@ SERVICE_CDC_PID=00ad ### Из монорепозитория (разработчик) ```bash -# Установить зависимости tools/production/ -just host::service-setup - -# Запустить TUI -just host::service-tui +just host::service-setup # установить зависимости tools/production/ +just host::service-tui # запустить TUI ``` ### Standalone-бинарь (сервисник) -Скачать `service_tui` из [GitHub Releases](https://github.com/OSabuser/tft_manufacture_test/releases) и запустить двойным кликом — Python не требуется. - -Для сборки из исходников: - ```bash just host::service-build # → tools/production/dist/service_tui ``` -> **Важно:** standalone-бинарь не включает `tools/host/`. Перед сборкой убедитесь что `tools/host/` инициализирован (`just host::setup-tools`) и доступен рядом с бинарём, либо измените `_FLASH_USB_SCRIPT` в `flasher.py` на абсолютный путь. +> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен инициализированный `tools/host/` (`just host::setup-tools`), либо абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`). --- @@ -639,56 +418,65 @@ just host::service-build ```bash 1. BOOT_MOD_1 → GND, сбросить плату -2. Подключить USB к сервисному ПК -3. TUI: WaitingScreen → обнаружен CDC 1996:00AD → DiagScreen -4. Выбрать тесты (или Все тесты) → Запустить -5. Ответить на интерактивные запросы (display, mqs) -6. Получить итог PASS / FAIL +2. Подключить USB → DiagScreen +3. Выбрать тесты (по умолчанию ничего не выбрано) или "Выбрать все" +4. Запустить → ответить на интерактивные запросы +5. Получить итог; FAIL-тесты — наверху таблицы, detail виден полностью ``` ### Перепрошивка firmware_test ```bash -1. BOOT_MOD_1 → 3V3, сбросить плату -2. Подключить USB → TUI: FlashScreen -3. Выбрать firmware_test → Прошить -4. BOOT_MOD_1 → GND, сбросить плату -5. TUI автоматически переходит в DiagScreen +1. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen +2. Выбрать firmware_test → Прошить +3. PostFlashScreen: BOOT_MOD_1 → GND, сбросить плату +4. Нажать "Готово" (или дождаться авто-перехода через 40с) +5. TUI автоматически попадает в DiagScreen при следующем подключении ``` -### Chip Erase (сброс Flash в FF) +### Chip Erase ```bash 1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3) 2. FlashScreen → Chip Erase (~30 с) -3. После erase: BootROM не загрузит прошивку — - необходимо перепрошить (пункт выше) +3. После erase BootROM не загрузит прошивку — требуется перепрошить ``` --- ## Зависимости -| Пакет | Версия | Назначение | -| --------------- | ------ | ------------------------ | -| `textual` | ≥ 0.80 | TUI фреймворк | -| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | -| `python-dotenv` | ≥ 1.0 | загрузка `.env` | -| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | +| Пакет | Версия | Назначение | +| --------------- | ------ | ----------------------------------------------------- | +| `textual` | ≥ 0.80 | TUI фреймворк | +| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | +| `pyusb` | ≥ 1.0 | детект BootROM SDP (не виден через pyserial на macOS) | +| `python-dotenv` | ≥ 1.0 | загрузка `.env` | +| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | -**Runtime-зависимость (не в `pyproject.toml`):** -`flasher.py` вызывает `tools/host/flash_usb.py` через `uv run` — uv-окружение `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)`. --- ## Логирование -TUI логирует в файл (не в stdout — Textual захватывает терминал): - ```bash tools/production/service_tui.log ← по умолчанию $SERVICE_LOG_DIR/service_tui.log ← если задан в .env ``` -Уровень: `DEBUG` для всех модулей, `WARNING` для textual. -При standalone-запуске лог создаётся рядом с исполняемым файлом. +Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не пишет в stdout — Textual захватывает терминал. diff --git a/tools/production/app/m5_client.py b/tools/production/app/m5_client.py index 03ea043..cd0c4fe 100644 --- a/tools/production/app/m5_client.py +++ b/tools/production/app/m5_client.py @@ -19,7 +19,7 @@ Blocking-вызовы выполняются в executor чтобы не бло """ from __future__ import annotations - +import os import asyncio import json import logging @@ -30,9 +30,9 @@ import serial.tools.list_ports logger = logging.getLogger(__name__) -# VID/PID M5StampPLC (Espressif USB JTAG/serial) -_M5_VID = 0x303A -_M5_PID = 0x1001 +# VID/PID M5StampPLC +_M5_VID = int(os.environ.get("SERVICE_M5_VID", "0x303A"), 16) +_M5_PID = int(os.environ.get("SERVICE_M5_PID", "0x4001"), 16) _READLINE_TIMEOUT_S = 0.5 _CMD_TIMEOUT_S = 3.0 @@ -142,18 +142,18 @@ class M5Client: async def ping(self) -> bool: """Проверить связь с агентом.""" resp = await self._cmd({"cmd": "ping"}) - return resp is not None and resp.get("status") == "ok" + return resp is not None and resp.get("ok") is True async def relay_set(self, relay: int, state: bool) -> bool: """ Переключить реле. - :param relay: Номер реле 1..4. + :param relay: Номер реле 1..4 (в протоколе агента — поле "ch"). :param state: True = ON, False = OFF. :return: True при успехе. """ - resp = await self._cmd({"cmd": "relay_set", "relay": relay, "state": state}) - return resp is not None and resp.get("status") == "ok" + resp = await self._cmd({"cmd": "relay_set", "ch": relay, "state": state}) + return resp is not None and resp.get("ok") is True async def relay_get(self, relay: int) -> Optional[bool]: """ @@ -161,15 +161,15 @@ class M5Client: :return: True/False или None при ошибке. """ - resp = await self._cmd({"cmd": "relay_get", "relay": relay}) - if resp and resp.get("status") == "ok": + resp = await self._cmd({"cmd": "relay_get", "ch": relay}) + if resp and resp.get("ok") is True: return bool(resp.get("state")) return None async def can_send(self, can_id: int, data: list[int]) -> bool: """Отправить CAN-фрейм через M5.""" resp = await self._cmd({"cmd": "can_send", "id": can_id, "data": data}) - return resp is not None and resp.get("status") == "ok" + return resp is not None and resp.get("ok") is True async def can_recv(self, timeout_ms: int = 500) -> Optional[dict]: """ @@ -178,7 +178,7 @@ class M5Client: :return: {"id": int, "data": list[int]} или None при таймауте/ошибке. """ resp = await self._cmd({"cmd": "can_recv", "timeout_ms": timeout_ms}) - if resp and resp.get("status") == "ok": + if resp and resp.get("ok") is True: return {"id": resp["id"], "data": resp["data"]} return None diff --git a/tools/production/docs/TUI_PLAN.md b/tools/production/docs/TUI_PLAN.md index 1d996b6..1463e00 100644 --- a/tools/production/docs/TUI_PLAN.md +++ b/tools/production/docs/TUI_PLAN.md @@ -4,4 +4,5 @@ 2. Уменьшить размер активной области до предела и сделать его равным для всех экранов 3. Добавить обратный отчет в тесте buttons (таймаут) 4. Выгрузка результатов тестов в json файл с привязкой к UID микроконтроллера -5. Возможно ли копировать UID с экрана терминала? \ No newline at end of file +5. Возможно ли копировать UID с экрана терминала? +6. Проверить работоспособность HIL тестов (CAN & Opto) From 0132d8056dcfd8fccbb7ad6332ea128a56d0c086 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Wed, 1 Jul 2026 11:11:35 +0300 Subject: [PATCH 7/9] # firmware_test: TUI app screens styling & app_logo --- tools/production/app/app.tcss | 39 ++++++++++++++++------ tools/production/app/boot_art.py | 33 ++++++++++++++++++ tools/production/app/screens/post_flash.py | 14 ++++---- tools/production/app/screens/waiting.py | 32 +++++++++++++++++- tools/production/pyproject.toml | 2 +- tools/production/uv.lock | 2 +- 6 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 tools/production/app/boot_art.py diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss index 969a812..621a2ae 100644 --- a/tools/production/app/app.tcss +++ b/tools/production/app/app.tcss @@ -15,8 +15,8 @@ Screen { AppFrame { width: 100%; height: 100%; - max-width: 160; - max-height: 50; + max-width: 112; + max-height: 35; border: heavy $primary; background: $surface; padding: 1 2; @@ -25,19 +25,31 @@ AppFrame { /* ── WaitingScreen ──────────────────────────────────────── */ #waiting-frame { - align: center middle; + align: center top; } -#waiting-logo { - width: 52; - height: 7; - border: round $primary; - padding: 1 2; - content-align: center middle; - color: $primary; +#waiting-logo-row { + width: 100%; + height: auto; + margin-top: 1; +} + +#waiting-logo-art { + width: auto; +} + +#waiting-version-row { + width: 100%; + height: auto; + margin-top: 1; +} + +#waiting-version { + color: $warning; text-style: bold; + text-align: center; + margin-bottom: 1; } - #waiting-reason { margin-top: 1; padding: 1 2; @@ -163,6 +175,11 @@ AppFrame { margin-right: 2; } +#post-flash-title-row, +#post-flash-instruction-row { + width: 100%; + height: auto; +} /* ── DiagScreen ─────────────────────────────────────────── */ #diag-frame { diff --git a/tools/production/app/boot_art.py b/tools/production/app/boot_art.py new file mode 100644 index 0000000..08d9e64 --- /dev/null +++ b/tools/production/app/boot_art.py @@ -0,0 +1,33 @@ +""" +boot_art.py — ASCII-логотип компании для WaitingScreen. + +Сгенерирован программно (coverage-based растеризация logo.png в +символы плотности + разделение по цвету blue/dark), затем зафиксирован +как статичная строка — рантайм-зависимости на Pillow/etc. не требуется. +""" + +from __future__ import annotations + +LOGO_ART = """ +[grey37]........[/grey37][#3ca0dc].**:+*::*+.[/#3ca0dc][grey37]..........[/grey37] +[grey37]........[/grey37][#3ca0dc].##:*#++#*.[/#3ca0dc][grey37]..........[/grey37] +[grey37].........[/grey37][#3ca0dc]*#[/#3ca0dc][grey37].[/grey37][#3ca0dc]:#..#+[/#3ca0dc][grey37]...........[/grey37] +[grey37].......[/grey37][#3ca0dc].+#:[/#3ca0dc][grey37].[/grey37][#3ca0dc]:#..#+[/#3ca0dc][grey37]...........[/grey37] +[grey37].....[/grey37][#3ca0dc].+#+..:#*..#+[/#3ca0dc][grey37]...........[/grey37] +[grey37]...[/grey37][#3ca0dc].+#+..:**:..*#.[/#3ca0dc][white].:.[/white][grey37]........[/grey37] +[grey37].[/grey37][#3ca0dc].+#+..:#*:..*#+.[/#3ca0dc][white]+@@+[/white][grey37]........[/grey37] +[grey37].[/grey37][#3ca0dc]##..:#*.[/#3ca0dc][grey37].[/grey37][#3ca0dc].*#+.[/#3ca0dc][white]%@@@@%[/white][grey37].[/grey37][white]::.[/white][grey37]....[/grey37] +[grey37].[/grey37][#3ca0dc]#*[/#3ca0dc][grey37].[/grey37][#3ca0dc]:#:[/#3ca0dc][grey37].[/grey37][#3ca0dc].*#+.[/#3ca0dc][white]+@@@@@@%[/white][grey37].[/grey37][white]+@...[/white][grey37]..[/grey37] +[grey37].[/grey37][#3ca0dc]#*[/#3ca0dc][grey37].[/grey37][#3ca0dc]+#..#+.[/#3ca0dc][grey37]........[/grey37][white]%%[/white][grey37].[/grey37][white]+@..%.[/white][grey37].[/grey37] +[grey37].[/grey37][#3ca0dc]#*[/#3ca0dc][grey37].[/grey37][#3ca0dc]+#.:#:[/#3ca0dc][grey37].........[/grey37][white]%%[/white][grey37].[/grey37][white]+@..@:[/white][grey37].[/grey37] +[grey37].[/grey37][#3ca0dc]**[/#3ca0dc][grey37].[/grey37][#3ca0dc]+#.:#:[/#3ca0dc][grey37]........[/grey37][white].%%[/white][grey37].[/grey37][white]+@..@:[/white][grey37].[/grey37] +[grey37].[/grey37][#3ca0dc]..[/#3ca0dc][grey37].[/grey37][#3ca0dc]+#.:#*+++++.[/#3ca0dc][white].:@%.[/white][grey37].[/grey37][white]+@..@:[/white][grey37].[/grey37] +[grey37]....[/grey37][#3ca0dc].*.:#####+.[/#3ca0dc][white]+@%.[/white][grey37].[/grey37][white]:%%:[/white][grey37].[/grey37][white]:@:[/white][grey37].[/grey37] +[grey37]......[/grey37][#3ca0dc].:###+.[/#3ca0dc][white]+@%.[/white][grey37].[/grey37][white].%%:..+@+.[/white][grey37].[/grey37] +[grey37].......[/grey37][#3ca0dc].:+.[/#3ca0dc][white]:@%.[/white][grey37].[/grey37][white].%@:..+@+.[/white][grey37]...[/grey37] +[grey37]...........[/grey37][white]@%[/white][grey37].[/grey37][white].%@:..+@+.[/white][grey37].....[/grey37] +[grey37]...........[/grey37][white]@%[/white][grey37].[/grey37][white]%@..+@+.[/white][grey37].......[/grey37] +[grey37]...........[/grey37][white]@%[/white][grey37].[/grey37][white]%@[/white][grey37].[/grey37][white]:@:[/white][grey37].........[/grey37] +[grey37]..........[/grey37][white].@%.%@.+@+[/white][grey37].........[/grey37] +[grey37]..........[/grey37][white]:@@:@@+%@%.[/white][grey37]........[/grey37] +""".strip("\n") diff --git a/tools/production/app/screens/post_flash.py b/tools/production/app/screens/post_flash.py index aaf103d..eaf6b87 100644 --- a/tools/production/app/screens/post_flash.py +++ b/tools/production/app/screens/post_flash.py @@ -19,7 +19,7 @@ post_flash.py — экран-промпт после успешной проши from __future__ import annotations from textual.app import ComposeResult -from textual.containers import Horizontal +from textual.containers import Center, Horizontal from textual.message import Message from textual.screen import Screen from textual.timer import Timer @@ -49,12 +49,14 @@ class PostFlashScreen(Screen): def compose(self) -> ComposeResult: with AppFrame(id="post-flash-frame"): - yield Label("✅ firmware_test успешно записан", id="post-flash-title") + with Center(id="post-flash-title-row"): + yield Label("✅ firmware_test успешно записан", id="post-flash-title") - yield Static( - "Переведите плату в нормальный режим:\nBOOT_MOD_1 → GND → Reset", - id="post-flash-instruction", - ) + with Center(id="post-flash-instruction-row"): + yield Static( + "Переведите плату в нормальный режим:\nBOOT_MOD_1 → GND → Reset", + id="post-flash-instruction", + ) yield Static("", id="post-flash-countdown") diff --git a/tools/production/app/screens/waiting.py b/tools/production/app/screens/waiting.py index 12818c4..e577de6 100644 --- a/tools/production/app/screens/waiting.py +++ b/tools/production/app/screens/waiting.py @@ -14,25 +14,53 @@ waiting.py — экран ожидания подключения платы. from __future__ import annotations +import logging +import tomllib +from pathlib import Path from typing import Optional from textual.app import ComposeResult +from textual.containers import Center from textual.css.query import NoMatches from textual.message import Message from textual.screen import Screen from textual.timer import Timer from textual.widgets import Static +from ..boot_art import LOGO_ART from ..flasher import Flasher from ..models import AppMode from ..widgets import AppFrame +logger = logging.getLogger(__name__) + _DETECT_INTERVAL_S = 1.5 _SPIN_INTERVAL_S = 0.1 _REASON_DISPLAY_S = 4.0 _SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] +def _read_app_version() -> str: + """ + Прочитать версию service-tui из pyproject.toml. + + Не используем importlib.metadata — проект не устанавливается как пакет + (tool.uv.package = false), метаданные могут отсутствовать. Читаем файл + напрямую через tomllib (stdlib, requires-python >= 3.11 уже задан). + """ + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + try: + with pyproject_path.open("rb") as f: + data = tomllib.load(f) + return data.get("project", {}).get("version", "0.0.0") + except Exception as exc: + logger.warning("Не удалось прочитать версию из %s: %s", pyproject_path, exc) + return "0.0.0" + + +_APP_VERSION = _read_app_version() + + class WaitingScreen(Screen): """ Экран ожидания. @@ -58,7 +86,9 @@ class WaitingScreen(Screen): def compose(self) -> ComposeResult: with AppFrame(id="waiting-frame"): - yield Static("TFT Indicator Board\nService Tool", id="waiting-logo") + yield Static(f"service_tool v{_APP_VERSION}", id="waiting-version") + with Center(id="waiting-logo-row"): + yield Static(LOGO_ART, id="waiting-logo-art") yield Static("", id="waiting-reason", classes="hidden") yield Static("Подключите плату индикатора к USB...", id="waiting-hint") yield Static(_SPINNER_FRAMES[0], id="waiting-spinner") diff --git a/tools/production/pyproject.toml b/tools/production/pyproject.toml index 20b5d72..f319c69 100644 --- a/tools/production/pyproject.toml +++ b/tools/production/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "service-tui" -version = "0.1.0" +version = "0.1.1" description = "TUI сервисного инженера для диагностики платы MIMXRT1052" requires-python = ">=3.11" dependencies = [ diff --git a/tools/production/uv.lock b/tools/production/uv.lock index b041381..e4fccb2 100644 --- a/tools/production/uv.lock +++ b/tools/production/uv.lock @@ -200,7 +200,7 @@ wheels = [ [[package]] name = "service-tui" -version = "0.1.0" +version = "0.1.1" source = { virtual = "." } dependencies = [ { name = "pyinstaller" }, From 2a6256e745b536b7ee8b97b7383106fc94f4bca6 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Wed, 1 Jul 2026 15:00:52 +0300 Subject: [PATCH 8/9] # firmware_test: TUI app docs --- firmware/test/CMakeLists.txt | 2 +- tools/production/README.md | 524 +++++------------- tools/production/docs/DEV_ARCH.md | 453 +++++++++++++++ tools/production/docs/TUI_REPORT_1.md | 209 ------- tools/production/docs/TUI_REPORT_2.md | 8 - .../docs/{TUI_SESSION.md => TUI_SESSION_1.md} | 0 tools/production/docs/TUI_SESSION_2.md | 163 ++++++ tools/production/docs/pics/FLASH.png | Bin 56806 -> 0 bytes tools/production/docs/pics/POST_FLASH.png | Bin 45998 -> 0 bytes tools/production/docs/pics/TEST_SCREEN.png | Bin 88874 -> 0 bytes tools/production/docs/pics/WAITING.png | Bin 35099 -> 0 bytes .../docs/pics/WAITING_AFTER_LOSS.png | Bin 39786 -> 0 bytes 12 files changed, 755 insertions(+), 604 deletions(-) create mode 100644 tools/production/docs/DEV_ARCH.md delete mode 100644 tools/production/docs/TUI_REPORT_1.md delete mode 100644 tools/production/docs/TUI_REPORT_2.md rename tools/production/docs/{TUI_SESSION.md => TUI_SESSION_1.md} (100%) create mode 100644 tools/production/docs/TUI_SESSION_2.md delete mode 100644 tools/production/docs/pics/FLASH.png delete mode 100644 tools/production/docs/pics/POST_FLASH.png delete mode 100644 tools/production/docs/pics/TEST_SCREEN.png delete mode 100644 tools/production/docs/pics/WAITING.png delete mode 100644 tools/production/docs/pics/WAITING_AFTER_LOSS.png 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 19c5216f6c7037c9be6bbbe0f80c1b91d0758d02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56806 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%0ASlw_F^& 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 diff --git a/tools/production/docs/pics/POST_FLASH.png b/tools/production/docs/pics/POST_FLASH.png deleted file mode 100644 index a0c5998a3cc2da0fc383527046d75f0432c9b83c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45998 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 F{ttK2x+VYs diff --git a/tools/production/docs/pics/TEST_SCREEN.png b/tools/production/docs/pics/TEST_SCREEN.png deleted file mode 100644 index 32c74c02c22b458aefaa1980fe63166a12b6d07e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88874 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

jn0;@#0L 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 diff --git a/tools/production/docs/pics/WAITING.png b/tools/production/docs/pics/WAITING.png deleted file mode 100644 index f13b869a32eec1774c7e40a13b55c17f798306f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35099 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 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 52544add14705af095a5f5878c73927e5b5c616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39786 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 Date: Wed, 1 Jul 2026 16:54:10 +0300 Subject: [PATCH 9/9] # firmware_test: service-tui is ready for packing, docs updated --- docs/DEV_ARCH.md | 7 +- docs/HOW_TO_FLASH.md | 43 ++++- tools/host/README.md | 17 +- tools/host/flash_usb.py | 56 +++++- tools/production/README.md | 50 ++++- tools/production/app/app.py | 12 +- tools/production/app/app.tcss | 29 ++- tools/production/app/flasher.py | 177 +++++++++++++++++- tools/production/app/models.py | 43 +++++ tools/production/app/screens/flash.py | 96 ++++++++-- .../custom_binaries/TFT_BOOTLOADER_NEW.bin | Bin 0 -> 39676 bytes .../custom_binaries/TFT_BOOTLOADER_OLD.bin | Bin 0 -> 47432 bytes tools/production/docs/DEV_ARCH.md | 131 ++++++++++++- tools/production/pyproject.toml | 2 +- tools/production/uv.lock | 2 +- 15 files changed, 606 insertions(+), 59 deletions(-) create mode 100644 tools/production/custom_binaries/TFT_BOOTLOADER_NEW.bin create mode 100644 tools/production/custom_binaries/TFT_BOOTLOADER_OLD.bin diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 26975b4..61bd5f8 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -194,9 +194,14 @@ flowchart LR │ │ ├── flash_usb.py ← USB SDP: sdphost + blhost │ │ ├── flash_swd.py ← SWD: FCB + HAB → pyOCD Flash │ │ ├── hab/ ← HAB yaml-конфиги (nxpimage) -│ │ ├── dcd/ ← w25q128_fdcb.bin, ivt_flashloader.bin +│ │ ├── dcd/ ← w25q128_fdcb.bin, w25q512_fdcb.bin, +│ │ │ dcd.bin, ivt_flashloader.bin │ │ └── uv.lock │ │ +│ ├── production/ ← service-tui: TUI сервисного инженера (Textual) +│ │ прошивка/диагностика готовых плат, см. +│ │ tools/production/README.md + DEV_ARCH.md +│ │ │ └── hil/ ← HIL pytest-окружение │ ├── conftest.py ← фикстуры: m5, loaded_, uart_ │ ├── pyocd_utils.py ← FLEXRAM init, ELF loader, run_from_vectors diff --git a/docs/HOW_TO_FLASH.md b/docs/HOW_TO_FLASH.md index fa5c11e..d47c9cb 100644 --- a/docs/HOW_TO_FLASH.md +++ b/docs/HOW_TO_FLASH.md @@ -72,7 +72,26 @@ flowchart TD ``` ROM-загрузчик сам конфигурирует FlexSPI через DCD из HAB-образа, поэтому FCB -в образе не нужен — его пишет Flashloader отдельно. +в образе не нужен — его пишет Flashloader отдельно. Это верно для W25Q128 +(текущая плата) — auto-config Flashloader для неё проверен на практике. +Для плат с другой памятью (W25Q256/512, 4-байтная адресация) надёжность +auto-config не подтверждена — см. 1.5. + +### 1.5 Нестандартная память (W25Q256/512) и сторонние бинарники + +`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не +в этом репозитории (например, старые платы с W25Q512), тем же способом +(USB SDP), но с двумя отличиями от штатного пути: + +- HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на + лету через `nxpimage`, а не заранее через `just build::hab-*` +- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`, + буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config + для 4-байтной адресации не проверялся, решили на него не полагаться + +Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь +(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему +использует auto-config Flashloader, как описано в 1.4. --- @@ -141,15 +160,19 @@ FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP Secure ## Сравнение способов -| | USB SDP | SWD | -| ------------------------- | -------------------------------- | -------------------- | -| Переключение BOOT_MODE | Нужно | Не нужно | -| Power cycle после записи | Не нужен | **Обязателен** | -| FCB в образе | Не нужен (Flashloader пишет сам) | **Обязателен** | -| Скорость записи | ~50–100 kB/s | ~8–10 kB/s | -| Совместимость с отладкой | Раздельно | MCU-Link монопольный | -| Производственный сценарий | ✓ | — | -| Итеративная разработка | Неудобно (смена режима) | ✓ | +| | USB SDP | SWD | +| ------------------------- | ----------------------- | -------------------- | +| Переключение BOOT_MODE | Нужно | Не нужно | +| Power cycle после записи | Не нужен | **Обязателен** | +| FCB в образе | Не нужен для W25Q128* | **Обязателен** | +| Скорость записи | ~50–100 kB/s | ~8–10 kB/s | +| Совместимость с отладкой | Раздельно | MCU-Link монопольный | +| Производственный сценарий | ✓ | — | +| Итеративная разработка | Неудобно (смена режима) | ✓ | + +\* Flashloader пишет FCB сам через auto-config — проверено для W25Q128. +Для сторонних бинарников с другой памятью `service-tui` пишет FCB явно, +см. 1.5. --- diff --git a/tools/host/README.md b/tools/host/README.md index 32fc649..40a326f 100644 --- a/tools/host/README.md +++ b/tools/host/README.md @@ -12,21 +12,28 @@ Python-окружение на базе [uv](https://docs.astral.sh/uv/) для ```bash tools/host/ ├── flash_usb.py — прошивка через USB ROM: sdphost → Flashloader → Flash +│ (+ --bin-path/--fcb-path — сторонние образы с явным +│ FCB, вызывается из service-tui, см. tools/production/) ├── flash_swd.py — прошивка через SWD: FCB + HAB → pyOCD → Flash -├── hab/ — HAB yaml-конфиги для nxpimage (по одному на проект × тип) +├── hab/ — HAB yaml-конфиги для nxpimage (по одному на проект × тип; +│ service-tui генерирует такие же временно, на лету — +│ см. tools/production/DEV_ARCH.md, §8) ├── dcd/ │ ├── ivt_flashloader.bin — NXP Flashloader (загружается в RAM через SDP) │ ├── dcd.bin — DCD: инициализация SDRAM (SEMC + MT48LC16M16A2P) │ ├── w25q128_fdcb.bin — FCB для W25Q128 Quad SPI ← используется │ ├── w25q64_fdcb.bin — FCB для W25Q64 Quad SPI -│ └── w25q512_fdcb.bin — FCB для W25Q512 Quad SPI +│ └── w25q512_fdcb.bin — FCB для W25Q512 Quad SPI ← используется ├── ../../docs/mimxrt1052/HAB_GUIDE.md — подробно про HAB-образы и процесс подписи ├── pyproject.toml └── uv.lock ``` > Все бинарники в `dcd/` получены из NXP SecureProvisioningTool и хранятся -> в репозитории — пересоздавать не нужно. +> в репозитории — пересоздавать не нужно. `w25q128`/`w25q512` — единственные +> два варианта в реальном использовании (64 и 256 сведены к ним же, см. +> `tools/production/DEV_ARCH.md`, §8.2); `w25q64_fdcb.bin` пока не подключён +> нигде — оставлен про запас. --- @@ -35,6 +42,10 @@ tools/host/ Подробное описание обоих способов прошивки — в `docs/HOW_TO_FLASH.md`. Сравнительная таблица, карта Flash, диагностика — там же. +Прошивка сторонних/легаси бинарников с нестандартной памятью (явный FCB, +без auto-config) — через `service-tui` (`tools/production/`), не напрямую +через `flash_usb.py` из терминала. Детали конвейера — `tools/production/DEV_ARCH.md`, §8. + --- ## Быстрый старт diff --git a/tools/host/flash_usb.py b/tools/host/flash_usb.py index 642aa0b..76e8b6a 100644 --- a/tools/host/flash_usb.py +++ b/tools/host/flash_usb.py @@ -76,6 +76,8 @@ FLEXSPI_FCB_VALUE = "0xF000000F" FLASH_BASE = 0x60000000 HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET +ERASE_ALL_TIMEOUT_MS = "200000" # W25Q512 стирается заметно дольше W25Q128 + # ─── Helpers ────────────────────────────────────────────────────────────────── @@ -216,10 +218,39 @@ def write_fcb() -> None: ) +def write_fcb_explicit(fcb_path: Path) -> None: + """Записывает буквальный FCB-блоб (512 байт) в Flash[0x60000000]. + + В отличие от write_fcb() (magic option word 0xF000000F — auto-config + Flashloader, надёжно проверен только для W25Q128), здесь FCB пишется + байт-в-байт через write-memory. Нужен для custom-бинарей: nxpimage + всегда собирает "чистый" app-образ без FCB (см. hab_*.yaml — FCB туда + не входит), поэтому его нужно подставлять явно под конкретный чип — + tools/host/dcd/w25q128_fdcb.bin или tools/host/dcd/w25q512_fdcb.bin. + """ + if not fcb_path.exists(): + print(f"[ERROR] FCB-файл не найден: {fcb_path}", file=sys.stderr) + sys.exit(1) + + step(f"Запись явного FCB ({fcb_path.name}) в Flash[0x60000000]") + run( + [ + "blhost", + "-u", + BLHOST_USB, + "--", + "write-memory", + f"0x{FLASH_BASE:08X}", + str(fcb_path), + "0", + ] + ) + + # ─── Основные операции ──────────────────────────────────────────────────────── -def flash(hab_bin: Path, ram_only: bool = False) -> None: +def flash(hab_bin: Path, ram_only: bool = False, fcb_path: Path | None = None) -> None: """Прошить HAB-образ в Flash или загрузить в RAM.""" if not hab_bin.exists(): print(f"[ERROR] Файл не найден: {hab_bin}", file=sys.stderr) @@ -280,7 +311,10 @@ def flash(hab_bin: Path, ram_only: bool = False) -> None: ] ) - write_fcb() + if fcb_path is not None: + write_fcb_explicit(fcb_path) + else: + write_fcb() run( [ @@ -318,7 +352,7 @@ def erase_chip() -> None: [ "blhost", "-t", - "100000", + ERASE_ALL_TIMEOUT_MS, "-u", BLHOST_USB, "--", @@ -342,6 +376,17 @@ def main() -> None: epilog=__doc__, ) + parser.add_argument( + "--fcb-path", + type=Path, + metavar="PATH", + default=None, + help=( + "Явный FCB-блоб (512 байт) для записи в 0x60000000 вместо " + "auto-config Flashloader. Имеет смысл только с --bin-path." + ), + ) + # Группа: что прошивать (взаимоисключающие варианты) target_group = parser.add_mutually_exclusive_group() target_group.add_argument( @@ -381,6 +426,9 @@ def main() -> None: if args.erase_chip and args.ram_only: parser.error("--erase-chip несовместим с --ram-only") + if args.fcb_path is not None and args.bin_path is None: + parser.error("--fcb-path имеет смысл только вместе с --bin-path") + if args.firmware is None and args.bin_path is None and not args.erase_chip: parser.error("Укажи --firmware, --bin-path или --erase-chip") @@ -413,7 +461,7 @@ def main() -> None: if args.erase_chip: erase_chip() elif hab_bin is not None: - flash(hab_bin, ram_only=args.ram_only) + flash(hab_bin, ram_only=args.ram_only, fcb_path=args.fcb_path) if __name__ == "__main__": diff --git a/tools/production/README.md b/tools/production/README.md index 4a7226d..395117a 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -45,13 +45,18 @@ TUI не пытается восстановить прежнее состоян │ ◉ Диагностическая прошивка (firmware_test) │ │ ○ Серийная прошивка (bootloader + tft_app) │ │ ○ Другое │ -│ [ Имя бинарного файла в custom_binaries/ (.bin) ]│ ← только если «Другое» +│ Файл (custom_binaries/) │ +│ [ TFT_BOOTLOADER_NEW.bin ▾ ] │ ← только если «Другое» +│ Память платы │ +│ [ W25Q128 / W25Q64 ▾ ] │ +│ ○ Использует SDRAM (DCD) │ │ │ │ [ ▶ Загрузить ] [ ⚠ Очистить память ] [ ✕ Выйти из приложения ] │ │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ -│ │ ▶ Прошивка: firmware_test │ │ +│ │ ▶ Сборка HAB-образа (nxpimage)... │ │ +│ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ @@ -59,7 +64,25 @@ TUI не пытается восстановить прежнее состоян Лог виден постоянно (не только во время прошивки), прогресс-бар — только во время активной операции (скрыт в простое), без числового `%` — только -полоса и построчный лог в реальном времени. +полоса и построчный лог в реальном времени. Панель выбора прошивки +ограничена по высоте и скроллится сама, если разрастается (варианты +"Другое") — лог снизу гарантированно не сжимается меньше 6 строк. + +**"Другое" — для бинарников, собранных не в этом репозитории.** В +`custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без +FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до `nxpimage`). +TUI сама собирает из него загружаемый образ на лету: + +1. `nxpimage hab export` — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") +2. в Flash пишется явный FCB под выбранную память платы (не тот же + auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для + W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`) +3. образ прошивается с `0x60001000`, как обычно + +**Выбор запоминается на весь запуск TUI** — файл, память платы и DCD не +нужно выставлять заново на каждой следующей плате: прошили одну, вынули +USB, вставили следующую такую же — TUI уже подставила прошлый выбор, +остаётся нажать "Загрузить". Сбрасывается только при перезапуске TUI. ### Переход в рабочий режим (PostFlashScreen) @@ -146,11 +169,25 @@ Production/Custom этот шаг не нужен). 5. TUI автоматически попадает в DiagScreen при следующем подключении ``` +### Прошивка стороннего бинарника (custom_binaries/) + +Для плат старых ревизий и любых образов, собранных не в этом репозитории. + +```bash +1. Положить сырой бинарник (без FCB/IVT/DCD) в custom_binaries/ + (или в директорию из SERVICE_CUSTOM_BINARIES_DIR) +2. BOOT_MOD_1 → 3V3, сбросить плату → FlashScreen +3. Выбрать «Другое» → выбрать файл → выбрать память платы → DCD при необходимости +4. Загрузить — TUI сама соберёт HAB-образ и запишет правильный FCB +5. Партия одинаковых плат: вынуть USB, вставить следующую — выбор уже + подставлен, останется нажать «Загрузить» +``` + ### Chip Erase ```bash 1. Плата в SDP-режиме (BOOT_MOD_1 → 3V3) -2. FlashScreen → Очистить память (~30 с) +2. FlashScreen → Очистить память (~30 с для W25Q128, дольше для W25Q512) 3. После erase BootROM не загрузит прошивку — требуется перепрошить ``` @@ -175,6 +212,11 @@ SERVICE_CDC_PID=00ad SERVICE_M5_VID=303a SERVICE_M5_PID=4001 +# Директория с сырыми кастомными бинарниками для FlashScreen → "Другое". +# По умолчанию — custom_binaries/ рядом с исполняемым файлом (или рядом +# с main.py в dev-режиме); создаётся автоматически при старте. +# SERVICE_CUSTOM_BINARIES_DIR=/path/to/custom_binaries + # Тип сборки firmware_test для прошивки (Debug | Release). # Release временно нестабилен — по умолчанию Debug. FIRMWARE_BUILD_TYPE=Debug diff --git a/tools/production/app/app.py b/tools/production/app/app.py index f8085ca..4adfe94 100644 --- a/tools/production/app/app.py +++ b/tools/production/app/app.py @@ -17,7 +17,7 @@ from textual.binding import Binding from .firmware_client import FirmwareClient from .m5_client import M5Client -from .models import AppMode, FlashTarget +from .models import AppMode, FlashPreset, FlashTarget from .screens import DiagScreen, FlashScreen, PostFlashScreen, WaitingScreen logger = logging.getLogger(__name__) @@ -41,16 +41,21 @@ class ServiceApp(App): super().__init__() self._fw: Optional[FirmwareClient] = None self._m5: Optional[M5Client] = None + # «Липкий» выбор оператора на FlashScreen — переносится на следующую + # плату в рамках одного запуска TUI (см. FlashPreset docstring). + # Сбрасывается при перезапуске TUI, не персистится на диск. + self._last_flash_preset = FlashPreset() def on_mount(self) -> None: self.push_screen(WaitingScreen()) # ── Переходы между экранами ─────────────────────────────────────────────── + @on(WaitingScreen.DeviceDetected) @on(WaitingScreen.DeviceDetected) def _on_device_detected(self, event: WaitingScreen.DeviceDetected) -> None: if event.mode == AppMode.FLASHING: - self.switch_screen(FlashScreen()) + self.switch_screen(FlashScreen(preset=self._last_flash_preset)) elif event.mode == AppMode.DIAGNOSING: self._connect_and_diagnose() @@ -63,6 +68,9 @@ class ServiceApp(App): - target=None — маркер потери соединения (см. ConnectionWatcherMixin) → WaitingScreen с явной причиной возврата """ + if event.preset is not None: + self._last_flash_preset = event.preset + if event.target is None and not event.success: self.switch_screen( WaitingScreen(disconnect_reason="Соединение с платой потеряно") diff --git a/tools/production/app/app.tcss b/tools/production/app/app.tcss index 621a2ae..caaf5cb 100644 --- a/tools/production/app/app.tcss +++ b/tools/production/app/app.tcss @@ -16,7 +16,7 @@ AppFrame { width: 100%; height: 100%; max-width: 112; - max-height: 35; + max-height: 40; border: heavy $primary; background: $surface; padding: 1 2; @@ -88,19 +88,39 @@ AppFrame { padding: 1 2; margin-bottom: 1; height: auto; + max-height: 18; + overflow-y: auto; } -#flash-custom-path { +#flash-target-group .section-title { + text-style: bold; + color: $text-muted; + padding: 0 0 1 0; +} + +#flash-custom-group { margin-top: 1; height: auto; } -#flash-custom-path.hidden { +#flash-custom-group.hidden { display: none; } -#flash-custom-input { +#flash-custom-select, +#flash-fcb-select { width: 1fr; + margin-bottom: 1; +} + +#flash-dcd-row { + height: auto; + align: left middle; + margin-top: 1; +} + +#flash-dcd-switch { + margin-right: 1; } #flash-btn-row { @@ -131,6 +151,7 @@ AppFrame { #flash-log { height: 1fr; + min-height: 6; margin-top: 1; border: round $panel; background: $surface-darken-1; diff --git a/tools/production/app/flasher.py b/tools/production/app/flasher.py index 3d260b2..ef041c2 100644 --- a/tools/production/app/flasher.py +++ b/tools/production/app/flasher.py @@ -19,10 +19,12 @@ import asyncio import logging import os import re +import sys +import tempfile from pathlib import Path from typing import Awaitable, Callable, Optional -from .models import FlashProgress, FlashTarget +from .models import FcbVariant, FlashProgress, FlashTarget logger = logging.getLogger(__name__) @@ -39,6 +41,31 @@ _FIRMWARE_BUILD_TYPE = os.environ.get("FIRMWARE_BUILD_TYPE", "Debug") # Путь до flash_usb.py относительно корня репозитория _FLASH_USB_SCRIPT = Path(__file__).parents[3] / "tools" / "host" / "flash_usb.py" _HOST_TOOLS_DIR = _FLASH_USB_SCRIPT.parent +_HAB_DIR = _HOST_TOOLS_DIR / "hab" +_DCD_DIR = _HOST_TOOLS_DIR / "dcd" + + +def _resolve_custom_binaries_dir() -> Path: + """ + Директория с «сырыми» кастомными бинарниками для FlashScreen. + + Не пакуется в PyInstaller-бандл — внешняя директория, путь к которой + можно переопределить через SERVICE_CUSTOM_BINARIES_DIR. sys.executable + указывает на реальный exe и для --onefile, и для --onedir (в отличие + от sys._MEIPASS — временной распаковки onefile). + """ + override = os.environ.get("SERVICE_CUSTOM_BINARIES_DIR") + if override: + base = Path(override) + elif getattr(sys, "frozen", False): + base = Path(sys.executable).resolve().parent / "custom_binaries" + else: + base = Path(__file__).parents[1] / "custom_binaries" + base.mkdir(parents=True, exist_ok=True) + return base + + +CUSTOM_BINARIES_DIR = _resolve_custom_binaries_dir() # Паттерны stdout flash_usb.py для извлечения прогресса _RE_PERCENT = re.compile(r"(\d{1,3})\s*%") @@ -108,6 +135,11 @@ class Flasher: """True если виден CDC firmware_test (1996:00AD).""" return _detect_usb(_CDC_VID, _CDC_PID) + @staticmethod + def list_custom_binaries() -> list[Path]: + """Отсканировать custom_binaries/ на *.bin, отсортировано по имени.""" + return sorted(CUSTOM_BINARIES_DIR.glob("*.bin")) + # ── Erase ──────────────────────────────────────────────────────────────── async def erase_chip( @@ -138,6 +170,8 @@ class Flasher: target: FlashTarget, progress_cb: Optional[ProgressCallback] = None, bin_path: Optional[Path] = None, + use_dcd: bool = False, + fcb_variant: FcbVariant = FcbVariant.W25Q128, ) -> bool: """ Запустить прошивку через flash_usb.py. @@ -145,6 +179,10 @@ class Flasher: :param target: Что прошиваем (firmware_test, production или custom). :param progress_cb: Async callback с FlashProgress (может быть None). :param bin_path: Путь к бинарю (обязателен для CUSTOM). + :param use_dcd: Только для CUSTOM — включить DCDFilePath (SDRAM-init) + при сборке HAB-образа через nxpimage. + :param fcb_variant: Только для CUSTOM — какой явный FCB-блоб (dcd/*_fdcb.bin) + записать в Flash[0x60000000] вместо auto-config. :return: True при успехе. """ if target == FlashTarget.FIRMWARE_TEST: @@ -159,9 +197,144 @@ class Flasher: elif target == FlashTarget.CUSTOM: if bin_path is None: raise ValueError("FlashTarget.CUSTOM требует bin_path") - return await self._run_flash_bin(bin_path, progress_cb) + return await self._run_flash_custom( + bin_path, use_dcd, fcb_variant, progress_cb + ) return False + async def _run_flash_custom( + self, + raw_bin_path: Path, + use_dcd: bool, + fcb_variant: FcbVariant, + progress_cb: Optional[ProgressCallback], + ) -> bool: + """ + Прошить «сырой» (не-HAB) кастомный бинарник из custom_binaries/. + + Два шага: + 1. Собрать HAB-образ (IVT + опционально DCD, БЕЗ FCB) через + nxpimage — так же, как just build::hab-* собирает штатные + прошивки, только конфиг генерируется на лету под выбранный файл. + 2. Прошить получившийся HAB-образ через flash_usb.py --bin-path, + подставив явный FCB-блоб (--fcb-path) под выбранный тип памяти — + см. FcbVariant. + """ + if progress_cb is not None: + await progress_cb( + FlashProgress( + phase="nxpimage", percent=0, message="Сборка HAB-образа..." + ) + ) + + hab_bin = await self._build_custom_hab(raw_bin_path, use_dcd, progress_cb) + if hab_bin is None: + if progress_cb is not None: + await progress_cb( + FlashProgress( + phase="error", + percent=0, + message="Ошибка сборки HAB-образа (nxpimage)", + ) + ) + return False + + fcb_path = _DCD_DIR / fcb_variant.fcb_filename + try: + cmd = [ + "uv", + "run", + "--directory", + str(_HOST_TOOLS_DIR), + "python", + str(_FLASH_USB_SCRIPT), + "--bin-path", + str(hab_bin), + "--fcb-path", + str(fcb_path), + ] + return await self._run_cmd(cmd, raw_bin_path.stem, progress_cb) + finally: + hab_bin.unlink(missing_ok=True) + + async def _build_custom_hab( + self, + raw_bin: Path, + use_dcd: bool, + progress_cb: Optional[ProgressCallback] = None, + ) -> Optional[Path]: + """ + Собрать HAB-образ из сырого бинарника через nxpimage. + + Временный YAML пишется прямо в tools/host/hab/ (как и штатные + hab_*.yaml) и nxpimage запускается с cwd=tools/host/hab/ — это + обязательно: относительный DCDFilePath ("../dcd/dcd.bin") в + существующих конфигах резолвится именно так (см. build.just, + группа hab_image_gen — `cd tools/host/hab && uv run nxpimage ...`). + Отходить от этой схемы рискованно — nxpimage не документирует + явно, от чего резолвит относительные пути. + + :return: путь к собранному *.hab.bin, либо None при ошибке nxpimage. + """ + yaml_f = tempfile.NamedTemporaryFile( + dir=_HAB_DIR, suffix=".yaml", prefix="_tui_custom_", delete=False + ) + yaml_path = Path(yaml_f.name) + out_path = yaml_path.with_suffix(".hab.bin") + + lines = [ + "options:", + " flags: 0x00", + " startAddress: 0x60000000", + " ivtOffset: 0x1000", + " initialLoadSize: 0x2000", + " family: mimxrt1050", + ] + if use_dcd: + lines.append(" DCDFilePath: ../dcd/dcd.bin") + lines.append(f'inputImageFile: "{raw_bin.resolve()}"') + lines.append("sections: []") + + try: + yaml_f.write("\n".join(lines).encode("utf-8")) + yaml_f.close() + + cmd = [ + "uv", + "run", + "nxpimage", + "hab", + "export", + "--force", + "-c", + str(yaml_path), + "-o", + str(out_path), + ] + logger.info("Building custom HAB image: %s", " ".join(cmd)) + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=str(_HAB_DIR), + ) + assert proc.stdout is not None + async for raw_line in proc.stdout: + line = raw_line.decode("utf-8", errors="replace").rstrip() + logger.debug("nxpimage: %s", line) + if progress_cb is not None and line: + await progress_cb( + FlashProgress(phase="nxpimage", percent=0, message=line) + ) + await proc.wait() + + if proc.returncode != 0 or not out_path.exists(): + logger.error("nxpimage hab export failed (rc=%s)", proc.returncode) + return None + return out_path + finally: + yaml_path.unlink(missing_ok=True) + async def _run_flash( self, firmware: str, diff --git a/tools/production/app/models.py b/tools/production/app/models.py index 519b3c8..cdf4a60 100644 --- a/tools/production/app/models.py +++ b/tools/production/app/models.py @@ -37,6 +37,49 @@ class FlashTarget(Enum): CUSTOM = "custom" # произвольный HAB-бинарь, путь задаётся отдельно +class FcbVariant(str, Enum): + """Вариант FCB для кастомных бинарей. + + W25Q128/W25Q64 (3-байтная адресация) и W25Q256/W25Q512 (4-байтная) + сведены к двум случаям — см. обсуждение прошивки старых плат. + """ + + W25Q128 = "w25q128" + W25Q512 = "w25q512" + + @property + def fcb_filename(self) -> str: + """Имя файла в tools/host/dcd/, соответствующее варианту.""" + return f"{self.value}_fdcb.bin" + + @property + def display_name(self) -> str: + return { + FcbVariant.W25Q128: "W25Q128 / W25Q64", + FcbVariant.W25Q512: "W25Q256 / W25Q512", + }[self] + + +@dataclass +class FlashPreset: + """«Липкий» выбор оператора на FlashScreen. + + Живёт в памяти ServiceApp (не на диске), переносится на следующую + плату в рамках одного запуска TUI — чтобы не выбирать заново файл + и опции при прошивке партии одинаковых плат. Сбрасывается при + перезапуске TUI. Обновляется в момент нажатия «Загрузить» (не только + при успехе — неудача чаще всего про USB-кабель, а не про то, что + выбор был неверным). + + DCD/FCB-поля имеют смысл только при target == FlashTarget.CUSTOM. + """ + + target: FlashTarget = FlashTarget.FIRMWARE_TEST + custom_bin_name: Optional[str] = None + use_dcd: bool = False + fcb_variant: FcbVariant = FcbVariant.W25Q128 + + @dataclass(frozen=True) class TestInfo: """Метаданные теста из list_tests.""" diff --git a/tools/production/app/screens/flash.py b/tools/production/app/screens/flash.py index 96c3782..9ba872b 100644 --- a/tools/production/app/screens/flash.py +++ b/tools/production/app/screens/flash.py @@ -20,16 +20,17 @@ from textual.message import Message from textual.screen import Screen from textual.widgets import ( Button, - Input, Label, Log, ProgressBar, RadioButton, RadioSet, + Select, + Switch, ) -from ..flasher import Flasher -from ..models import FlashProgress, FlashTarget +from ..flasher import CUSTOM_BINARIES_DIR, Flasher +from ..models import FcbVariant, FlashPreset, FlashProgress, FlashTarget from ..widgets import AppFrame from .connection_watcher import ConnectionLost, ConnectionWatcherMixin @@ -56,15 +57,22 @@ class FlashScreen(Screen, ConnectionWatcherMixin): ] class FlashDone(Message): - def __init__(self, success: bool, target: Optional[FlashTarget] = None) -> None: + def __init__( + self, + success: bool, + target: Optional[FlashTarget] = None, + preset: Optional[FlashPreset] = None, + ) -> None: super().__init__() self.success = success self.target = target + self.preset = preset - def __init__(self, **kwargs) -> None: + def __init__(self, preset: Optional[FlashPreset] = None, **kwargs) -> None: super().__init__(**kwargs) self._flasher = Flasher() self._flashing = False + self._preset = preset or FlashPreset() def compose(self) -> ComposeResult: with AppFrame(id="flash-frame"): @@ -79,21 +87,37 @@ class FlashScreen(Screen, ConnectionWatcherMixin): yield RadioButton( "Диагностическая прошивка (firmware_test)", id="radio-fw-test", - value=True, + value=self._preset.target == FlashTarget.FIRMWARE_TEST, ) yield RadioButton( "Серийная прошивка (bootloader + tft_app)", id="radio-production", + value=self._preset.target == FlashTarget.PRODUCTION, ) yield RadioButton( "Другое", id="radio-custom", + value=self._preset.target == FlashTarget.CUSTOM, ) - with Horizontal(id="flash-custom-path", classes="hidden"): - yield Input( - placeholder="Имя бинарного файла в custom_binaries/ (.bin)", - id="flash-custom-input", + is_custom = self._preset.target == FlashTarget.CUSTOM + with Vertical( + id="flash-custom-group", + classes="" if is_custom else "hidden", + ): + yield Label("Файл (custom_binaries/)", classes="section-title") + yield Select[str]( + [], id="flash-custom-select", prompt="Выберите файл..." ) + yield Label("Память платы", classes="section-title") + yield Select[str]( + [(v.display_name, v.value) for v in FcbVariant], + id="flash-fcb-select", + value=self._preset.fcb_variant.value, + allow_blank=False, + ) + with Horizontal(id="flash-dcd-row"): + yield Switch(value=self._preset.use_dcd, id="flash-dcd-switch") + yield Label("Использует SDRAM (DCD)", classes="section-title") with Horizontal(id="flash-btn-row"): yield Button("▶ Загрузить", id="flash-btn-flash", variant="warning") @@ -112,6 +136,17 @@ class FlashScreen(Screen, ConnectionWatcherMixin): def on_mount(self) -> None: self._start_connection_watch(self._check_sdp_present) + self._populate_custom_select() + + def _populate_custom_select(self) -> None: + select = self.query_one("#flash-custom-select", Select) + names = [p.name for p in Flasher.list_custom_binaries()] + select.set_options([(name, name) for name in names]) + if not names: + self._log(f"⚠ Пусто: {CUSTOM_BINARIES_DIR}") + return + if self._preset.custom_bin_name in names: + select.value = self._preset.custom_bin_name def on_unmount(self) -> None: self._stop_connection_watch() @@ -132,11 +167,11 @@ class FlashScreen(Screen, ConnectionWatcherMixin): @on(RadioSet.Changed, "#flash-radio") def _on_radio_changed(self, event: RadioSet.Changed) -> None: is_custom = event.pressed.id == "radio-custom" - path_row = self.query_one("#flash-custom-path") + group = self.query_one("#flash-custom-group") if is_custom: - path_row.remove_class("hidden") + group.remove_class("hidden") else: - path_row.add_class("hidden") + group.add_class("hidden") @on(Button.Pressed, "#flash-btn-flash") def _on_flash_pressed(self) -> None: @@ -144,9 +179,20 @@ class FlashScreen(Screen, ConnectionWatcherMixin): return target, bin_path = self._resolve_target() if target is None: - self._log("⚠ Укажите корректное имя бинарного файла") + self._log("⚠ Выберите файл в custom_binaries/") return - self._do_flash(target, bin_path) + + if target == FlashTarget.CUSTOM: + preset = FlashPreset( + target=target, + custom_bin_name=bin_path.name, + use_dcd=self._current_use_dcd(), + fcb_variant=self._current_fcb_variant(), + ) + else: + preset = FlashPreset(target=target) + + self._do_flash(target, bin_path, preset) @on(Button.Pressed, "#flash-btn-erase") def _on_erase_pressed(self) -> None: @@ -165,19 +211,23 @@ class FlashScreen(Screen, ConnectionWatcherMixin): # ── Workers ─────────────────────────────────────────────────────────────── @work(exclusive=True, thread=False) - async def _do_flash(self, target: FlashTarget, bin_path: Optional[Path]) -> None: + async def _do_flash( + self, target: FlashTarget, bin_path: Optional[Path], preset: FlashPreset + ) -> None: self._set_busy(True) self._show_progress(True) self._log(f"▶ Прошивка: {target.value}") ok = await self._flasher.flash( target=target, bin_path=bin_path, + use_dcd=preset.use_dcd, + fcb_variant=preset.fcb_variant, progress_cb=self._on_progress, ) self._set_busy(False) self._finish_progress(ok) self._log("✅ Готово" if ok else "❌ Ошибка") - self.post_message(self.FlashDone(success=ok, target=target)) + self.post_message(self.FlashDone(success=ok, target=target, preset=preset)) @work(exclusive=True, thread=False) async def _do_erase(self) -> None: @@ -200,16 +250,22 @@ class FlashScreen(Screen, ConnectionWatcherMixin): if pressed_id == "radio-production": return FlashTarget.PRODUCTION, None if pressed_id == "radio-custom": - raw = self.query_one("#flash-custom-input", Input).value.strip() - if not raw: + name = self.query_one("#flash-custom-select", Select).value + if name is None or name is Select.BLANK: return None, None - p = Path(raw) + p = CUSTOM_BINARIES_DIR / str(name) if not p.exists(): self._log(f"⚠ Файл не найден: {p}") return None, None return FlashTarget.CUSTOM, p return None, None + def _current_fcb_variant(self) -> FcbVariant: + return FcbVariant(self.query_one("#flash-fcb-select", Select).value) + + def _current_use_dcd(self) -> bool: + return self.query_one("#flash-dcd-switch", Switch).value + async def _on_progress(self, progress: FlashProgress) -> None: bar = self.query_one("#flash-progress-bar", ProgressBar) bar.update(total=100, progress=progress.percent) diff --git a/tools/production/custom_binaries/TFT_BOOTLOADER_NEW.bin b/tools/production/custom_binaries/TFT_BOOTLOADER_NEW.bin new file mode 100644 index 0000000000000000000000000000000000000000..336c7aec67d9fb13fca720414cb0302bde5b5ad5 GIT binary patch literal 39676 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 CUaH*y literal 0 HcmV?d00001 diff --git a/tools/production/custom_binaries/TFT_BOOTLOADER_OLD.bin b/tools/production/custom_binaries/TFT_BOOTLOADER_OLD.bin new file mode 100644 index 0000000000000000000000000000000000000000..f5a469290f8312405ba9678b4fc812dd15610daf GIT binary patch literal 47432 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 SCNANwBw literal 0 HcmV?d00001 diff --git a/tools/production/docs/DEV_ARCH.md b/tools/production/docs/DEV_ARCH.md index 821a1a7..8e8563f 100644 --- a/tools/production/docs/DEV_ARCH.md +++ b/tools/production/docs/DEV_ARCH.md @@ -17,6 +17,8 @@ tools/production/ ├── main.py ← точка входа (10 строк) ├── pyproject.toml ← зависимости uv ├── uv.lock +├── custom_binaries/ ← runtime, gitignored, создаётся автоматически +│ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое» └── app/ ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов ├── app.tcss ← единый файл стилей для всех экранов @@ -202,7 +204,9 @@ flowchart TD - При срабатывании — `ConnectionLost` message → экран постит `FlashDone(success=False, target=None)` / `DiagDone(reason=...)` → `ServiceApp` разрывает сессию (`FirmwareClient.disconnect()`) и переключает - на `WaitingScreen(disconnect_reason=...)`. + на `WaitingScreen(disconnect_reason=...)`. `FlashDone` в этой ветке не несёт + `preset` — «липкий» выбор (см. §8) сохраняется отдельно, в момент нажатия + «Загрузить», а не при завершении прошивки. - `WaitingScreen` показывает причину возврата баннером на 4 секунды, затем продолжает обычный автодетект. @@ -228,7 +232,6 @@ AppFrame { ``` Решает две задачи: - 1. **Визуальная консистентность** — одна и та же рамка на всех экранах. 2. **Устраняет краш Textual 8.x** при mouse drag (`assert isinstance(content_widget.parent, Widget)`) — раньше `Screen` мог @@ -244,7 +247,110 @@ AppFrame { --- -## 8. Архитектура экранов +## 8. Прошивка кастомных бинарников и «липкий» выбор (FlashPreset) + +### 8.1 Проблема + +Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются +`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 — +для них auto-config Flashloader (`configure-memory 0xC0000007` → +`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси +бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader +не документирован как надёжный для 4-байтной адресации, а сами бинарники +приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат, +что `inputImageFile` в `hab_*.yaml` до сборки). Решение — собирать HAB +на лету и писать FCB явно, а не полагаться на auto-config. + +### 8.2 Модели (`models.py`) + +```python +class FcbVariant(str, Enum): + W25Q128 = "w25q128" # 3-байтная адресация — auto-config работал бы, + W25Q512 = "w25q512" # но пишем явно и здесь, для единообразия пути + # W25Q64/W25Q256 сведены к этим двум случаям — см. обсуждение + +@dataclass +class FlashPreset: + target: FlashTarget = FlashTarget.FIRMWARE_TEST + custom_bin_name: Optional[str] = None + use_dcd: bool = False + fcb_variant: FcbVariant = FcbVariant.W25Q128 +``` + +`FlashPreset` — «липкий» выбор оператора, живёт в `ServiceApp._last_flash_preset` +(память процесса, не диск). Захватывается в `FlashScreen._on_flash_pressed()` +**в момент нажатия «Загрузить»**, не только при успехе — неудача чаще всего +про физическое соединение, а не про то, что выбор был неверным. Передаётся +в конструктор следующего `FlashScreen` через `FlashDone.preset` → +`ServiceApp._on_flash_done()`. Решает конкретную задачу: прошивка партии +одинаковых плат подряд — вставил, TUI уже подставила прошлый выбор файла/ +памяти/DCD, нажал «Загрузить», вынул, вставил следующую. + +Рассматривался отдельный режим «массовое программирование» (авто-прошивка +по факту детекта SDP, без нажатия кнопки на каждую плату) — отклонён: +в SDP/Flashloader-режиме нет способа прочитать UID платы, авто-старт без +подтверждения оператора убирает последний шанс заметить, что в руках не та +плата. Оставлена только «липкая» память выбора (этот раздел). + +### 8.3 Конвейер сборки (`flasher.py`) + +``` +Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) + └── _run_flash_custom() + ├── _build_custom_hab(raw_bin, use_dcd, progress_cb) + │ ├── генерирует temp .yaml в tools/host/hab/ (по образцу hab_bootloader_*.yaml: + │ │ startAddress=0x60000000, ivtOffset=0x1000, initialLoadSize=0x2000, + │ │ family=mimxrt1050, + DCDFilePath: ../dcd/dcd.bin если use_dcd) + │ ├── uv run nxpimage hab export --force -c -o , + │ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath + │ │ резолвится от этой директории, как в build.just) + │ └── стриминг stdout nxpimage в progress_cb (не только logger.debug — + │ иначе во время сборки лог FlashScreen выглядит «зависшим») + └── flash_usb.py --bin-path --fcb-path tools/host/dcd/{fcb_variant}_fdcb.bin + (временный .yaml и собранный HAB-образ удаляются после прошивки) +``` + +`dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не +зависит от того, что именно исполняется), поэтому просто константный путь, +без вариантов. + +### 8.4 `flash_usb.py` — явная запись FCB вместо auto-config + +```python +def write_fcb_explicit(fcb_path: Path) -> None: + """write-memory 0x60000000 — буквальная запись 512-байтного + FCB-блоба (tag 'FCFB'), а не magic option word 0xF000000F. + Обязателен для кастомных бинарей — auto-config Flashloader проверен + только для W25Q128.""" +``` + +Активируется флагом `--fcb-path` (только вместе с `--bin-path`). Штатный +`--firmware`-путь (три сборки из `BUILD_DIR`) не тронут: без `--fcb-path` +поведение идентично тому, что было до этой доработки. + +Заодно увеличен таймаут `blhost` для `flash-erase-all` (chip erase) — +`-t 200000` вместо дефолтного: W25Q512 стирается заметно дольше W25Q128, +дефолтного таймаута `blhost` не хватало. `flash-erase-region` (стирание +пары секторов под FCB+HAB при обычной прошивке) не трогали — там масштаб +на порядки меньше, дефолта достаточно независимо от чипа. + +### 8.5 UI (`flash.py`) + +При выборе радиокнопки «Другое» появляется `Vertical#flash-custom-group`: +`Select` по содержимому `custom_binaries/` (пересканируется в `on_mount()`), +`Select` по `FcbVariant`, `Switch` DCD. Выбор любой ДРУГОЙ радиокнопки в том +же `RadioSet` автоматически скрывает группу — отдельного «Назад» не +потребовалось, это штатное поведение взаимоисключающего `RadioSet`. + +`#flash-target-group` ограничена `max-height: 18` с собственным скроллом — +без этого разросшаяся custom-группа (два `Select` + `Switch`) на маленьком +терминале выталкивала `#flash-log` почти до нулевой высоты. `#flash-log` +дополнительно защищён `min-height: 6` — лог гарантированно виден даже в +худшем случае. + +--- + +## 9. Архитектура экранов ```mermaid graph TB @@ -292,7 +398,7 @@ graph TB --- -## 9. Жизненный цикл диагностической сессии +## 10. Жизненный цикл диагностической сессии ```mermaid sequenceDiagram @@ -365,7 +471,7 @@ sequenceDiagram --- -## 10. Версионирование firmware +## 11. Версионирование firmware `firmware_test` версионируется через CMake (`project(firmware_test VERSION X.Y.Z)`), генерирует `version.h` через @@ -382,7 +488,7 @@ sequenceDiagram --- -## 11. Логотип (`boot_art.py`) +## 12. Логотип (`boot_art.py`) `LOGO_ART` — Rich-markup строка (29×21 символов, цвета `#3ca0dc` для синей части логотипа, `white` для тёмной, `grey37` для фоновых точек), полученная @@ -400,7 +506,7 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл --- -## 12. Известные грабли Textual 8.x +## 13. Известные грабли Textual 8.x Зафиксировано на практике — экономит время при будущих доработках: @@ -451,3 +557,14 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл архитектурным решением, а не техдолгом. - Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и «копирование UID с экрана» — отложены, не начаты. +- **Массовое программирование** — решено НЕ делать авто-прошивку по факту + детекта SDP (см. §8.2); ограничились «липким» `FlashPreset`. Если в будущем + понадобится полный батч-режим — потребуется отдельный предохранитель + (задержка с отменой перед стартом), т.к. в SDP-режиме плату нельзя + идентифицировать по UID. +- **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили + не полагаться на него вообще, для кастомных бинарей FCB всегда пишется + явно (`--fcb-path`, см. §8.4). Остаётся не до конца понятым, работает ли + `configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос + снят с повестки архитектурным решением, а не исследован до конца. + \ No newline at end of file diff --git a/tools/production/pyproject.toml b/tools/production/pyproject.toml index f319c69..20b5d72 100644 --- a/tools/production/pyproject.toml +++ b/tools/production/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "service-tui" -version = "0.1.1" +version = "0.1.0" description = "TUI сервисного инженера для диагностики платы MIMXRT1052" requires-python = ">=3.11" dependencies = [ diff --git a/tools/production/uv.lock b/tools/production/uv.lock index e4fccb2..b041381 100644 --- a/tools/production/uv.lock +++ b/tools/production/uv.lock @@ -200,7 +200,7 @@ wheels = [ [[package]] name = "service-tui" -version = "0.1.1" +version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "pyinstaller" },