diff --git a/.env.example b/.env.example index fd35c23..2b70b39 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,8 @@ FLASHLOADER_VID=15a2 FLASHLOADER_PID=0073 SERVICE_CDC_VID=1996 SERVICE_CDC_PID=00ad - +SERVICE_M5_VID=303a +SERVICE_M5_PID=4001 # --- Paths --- # BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile # через justfile_directory(), здесь можно переопределить если нужно @@ -61,4 +62,4 @@ HIL_USB_CDC_BAUD=115200 HIL_USB_CDC_TIMEOUT=5.0 # Тип сборки firmware_test для TUI (Debug | Release) -FIRMWARE_BUILD_TYPE=Release \ No newline at end of file +FIRMWARE_BUILD_TYPE=Debug \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bf8b674 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,261 @@ +name: Release + +# Раздельные теги (FIRST_RELEASE_PLAN.md, Шаг 2.1): firmware_test и +# service-tui версионируются и релизятся независимо друг от друга. +# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug) +# tui-vX.Y.Z → publish-tui (service-tui-vX.Y.Z-{macos,windows}.zip) +# HAB firmware_test, вшиваемый в TUI-бандл, всегда собирается заново из +# текущего HEAD тега tui-v*, а не берётся из отдельного firmware-релиза — +# так проще и не тянет зависимость на чужой GitHub Release. +on: + push: + tags: + - "tui-v*" + - "firmware-v*" + workflow_dispatch: + inputs: + release_type: + description: "Тестовый прогон без публикации (job publish-* не запускается вне push тега)" + type: choice + options: + - tui + - firmware + default: tui + +concurrency: + group: release-${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write + +jobs: + # ───────────────────────────────────────────────────────────────────────── + # firmware — собирает HAB Debug firmware_test. Нужна как для standalone + # firmware-релиза, так и для вшивания в TUI-бандл — выполняется всегда. + # ───────────────────────────────────────────────────────────────────────── + firmware: + name: Build firmware_test HAB (Debug) + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build devcontainer image with cache + uses: docker/build-push-action@v7 + with: + context: . + file: .devcontainer/Dockerfile + tags: tft-devcontainer-ci:latest + load: true + cache-from: type=gha,scope=tft-devcontainer + cache-to: type=gha,mode=max,scope=tft-devcontainer + + - name: Sync host Python tools inside container + run: | + docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \ + tft-devcontainer-ci:latest bash -lc 'cd tools/host && uv sync' + + - name: Проверить, что тег совпадает с версией в CMakeLists.txt + if: startsWith(github.ref, 'refs/tags/firmware-v') + run: | + tag_version="${GITHUB_REF_NAME#firmware-v}" + file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/test/CMakeLists.txt | awk '{print $2}') + if [[ "$tag_version" != "$file_version" ]]; then + echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/test/CMakeLists.txt (${file_version})" + exit 1 + fi + echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}" + + - name: Собрать firmware_test HAB (Debug) + run: | + docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \ + tft-devcontainer-ci:latest bash -lc 'just build::hab-firmware-test-debug' + + - name: Upload firmware_test_hab.bin + uses: actions/upload-artifact@v4 + with: + name: firmware-hab-debug + path: build/Debug/firmware_test_hab.bin + if-no-files-found: error + retention-days: 14 + + # ───────────────────────────────────────────────────────────────────────── + # publish-firmware — только на тег firmware-v*, отдельный standalone-релиз + # HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI). + # ───────────────────────────────────────────────────────────────────────── + publish-firmware: + name: Publish firmware release + needs: firmware + if: startsWith(github.ref, 'refs/tags/firmware-v') + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download firmware_test_hab.bin + uses: actions/download-artifact@v4 + with: + name: firmware-hab-debug + path: release-assets/ + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + release-assets/firmware_test_hab.bin \ + --title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \ + --generate-notes + + # ───────────────────────────────────────────────────────────────────────── + # service-tui-{macos,windows} — упаковка PyInstaller-бандла с вшитым HAB + # из job firmware. Запускается на тег tui-v* и на workflow_dispatch с + # release_type=tui (для тестового прогона без публикации). + # ───────────────────────────────────────────────────────────────────────── + service-tui-macos: + name: Package service-tui (macOS) + needs: firmware + if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui') + runs-on: macos-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически + + - name: Install just + uses: extractions/setup-just@v4 + + - name: Download firmware_test_hab.bin + uses: actions/download-artifact@v4 + with: + name: firmware-hab-debug + path: build/Debug/ + + - name: Проверить, что тег совпадает с версией в pyproject.toml + if: startsWith(github.ref, 'refs/tags/tui-v') + run: | + tag_version="${GITHUB_REF_NAME#tui-v}" + file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/') + if [[ "$tag_version" != "$file_version" ]]; then + echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})" + exit 1 + fi + echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}" + + - name: Собрать зависимости и упаковать бандл + run: | + just host::service-setup + just host::package-tui + + - name: Заархивировать бандл + working-directory: tools/service_tui/dist + run: | + name=$(ls -d service-tui-v*-macos) + zip -r "${name}.zip" "${name}" + + - name: Upload bundle + uses: actions/upload-artifact@v4 + with: + name: service-tui-macos + path: tools/service_tui/dist/service-tui-v*-macos.zip + if-no-files-found: error + retention-days: 14 + + service-tui-windows: + name: Package service-tui (Windows) + needs: firmware + if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui') + runs-on: windows-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически + + - name: Install just + uses: extractions/setup-just@v4 + + - name: Download firmware_test_hab.bin + uses: actions/download-artifact@v4 + with: + name: firmware-hab-debug + path: build/Debug/ + + - name: Проверить, что тег совпадает с версией в pyproject.toml + if: startsWith(github.ref, 'refs/tags/tui-v') + shell: bash + run: | + tag_version="${GITHUB_REF_NAME#tui-v}" + file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/') + if [[ "$tag_version" != "$file_version" ]]; then + echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})" + exit 1 + fi + echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}" + + - name: Собрать зависимости и упаковать бандл + shell: bash + run: | + just host::service-setup + just host::package-tui + + - name: Заархивировать бандл + working-directory: tools/service_tui/dist + shell: pwsh + run: | + $name = (Get-ChildItem -Directory -Filter "service-tui-v*-windows").Name + Compress-Archive -Path $name -DestinationPath "$name.zip" + + - name: Upload bundle + uses: actions/upload-artifact@v4 + with: + name: service-tui-windows + path: tools/service_tui/dist/service-tui-v*-windows.zip + if-no-files-found: error + retention-days: 14 + + # ───────────────────────────────────────────────────────────────────────── + # publish-tui — только на тег tui-v*, релиз с обоими zip-бандлами. + # ───────────────────────────────────────────────────────────────────────── + publish-tui: + name: Publish service-tui release + needs: [service-tui-macos, service-tui-windows] + if: startsWith(github.ref, 'refs/tags/tui-v') + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download bundles + uses: actions/download-artifact@v4 + with: + pattern: service-tui-* + path: release-assets/ + merge-multiple: true + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + release-assets/*.zip \ + --title "service-tui ${GITHUB_REF_NAME#tui-v}" \ + --generate-notes diff --git a/CHANGELOG.md b/CHANGELOG.md index 753c485..93eb8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,11 +191,6 @@ ### Известные ограничения -- `service_tui.spec` не включает `datas` для `spsdk`/`dcd/*.bin`/ - `pyproject.toml`, хотя уже собранные alpha-бандлы их содержат — спек - нужно синхронизировать перед следующей сборкой релиза. -- `pyusb` в `pyproject.toml` — мёртвая зависимость (Р7 перевёл детект на - `spsdk`/`serial.tools.list_ports`), кандидат на удаление. - Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили не полагаться на него вообще, FCB для кастомных бинарей всегда пишется явно. diff --git a/FIRST_RELEASE_PLAN.md b/FIRST_RELEASE_PLAN.md deleted file mode 100644 index ba260ef..0000000 --- a/FIRST_RELEASE_PLAN.md +++ /dev/null @@ -1,226 +0,0 @@ -# План первого релиза в GitHub - -> Цель: в GitHub Releases появляется тег с прикреплёнными бинарниками — -> `firmware_test` (HAB-образ) и `service-tui` (standalone-бандлы под macOS и -> Windows). Этот документ — практический чек-лист «как довести до кнопки -> Publish», а не повторение инженерных фаз `RELEASE_ROADMAP.md`. - -## Как связаны два артефакта - -`firmware_test` (C, i.MX RT1052, версия `0.1.2` из -`firmware/test/CMakeLists.txt`) и `service-tui` (Python/Textual, версия -`0.2.0` из `tools/service_tui/pyproject.toml`) — независимо версионируемые -проекты, но релиз одного без другого бесполезен сервисному инженеру: -`service-tui` — это инструмент, которым он *прошивает* плату диагностической -прошивкой, и HAB-образ `firmware_test` кладётся внутрь бандла TUI как -`firmware//firmware_test_hab.bin` (см. `just host::package-tui`, -`tools/service_tui/docs/DEV_ARCH.md` §14). Поэтому релиз собирается как один -комплект, даже если версии независимые. - -Известное ограничение (задокументировано в `README.md`/`DEV_ARCH.md`): -Release-сборка `firmware_test` нестабильна (проблема с FCB/clock), поэтому -в бандл TUI кладётся **Debug**-образ (`FIRMWARE_BUILD_TYPE=Debug`). В релиз -GitHub имеет смысл положить оба HAB-образа отдельными assets (Debug — как -основной для TUI, Release — с пометкой «experimental», для тех, кто прошивает -через `tools/host/flash_usb.py` вручную), либо только Debug — см. открытый -вопрос в шаге 1.5. - ---- - -## Текущее состояние (снимок на момент написания плана) - -| Область | Состояние | -| --- | --- | -| `firmware_test` | Собирается, HAB-образ генерируется (`just build::hab-firmware-test-{debug,release}`), Release нестабилен | -| `service-tui` | v0.2.0, PyInstaller onedir, alpha-бандлы уже вручную собраны и прогнаны на живом железе macOS+Windows (коммиты `c694258`/`bfe4dd6`) | -| `service_tui.spec` | **Устарел относительно того, чем реально собраны протестированные alpha-бандлы** — не содержит `datas` для `spsdk`, `dcd/*.bin`, `pyproject.toml` (задокументировано в `DEV_ARCH.md` §14 и `CHANGELOG.md`「Известные ограничения」) | -| `tools/service_tui/dist/service-tui-v0.2.0-{macos,windows}/` | Закоммичены в git (906 файлов, ~96 МБ суммарно) и **устарели относительно HEAD** — собраны до коммитов `2dbe3e6`/`22c4077`/`31e3237` (фиксы моков тестов, рефакторинг докстрингов) | -| CI (`.github/workflows/ci.yml`) | Только `build`+`test` в devcontainer на `ubuntu-latest`; не собирает `service-tui`, нет macOS/Windows раннеров, нет release-пайплайна, нет тегов в репозитории | -| `just/ci.just` | Есть рецепт `release` (→ `just build::hab-all-release`) — только firmware, ничего про упаковку TUI или публикацию на GitHub | -| Ветки | `feature-tui-monolith` на 15 коммитов впереди `dev`, ещё не смёржена; в репозитории также есть `main` — политика, какая ветка режет релизы, явно не зафиксирована | - ---- - -## Шаг 1 — Закрыть блокирующие долги перед тегом - -Без этого CI-сборка (шаг 3) не будет соответствовать тому, что уже -провалидировано на железе, — а «релиз, который не воспроизводим из -исходников» хуже отсутствия релиза. - -1. **Актуализировать `tools/service_tui/service_tui.spec`** — добавить - `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` если понадобится), - `collect_dynamic_libs("libusbsio")`, `datas` для - `tools/host/dcd/{dcd.bin,w25q128_fdcb.bin,w25q512_fdcb.bin,ivt_flashloader.bin}` - и `pyproject.toml`. Ориентир — реальное содержимое уже собранных - alpha-бандлов в `dist/` (их можно инспектировать перед удалением из git, - см. следующий пункт). -2. **Убрать `tools/service_tui/dist/` из git**: `git rm -r --cached - tools/service_tui/dist` + добавить `tools/service_tui/dist/` в - `.gitignore`. Собранные бандлы — это build-артефакты, их место в GitHub - Release assets или CI-артефактах, не в истории репозитория. -3. **(Дёшево, но не блокирует)** Убрать мёртвую зависимость `pyusb` из - `tools/service_tui/pyproject.toml` — детект давно переведён на - `spsdk`/`serial.tools.list_ports` (Р7), ни один модуль `app/` её не - импортирует. -4. **Пересобрать бандлы локально** с исправленным spec из актуального HEAD - (`just host::package-tui` на macOS и на Windows) и повторить хотя бы - дымовой прогон чек-листа Гейта 5 из `RELEASE_ROADMAP.md` (детект SDP → - прошивка `firmware_test` → диагностика → выход). Полный деструктивный - чек-лист (обрыв USB и т.п.) уже пройден на предыдущей сборке — здесь - цель убедиться, что исправленный spec не сломал состав бандла, а не - повторять всё с нуля. -5. **Открытый вопрос:** класть ли в релиз Release-сборку `firmware_test` - вообще (сейчас нестабильна) — по умолчанию план предполагает **только - Debug**-образ как основной asset, Release не публикуется до починки - FCB/clock-проблемы. Требует подтверждения. - - → **Решено:** только Debug. -6. **(Добавилось по ходу, не было в исходном плане)** Помимо самого - `datas`/`binaries` в spec, ужесточили сборку двумя хардфейлами вместо - тихих warning'ов: - - `service_tui.spec` теперь падает с `FileNotFoundError`, если в - `tools/host/dcd/` нет хотя бы одного из - `dcd.bin`/`ivt_flashloader.bin`/`w25q128_fdcb.bin`/`w25q512_fdcb.bin`; - - `just host::package-tui` падает с `exit 1`, если не нашёлся Debug - `*_hab.bin` в `build/Debug` (Release остаётся необязательным). - Заодно поправлен баг расположения `custom_binaries/` — recipe создавал - пустую декоративную папку рядом с `service_tui/`, а не внутри неё, хотя - `flasher._resolve_custom_binaries_dir()` смотрит именно внутрь (рядом с - исполняемым файлом); теперь `custom_binaries/` создаётся в правильном - месте и сразу наполняется `TFT_BOOTLOADER_NEW.bin`/`TFT_BOOTLOADER_OLD.bin` - из `tools/service_tui/custom_binaries/`. - - **TODO (отложено, не забыть перед шагом 2):** актуализировать - `tools/service_tui/README.md` и `tools/service_tui/docs/DEV_ARCH.md` §14 — - они всё ещё описывают старое поведение (в частности, блок «Расхождение - spec/факт» в DEV_ARCH.md §14 уже неактуален, spec восстановлен и - ужесточён). Сознательно отложено до ручной валидации сборки на - macOS/Windows (пункт 4) — чтобы задокументировать то, что реально - проверено на железе, а не то, что должно было бы работать. - ---- - -## Шаг 2 — Версия и тег - -1. **Схема тега** — `firmware_test` (0.1.2) и `service-tui` (0.2.0) - версионируются независимо. Предлагается: тег вида `vX.Y.Z` = версия - `service-tui` (это главный продукт релиза для сервисного инженера), - версия `firmware_test` указывается в описании релиза отдельной строкой. - Альтернатива — раздельные теги (`tui-v0.2.0` + `firmware-v0.1.2`), если - в будущем оба проекта должны релизиться независимо друг от друга. - **Требует подтверждения**, план ниже считает первый вариант. -2. **Ветка релиза** — в репозитории есть и `dev`, и `main`, при этом - `main` в `git log` не встречается в истории `feature-tui-monolith`/`dev` - (нужно свериться отдельно, если `main` уже используется под что-то - другое). Рекомендация: смёржить `feature-tui-monolith → dev`, затем - `dev → main`, тег ставить на `main` — так `main` остаётся точкой, - соответствующей опубликованным релизам, а `dev` — интеграционной веткой. - **Требует подтверждения**, если у проекта другая договорённость про - `main`. -3. **`CHANGELOG.md`** — закрыть секцию `[Не выпущено] — service-tui: ...` - → `[YYYY-MM-DD] — v0.2.0`, вычеркнуть из «Известные ограничения» то, что - закрывается шагом 1 (spec-расхождение, `pyusb`). - ---- - -## Шаг 3 — CI: собрать релизные бинарники автоматически - -Текущий `.github/workflows/ci.yml` собирает только `firmware_test` на -`ubuntu-latest` внутри devcontainer — этого недостаточно для -кросс-платформенной упаковки `service-tui`. Нужен отдельный workflow, -не смешанный с обычным PR-циклом (см. `just/ci_workflow.md`, «Шаг 3 — -выделить release workflow»). - -Новый `.github/workflows/release.yml`, триггер — тег `v*` (плюс -`workflow_dispatch` для тестового прогона без публикации): - -| Job | Раннер | Что делает | -| --- | --- | --- | -| `firmware` | `ubuntu-latest` (тот же devcontainer-подход, что в `ci.yml`) | `just ci::release` → `hab-all-release` (по факту нужен только `firmware_test`, Debug+Release); выгрузить `firmware_test_hab.bin` (оба типа) как артефакт | -| `service-tui-macos` | `macos-latest` | скачать firmware-артефакт из job `firmware`; `uv sync` в `tools/service_tui`; `just host::package-tui`; заархивировать `dist/service-tui-vX.Y.Z-macos/` | -| `service-tui-windows` | `windows-latest` | то же самое, PowerShell-совместимые команды (`just`/`uv` доступны на Windows) | -| `publish-release` | `ubuntu-latest`, `needs: [firmware, service-tui-macos, service-tui-windows]` | скачать все артефакты, создать GitHub Release через `gh release create` / `softprops/action-gh-release@v2`, прикрепить `firmware_test_hab.bin` (Debug, + Release с пометкой experimental, если решение по шагу 1.5 — «класть оба»), `service-tui-vX.Y.Z-macos.zip`, `service-tui-vX.Y.Z-windows.zip` | - -Важные нюансы: - -- Firmware для бандла TUI собирается **один раз** в job `firmware` и - передаётся в macOS/Windows job'ы артефактом — пересобирать ARM-прошивку - на каждом раннере отдельно избыточно (и на macOS/Windows раннерах нет - подготовленного devcontainer/toolchain). -- Аппаратные гейты (детект SDP на живой плате, деструктивные сценарии - обрыва USB) **CI выполнить не может** — GitHub-hosted раннеры не видят - реальное USB-устройство. Это ручной шаг, который уже пройден один раз - вручную (коммиты «MacOS tested»/«Windows tested») и должен повторяться - вручную перед каждым релизом, пока не поднят self-hosted HIL-раннер - (см. `just/ci_workflow.md`, «Шаг 5»). План релиза это не блокирует, но - release notes должны явно фиксировать, что сборка прошла ручную проверку - на железе, а не только CI. - ---- - -## Шаг 4 — Ручные шаги перед Publish - -1. Скачать `service-tui-vX.Y.Z-{macos,windows}.zip`, собранные именно CI - (не локальную сборку из шага 1.4) — прогнать сокращённый чек-лист Гейта - 5: детект SDP → прошивка `firmware_test` → диагностика на обеих ОС. - Цель — убедиться, что CI-сборка не разошлась с уже провалидированной - локальной. -2. Обновить `tools/service_tui/README.md`/корневой `README.md` — ссылка на - релиз/инструкция «откуда скачать сервисному инженеру». - -## Шаг 5 — Публикация - -```bash -git tag vX.Y.Z -git push origin vX.Y.Z -``` - -— триггерит `release.yml`. Проверить, что все 3 asset'а прикрепились и -release notes корректны (описание можно сгенерировать из секции -`CHANGELOG.md` за этот релиз + `gh release create --generate-notes` как -дополнение). Если Гейт 6 (`RELEASE_ROADMAP.md`) закрыт не полностью -(например, POST-1 сознательно отложен — это нормально, он и заявлен как -пост-релизный) — релиз всё равно можно публиковать как обычный, а не -pre-release, POST-1 не блокирует v1 по замыслу roadmap. - ---- - -## Шаг 6 — Развитие CI после первого релиза (не блокирует, но логично заложить сразу) - -Эти пункты уже зафиксированы в `just/ci_workflow.md` («Рекомендуемые -следующие шаги»), возвращаемся сюда после первого релиза: - -- `lint` job (`just ci::lint` сейчас заглушка) — `clang-format --dry-run - --Werror` + `clang-tidy`. -- Coverage (`just ci::_coverage` уже есть, но не подключён в workflow). -- Публикация devcontainer image в GHCR — сократит время `firmware` job в - `release.yml` и обычном `ci.yml` (сейчас образ пересобирается в каждой - job, даже с layer-кэшем). -- Self-hosted HIL lane — единственный способ когда-нибудь автоматизировать - то, что сейчас в шаге 4 делается руками. - ---- - -## Сводная последовательность - -``` -Шаг 1 (spec + dist из git + пересборка) - │ -Шаг 2 (тег/ветка/CHANGELOG — решения по открытым вопросам) - │ -Шаг 3 (release.yml: firmware → macOS/Windows package → publish) - │ -Шаг 4 (ручная проверка CI-бинарников на железе) - │ -Шаг 5 (git tag → publish) - │ -Шаг 6 (lint/coverage/GHCR/HIL — после релиза, не блокирует) -``` - -## Открытые вопросы, требующие решения пользователя - -| # | Вопрос | Где всплывает | -| --- | --- | --- | -| 1 | Класть ли Release-сборку `firmware_test` в релиз (сейчас нестабильна) | Шаг 1.5 | -| 2 | Схема тега — один `vX.Y.Z` (=версия TUI) или раздельные теги firmware/TUI | Шаг 2.1 | -| 3 | Тег ставится на `main` (после `dev → main`) или сразу на `dev`/на самой feature-ветке | Шаг 2.2 | diff --git a/README.md b/README.md index c59e507..dea2fba 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ | Проект | Путь | Описание | | ------------------- | ---------------------- | -------------------------------------------------------------------------------------- | -| Тестовая прошивка (✅ реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | -| Загрузчик (⏳ запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | -| Production прошивка (⏳ запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | +| Тестовая прошивка (реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | +| Загрузчик (запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | +| Production прошивка (запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | --- @@ -21,9 +21,9 @@ | Инструмент | Путь | Назначение | | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- | -| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером (Textual, standalone-бинарь). [README](tools/service_tui/README.md) | -| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке (`sdphost`/`blhost`/`nxpimage`/`pyOCD`). [README](tools/host/README.md) | -| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов (pyOCD + M5StampPLC). [README](tools/hil/README.md) | +| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) | +| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) | +| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) | --- @@ -68,21 +68,6 @@ just host::debug-server # GDB-сервер для отладки --- -## Зависимости - -| | Подход | -| --------------------------------------------- | -------------------- | -| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored | -| Unity, fff, SEGGER RTT | vendored | -| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` | -| spsdk (nxpimage, blhost, sdphost — dev-CLI) | `tools/host/uv.lock` | -| spsdk (McuBoot/SDP/HabImage — прямой Python API), Textual | `tools/service_tui/uv.lock` | - -Всё что не меняется — vendored. Сборка работает после `git clone` без интернета -(кроме Python-зависимостей). - ---- - ## Быстрый старт ```bash diff --git a/docs/CI_WORKFLOW.md b/docs/CI_WORKFLOW.md new file mode 100644 index 0000000..48a1ecd --- /dev/null +++ b/docs/CI_WORKFLOW.md @@ -0,0 +1,189 @@ +# CI/CD: устройство GitHub Actions workflow + +> Проект: TFT Firmware (MIMXRT1052CVJ5B) +> Документ описывает схему и принцип работы двух GitHub Actions workflow в +> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация +> релизных бинарников по тегу). Для истории решений и roadmap развития +> `ci.yml` — см. `just/ci_workflow.md`; этот документ — техническая справка +> «как оно работает сейчас», а не хронология. + +--- + +## 1. Два workflow, два разных назначения + +| | `ci.yml` | `release.yml` | +| --- | --- | --- | +| Когда запускается | `push` в `dev`/`main`, любой `pull_request`, `workflow_dispatch` | `push` тега `tui-v*` / `firmware-v*`, `workflow_dispatch` | +| Что проверяет | Собирается ли проект и проходят ли host-тесты | Собираются ли и публикуются ли релизные бинарники | +| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) | +| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` | + +Они намеренно не смешаны в один файл (см. `just/ci_workflow.md`, «Шаг 3 — +выделить release workflow»): PR-цикл должен оставаться быстрым и не зависеть +от кросс-платформенной упаковки `service-tui`, а релизная публикация не +должна гонять host-тесты повторно на каждый push в PR. + +--- + +## 2. `ci.yml` — обычный PR/push-цикл + +```mermaid +flowchart LR + trigger["push (dev/main)\nPR\nworkflow_dispatch"] --> build["build\nсборка в devcontainer"] + build -->|"artifact: build-tree"| test["test\nhost-тесты в devcontainer"] +``` + +Оба job'а выполняются на `ubuntu-latest`, внутри одного и того же +devcontainer-образа (`.devcontainer/Dockerfile`) — то же окружение, что и у +разработчика локально (ARM toolchain, cmake, ninja, `just`, `uv`), не +отдельно собранное под раннер. Образ пересобирается в каждом job'е, но +кэшируется через `docker/build-push-action@v7` (`cache-from`/`cache-to: +type=gha, scope=tft-devcontainer`) — повторные прогоны переиспользуют слои, +не пересобирают с нуля. + +- **`build`** — checkout, поднять devcontainer, `tools/host && uv sync`, + `just ci::build` (→ `just build::build-all-release`, все три firmware- + проекта), выгрузить `build/` как артефакт `build-tree`. +- **`test`** — зависит от `build` (`needs: build`), скачивает `build-tree`, + поднимает тот же образ (тот же кэш), `just ci::test` (→ + `just build::test-host-release`, Unity/fff host-тесты), выгружает логи + + `build/` как `test-artifacts`. + +Команды внутри контейнера запускаются `docker run --user root -v +"$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest +bash -lc '...'` — `--user root` обязателен, иначе non-root пользователь в +контейнере не может писать в bind-mounted `$GITHUB_WORKSPACE` (ломало +`uv sync`/создание `.venv`). + +Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage, +сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо — +самостоятельная задача для self-hosted раннера). См. `just/ci_workflow.md` +для планов по каждому из этих пунктов. + +--- + +## 3. `release.yml` — публикация релиза + +### 3.1 Схема тегов + +Firmware (`firmware_test`) и `service-tui` версионируются и релизятся +**независимо** (`FIRST_RELEASE_PLAN.md`, Шаг 2.1) — два разных паттерна +тега запускают два разных сценария внутри одного workflow-файла: + +```mermaid +flowchart TD + push_fw["push tag\nfirmware-vX.Y.Z"] --> firmware + push_tui["push tag\ntui-vX.Y.Z"] --> firmware + dispatch["workflow_dispatch\n(release_type: tui | firmware)"] --> firmware + + firmware["firmware\n(ubuntu-latest, devcontainer)\njust build::hab-firmware-test-debug"] + + firmware -->|"тег firmware-v*"| publishFw["publish-firmware\ngh release create\n(HAB Debug)"] + + firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| macos["service-tui-macos\njust host::package-tui"] + firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| windows["service-tui-windows\njust host::package-tui"] + + macos -->|"тег tui-v*"| publishTui["publish-tui\ngh release create\n(2× .zip)"] + windows -->|"тег tui-v*"| publishTui +``` + +Ключевое архитектурное решение: **HAB-образ `firmware_test`, который +вшивается внутрь TUI-бандла, всегда собирается заново из текущего HEAD** +джобой `firmware` — а не скачивается из последнего опубликованного +`firmware-v*` релиза. Поэтому job `firmware` выполняется **при любом +триггере**, без условия — она нужна и для отдельного firmware-релиза, и +как зависимость для упаковки TUI. + +### 3.2 Триггеры + +```yaml +on: + push: + tags: ["tui-v*", "firmware-v*"] + workflow_dispatch: + inputs: + release_type: {type: choice, options: [tui, firmware], default: tui} +``` + +`workflow_dispatch` — «сухой прогон» без публикации: собирает всё +(включая упаковку TUI под выбранный `release_type`), выгружает скачиваемые +артефакты, но **не** создаёт GitHub Release — джобы `publish-*` гейтятся +условием `if: startsWith(github.ref, 'refs/tags/...')`, которое на ручном +запуске всегда ложно (`github.ref` в этом случае — ветка, не тег). + +Важный нюанс реализации: условие для `service-tui-macos`/`-windows` +проверяет `release_type` через `github.event.inputs.release_type`, не +через голый контекст `inputs.release_type` — последний рассчитан прежде +всего на reusable workflows (`workflow_call`) и не даёт предсказуемого +результата в job-level `if:` для прямого `workflow_dispatch`. На первом +реальном прогоне (`inputs.release_type`) джобы `service-tui-*` молча +скипались даже при выбранном `release_type=tui` — потребовалась замена на +`github.event.inputs.*`, если снова понадобится ссылаться на inputs в +job-level `if:`, использовать именно эту форму. + +### 3.3 Джобы + +| Job | Раннер | Когда выполняется | Что делает | +| --- | --- | --- | --- | +| `firmware` | `ubuntu-latest`, devcontainer (тот же подход и кэш, что в `ci.yml`) | всегда | сверяет тег `firmware-v*` с `VERSION` в `firmware/test/CMakeLists.txt` (если применимо); `just build::hab-firmware-test-debug`; артефакт `firmware-hab-debug` | +| `publish-firmware` | `ubuntu-latest` | только push тега `firmware-v*` | скачивает `firmware-hab-debug`; `gh release create firmware-vX.Y.Z firmware_test_hab.bin` — standalone-релиз для `tools/host/flash_usb.py`, без TUI | +| `service-tui-macos` / `service-tui-windows` | `macos-latest` / `windows-latest` | push тега `tui-v*` ИЛИ `workflow_dispatch` с `release_type=tui` | `astral-sh/setup-uv` + `extractions/setup-just` (на раннерах нет `uv`/`just` из коробки); скачивает `firmware-hab-debug` в `build/Debug/`; сверяет тег `tui-v*` с `version` в `pyproject.toml` (если применимо); `just host::service-setup` + `just host::package-tui`; архивирует `dist/service-tui-vX.Y.Z-/` в zip (`zip -r` на macOS, `Compress-Archive` на Windows); артефакт `service-tui-macos`/`service-tui-windows` | +| `publish-tui` | `ubuntu-latest`, `needs: [service-tui-macos, service-tui-windows]` | только push тега `tui-v*` | скачивает оба zip; `gh release create tui-vX.Y.Z *.zip` | + +**Только Debug HAB** идёт в релиз (`FIRST_RELEASE_PLAN.md`, Шаг 1.5) — +Release-сборка `firmware_test` нестабильна (FCB/clock), поэтому `firmware` +джоба собирает только `hab-firmware-test-debug`, не полный +`hab-all-release`. + +**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках +(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в +`CMakeLists.txt`/`pyproject.toml`, job падает с понятной ошибкой вместо +того, чтобы молча опубликовать релиз с несовпадающим номером версии внутри +файлов (забытый version bump перед тегом). + +**Windows-раннер и bash** — корневой `Justfile` требует `bash` (`set shell +:= ["bash", ...]`); `windows-latest` образ GitHub Actions включает Git for +Windows (даёт `bash.exe` в PATH из коробки) — дополнительной настройки +shell не требуется, `just`-рецепты выполняются так же, как и локально под +Git Bash на Windows. + +### 3.4 Как проверить без публикации + +```bash +gh workflow run release.yml --ref dev -f release_type=tui +gh workflow run release.yml --ref dev -f release_type=firmware +``` + +или через веб-интерфейс: Actions → **Release** → **Run workflow** → выбрать +branch и `release_type`. Джобы `publish-*` в этом сценарии показываются как +**Skipped**, не **Failed** — это ожидаемое поведение, не баг. + +--- + +## 4. Общее между `ci.yml` и `release.yml` + +- **Один и тот же devcontainer-подход** для всего, что требует ARM + toolchain (сборка firmware) — job `firmware` в `release.yml` использует + дословно тот же паттерн `docker/build-push-action@v7` + + `docker run --user root ...`, что и `build`/`test` в `ci.yml`. +- **Общий GHA layer-кэш** — оба workflow используют `scope: + tft-devcontainer` в `cache-from`/`cache-to`, поэтому кэш переиспользуется + между обычными PR-прогонами и релизными сборками, а не живёт отдельно. +- **`workflow_dispatch` есть у обоих** — в `ci.yml` это просто способ + перезапустить pipeline вручную без нового коммита; в `release.yml` — это + единственный способ протестировать сборку без реальной публикации. + +--- + +## 5. Чего пока нет + +- **Self-hosted HIL-раннер.** Ни один из двух workflow не может + задетектировать SDP на живой плате или прогнать деструктивные сценарии + (обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной + шаг перед каждым релизом (`FIRST_RELEASE_PLAN.md`, Шаг 4), пока не + поднят self-hosted lane (`just/ci_workflow.md`, «Шаг 5»). +- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml` — + см. `just/ci_workflow.md`, «Рекомендуемые следующие шаги» (Шаги 1–2). +- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой + job'е каждого workflow (пусть и с layer-кэшем); заранее опубликованный + образ сократил бы время старта ещё сильнее (`just/ci_workflow.md`, «Шаг 4»). diff --git a/firmware/test/README-1.md b/firmware/test/README.md similarity index 100% rename from firmware/test/README-1.md rename to firmware/test/README.md diff --git a/firmware/test/src/tests/README-2.md b/firmware/test/src/tests/README.md similarity index 100% rename from firmware/test/src/tests/README-2.md rename to firmware/test/src/tests/README.md diff --git a/tools/service_tui/README.md b/tools/service_tui/README.md index c306d19..99f87f3 100644 --- a/tools/service_tui/README.md +++ b/tools/service_tui/README.md @@ -221,6 +221,10 @@ Production/Custom этот шаг не нужен). перед готовностью отвечать). WaitingScreen в это время показывает «Плата найдена, подключаемся...» — это штатное поведение, не зависание. См. `docs/DEV_ARCH.md`, §10. +- **macOS-бандл не подписан Apple Developer ID и не нотаризован** — при + первом запуске Gatekeeper блокирует каждый файл бандла по отдельности. + Обход — `xattr -cr` на распакованную папку, см. «Запуск» → «macOS: + первый запуск» выше. --- @@ -285,6 +289,27 @@ spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` н Python/uv на машине сервисника. Структура бандла и резолв путей во frozen — см. [DEV_ARCH.md](docs/DEV_ARCH.md), §14. +#### macOS: первый запуск («Apple could not verify...» на каждый файл) + +Бандл не подписан Apple Developer ID и не нотаризован — Gatekeeper при +первом запуске выдаёт отдельное предупреждение «не удалось проверить +разработчика» **на каждый** файл бандла (исполняемый файл + каждая `.dylib` +внутри `_internal/`, включая `libusbsio`), а не одно общее окно. Причина — +карантинный атрибут (`com.apple.quarantine`), который macOS проставляет на +файлы, распакованные из скачанного архива; через «Открыть» в Finder его +пришлось бы снимать по одному диалогу на файл. + +Снять его сразу со всего бандла одной командой в терминале (один раз, +после распаковки zip): + +```bash +xattr -cr service-tui-vX.Y.Z-macos/ +``` + +После этого бандл запускается без единого диалога. Это стандартный путь +для несигнированных бинарников, распространяемых вне App Store/сайта +разработчика — не баг конкретной сборки. + --- ## Зависимости @@ -293,7 +318,6 @@ Python/uv на машине сервисника. Структура бандл | --------------- | ------- | -------------------------------------------------------- | | `textual` | ≥ 0.80 | TUI фреймворк | | `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | -| `pyusb` | ≥ 1.0 | не используется в коде (детект SDP/CDC идёт через `spsdk`) — исторический остаток, кандидат на удаление из `pyproject.toml` | | `spsdk` | 3.7.0 | прошивка: SDP/McuBoot (Flash, chip erase) + HabImage (сборка кастомных HAB), нативный HID (libusbsio) — без Zadig | | `python-dotenv` | ≥ 1.0 | загрузка `.env` | | `pyinstaller` | ≥ 6.0 | сборка standalone-бандла (`just host::package-tui`) | diff --git a/tools/service_tui/docs/DEV_ARCH.md b/tools/service_tui/docs/DEV_ARCH.md index 815d593..73709e1 100644 --- a/tools/service_tui/docs/DEV_ARCH.md +++ b/tools/service_tui/docs/DEV_ARCH.md @@ -721,17 +721,18 @@ service-tui-vX.Y.Z-/ означает, что `libusb-1.0.*`/Zadig в бандле **не нужны** ни на Windows, ни на macOS — детект BootROM SDP и Flashloader работает из коробки. -> **Расхождение spec/факт:** закоммиченный `service_tui.spec` объявляет в -> `datas` только `('../shared', 'shared')` — без `dcd/*.bin`, -> `pyproject.toml` или `spsdk`-данных. Тем не менее уже собранные релизные -> бандлы в `tools/service_tui/dist/service-tui-v0.2.0-{macos,windows}/` -> фактически содержат `_internal/data/{dcd.bin,*_fdcb.bin,ivt_flashloader.bin}`, -> `_internal/pyproject.toml` и `_internal/spsdk/` — то есть сборки, тестировавшиеся -> на железе (Фаза 5, гейт по macOS/Windows), были собраны с более полным -> набором `datas`, чем то, что сейчас лежит в репозитории. `service_tui.spec` -> нужно актуализировать (`collect_data_files("spsdk")`, `tools/host/dcd/*.bin` -> → `data/`, `pyproject.toml`) до следующей сборки релиза — см. «Известные -> открытые вопросы». +`service_tui.spec` актуализирован и ужесточён (FIRST_RELEASE_PLAN.md, Шаг 1): +`collect_data_files("spsdk")`, `collect_dynamic_libs("libusbsio")`, `datas` +для `tools/host/dcd/*.bin` (→ `data/` внутри `_internal`) и `pyproject.toml`. +Сборка падает с `FileNotFoundError` уже на этапе генерации спека, если в +`tools/host/dcd/` не хватает хотя бы одного из обязательных блобов — +несоответствие spec/факт, из-за которого ранее собранные и протестированные +на железе бандлы расходились с тем, что в репозитории, больше не может +проскочить незамеченным. `dist/` больше не коммитится в git (см. +`.gitignore`) — собранные бандлы это build-артефакты, не история репозитория. +Пересобрано и провалидировано на живом железе macOS + Windows после +актуализации spec (Гейт 5: детект SDP → прошивка `firmware_test` → +диагностика → выход) — см. `FIRST_RELEASE_PLAN.md`, Шаг 1.4. --- @@ -769,13 +770,6 @@ service-tui-vX.Y.Z-/ `app/flash.py` (комментарий `_check_sdp_present`) и `flasher.py` (docstring модуля упоминает Фазу 2 буквально, что нормально как история провенанса, но стоит перепроверить при следующей правке этих файлов). -- **`service_tui.spec` не актуализирован под реальные релизные сборки** — - см. §14. Нужно добавить `datas` (`dcd/*.bin`, `pyproject.toml`, - `collect_data_files("spsdk")`) до следующей упаковки релиза. -- **`pyusb` в `pyproject.toml` — мёртвая зависимость.** Р7 перевёл детект - SDP/CDC на `spsdk`/`serial.tools.list_ports`; ни один модуль `app/` больше - не импортирует `usb`/`pyusb`. Кандидат на удаление при следующей - grep-зачистке (Фаза 6). - **Release-сборка firmware нестабильна** (медленное мигание — подозрение на проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно форсирует Debug через `FIRMWARE_BUILD_TYPE`. diff --git a/tools/service_tui/docs/DEV_PLAN.md b/tools/service_tui/docs/DEV_PLAN.md deleted file mode 100644 index cbad137..0000000 --- a/tools/service_tui/docs/DEV_PLAN.md +++ /dev/null @@ -1,115 +0,0 @@ -# service-tui — Единый мастер-план разработки и релиза (Master Plan v1.0) - -> **Статус документа:** Консолидированный рабочий документ на основе планов миграции на монолит (V4), USB-кроссплатформенности, верификации прошивки и дорожной карты выпуска версии v1.0. - ---- - -## 1. Контекст проекта и целевая архитектура - -**Цель прошивки (`firmware_test`):** Полная диагностика платы **MIMXRT1052CVJ5B** на сервисе и производстве (обработка возвратов по рекламации). Прошивка запускается напрямую через BootROM (USB SDP) без предварительной записи загрузчика в Flash. - -**Архитектура стенда:** - -* **Основной канал:** Хост подключается к плате через один кабель USB CDC ACM (интерфейс `firmware_test`). UART/LPUART в сервисе сознательно не используются. -* **HIL-стенд (опционально):** Автоматические HIL-тесты управляются через модуль **M5StampPLC** (реле RLY1–4 + CAN-трансивер). -* **Оркестрация:** TUI-приложение на Textual одновременно координирует работу `firmware_test` (через USB CDC) и M5 (через Serial JSON-lines). - ---- - -## 2. Закрытые архитектурные решения (Не пересматривать) - -### Транспорт и парсинг протокола - -* **Единый канал:** USB CDC ACM — единственный интерфейс рантайма. Строковый парсинг без использования тяжелого cJSON (через поиск `strstr` по ключевым полям `"type"` / `"cmd"`). -* **Разделение тестов:** Тесты делятся на автономные (`requires_hil=false`) и стендовые (`requires_hil=true`). Если M5StampPLC не обнаружен на шине, TUI автоматически делает HIL-тесты недоступными для выбора (окрашивает в серый цвет) и исключает их из группового запуска. -* **Динамический реестр (`list_tests`):** Плата сама отдает список поддерживаемых тестов с метаданными. TUI строит интерфейс динамически и не хардкодит список тестов внутри себя. Порядок выполнения при `run_selected` всегда определяется реестром таргета, а не порядком ID в запросе хоста. - -### Аппаратная интеграция и особенности NXP BSP - -* **SDRAM и DCD:** Контроллер SEMC инициализируется блоком DCD до передачи управления в `main()`. Функция `bsp_sdram_init()` выполняет исключительно верификацию стабильности памяти. -* **QSPI Flash в ITCM:** Функции работы с QSPI выполняются из быстрой памяти ITCM с использованием макросов `AT_QUICKACCESS_SECTION_CODE` и инициализации через `__STARTUP_INITIALIZE_RAMFUNCTION`. -* **W25Q256/512:** Используются выделенные 4-байтные опкоды чтения/записи/стирания без перевода чипа в глобальный 4-байтный режим (команда `0xB7`). -* **MQS Аудио:** Периферия требует стерео PCM16 буфера (SAI3 + eDMA канал 0). На плате выведен только правый канал (`MQS_RIGHT`), поэтому буфер формируется как стерео с идентичными данными L и R. -* **Порядок включения MQS:** Сначала запускается усилитель LM4875M (`bsp_mqs_amp_init()`), далее выдерживается пауза 300 мс для заряда конденсаторов C103/C105, и только потом инициализируется ядро MQS. Нарушение порядка приводит к громкому щелчку или отсутствию звука. Использование `bsp_mqs_play()` реализовано асинхронно с поллингом USB CDC во избежание голодания интерфейса. -* **PWMChannelEnable (SDK ≥ 2.13):** Флаг `pwmchannelenable` в структуре `pwm_signal_param_t` обязательно выставляется в `true`, иначе функция `PWM_SetupPwm()` не откроет выход `OUTEN`, что маскируется отладчиком и воспроизводится только при «холодном» сбросе платы. -* **ERRATA 50235 (FlexCAN + USB):** Макрос `FLEXCAN_Init()` вызывает `assert` на тактирование LPUART1 (`CCM_CCGR5_CG12`). Если после инициализации USB CDC этот гейт закрыт — плата уходит в `HardFault`. Решение: принудительный вызов `CLOCK_EnableClock(kCLOCK_Lpuart1)` перед инициализацией CAN. -* **Синхронное чтение оптовходов:** Внедрена функция `bsp_opto_force_read()` для прямого чтения состояния пинов без дебаунса, что устраняет race condition, возникающий из-за дребезга контактов реле на HIL-стенде. - -### Монолитная архитектура TUI (V4) и кроссплатформенность - -* **Отказ от Nuitka и venv:** Упаковка приложения выполняется исключительно штатными средствами PyInstaller в один самодостаточный каталог (`onedir`), без развертывания виртуального окружения Python на целевой машине инженера. -* **In-process spsdk:** Вся работа с BootROM SDP и McuBoot переведена на прямое использование Python API пакета `spsdk==3.7.0`. Скрипт `flash_usb.py` полностью исключен из production-цепочки и оставлен разработчикам как инструмент автоматизации в `Justfile`. -* **Нативный USB-детект без Zadig:** Устройства в режимах SDP (`1FC9:0130`) и Flashloader (`15A2:0073`) определяются как HID-устройства методами `SdpUSBInterface.scan()` и `MbootUSBInterface.scan()`. Обмен идет через системную библиотеку `libusbsio`, что полностью устраняет необходимость использования утилиты **Zadig** (WinUSB) на ОС Windows. -* **Резолв Serial-портов:** Осуществляется рантайм-поиск по VID:PID через `serial.tools.list_ports`. Явные пути к портам в `.env` используются только в качестве оверрайда (escape hatch). Платформа macOS использует неблокирующие callout-устройства `/dev/cu.usbmodem*`. -* **«Липкий» выбор (`FlashPreset`):** Выбор оператора (файл прошивки, тип памяти платы, тумблер DCD) кэшируется в контексте процесса. При прошивке партии одинаковых плат настройки подставляются автоматически, оператору достаточно нажать «Загрузить». -* **Каркас AppFrame:** Все экраны оборачиваются в фиксированный CSS-контейнер `AppFrame` (макс. размер `112x35`). Это гарантирует визуальную консистентность интерфейса и устраняет критический краш Textual 8.x при mouse drag. - ---- - -## 3. Итоговая матрица тестов прошивки - -| ID | Название теста | Critical | Требует HIL | Тип выполнения | Драйвер BSP | -| :-------- | :----------------- | :------: | :---------: | :--------------------------------- | :--------------- | -| `sdram` | SDRAM 32 MB | ✅ | ❌ | Автономный (self) | `bsp_sdram` | -| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Автономный (self) | `bsp_qspi_flash` | -| `usd` | microSD (SDIO) | ❌ | ❌ | Интерактивный | `bsp_sd` + FatFS | -| `display` | TFT Display RGB888 | ❌ | ❌ | Интерактивный (4 цвета, 2 ротации) | `bsp_display` | -| `buttons` | Test Buttons 1/2 | ❌ | ❌ | Интерактивный (физический клик) | `bsp_button` | -| `mqs` | MQS Audio Out | ❌ | ❌ | Интерактивный (тон ~4 сек) | `bsp_mqs` | -| `can` | CAN loopback | ❌ | ✅ | Стендовый HIL (M5 CAN RX/TX) | `bsp_can` | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | Стендовый HIL (M5 реле 2, 3, 4) | `bsp_opto` | - -*Примечание: Тесты `uart_ttl` и `uart_iso` удалены из реестра как избыточные для сервисного применения.* - ---- - -## 4. Дорожная карта до релиза v1.0 (Текущие фазы) - -### Фаза 4a — Добор типизации обрыва USB - -* **Цель:** Перехват всех физических отключений кабеля во время операций и вывод унифицированного сообщения *«Соединение с платой потеряно»* вместо необработанных исключений. -* **Реализация:** - 1. Расширить блоки `try-except` в `flash_backend.py` (`load_flashloader`, `flash`, `erase_chip`), обрабатывая кортеж исключений `(SPSDKConnectionError, SPSDKTimeoutError)`. - 2. Внедрить «Вариант B» для долгих операций (например, `flash_erase_all`), которые при таймауте возвращают `False` без генерации исключения: в случае `ok == False` вызывать мгновенный `detect_sdp()`. Если устройство исчезло с шины — поднимать `ConnectionLostError(connection_lost=True)`. Любой сбой самой проверки трактовать как обрыв связи. -* **Критерий успеха (Гейт):** Выдергивание кабеля во время стирания или записи вызывает корректную реакцию экрана `FlashScreen`, кнопки разблокируются, лог информирует об обрыве, а повторное подключение позволяет продолжить работу без перезапуска TUI. - -### Фаза 4b — Оптимизация логирования и троттлинг - -* **Цель:** Исключение избыточных HID-дампов из основного лога и разгрузка текстового виджета Textual. -* **Реализация:** - 1. В `main.py` установить глобальный уровень логов `INFO`. Сторонние логгеры (`spsdk`, `libusbsio`, подмодули протоколов bulk) принудительно перевести в режим `WARNING`. Полный дамп активировать только при передаче переменной окружения `SERVICE_LOG_LEVEL=DEBUG`. - 2. Внедрить шаг троттлинга в `flash.py::_on_progress`. Значения в графический прогресс-бар отправлять без задержек (для плавности), а текстовые записи фазы `write` отправлять в виджет `#flash-log` только при пересечении шага в **10%** (0%, 10%, 20%... 100%). -* **Критерий успеха (Гейт):** Лог одной сессии прошивки сокращается со ~135 строк до ~10. Отсутствует визуальное замедление интерфейса. - -### Фаза 5 — Упаковка через PyInstaller и полировка UI - -* **Цель:** Создание бинарного дистрибутива под целевые ОС (Windows, macOS). -* **Реализация:** - 1. Написать конфигурационный файл `service_tui.spec`. Использовать директиву `collect_dynamic_libs("libusbsio")` для копирования нативных библиотек HID-транспорта под текущую ОС. Библиотеки `pyusb` и `libusb-1.0` исключить из сборки. - 2. Настроить секцию `datas` для переноса файлов `tools/host/dcd/` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) во внутреннюю папку бандла `data/`. Двухрежимный резолвер путей в коде должен прозрачно переключаться на `sys._MEIPASS` при `getattr(sys, 'frozen', False)`. - 3. Скопировать стабильный отладочный образ `firmware_test_hab.bin` (Debug) в директорию `firmware/` на одном уровне с исполняемым файлом, обеспечивая возможность его замены инженерами без пересборки бандла. - 4. Добавить кнопку «✕ Выйти из приложения» на стартовый экран `WaitingScreen` с привязкой к методу `self.app.exit()`. -* **Критерий успеха (Гейт):** На чистой машине (без установленного Python, uv и привязанных через Zadig драйверов) дистрибутив запускается, успешно определяет плату в SDP-режиме, загружает Flashloader, выполняет Chip Erase и зашивает диагностическую прошивку. - -### Фаза 6 — Документация, CHANGELOG и выпуск релиза - -* **Цель:** Финализация кодовой базы и документации. -* **Реализация:** - 1. Выполнить тотальную зачистку (`grep-cleanup`) исходного кода. Удалить отладочные комментарии, неактуальные упоминания вызовов через `subprocess` и `uv run`. Актуализировать docstrings модулей `flash_backend.py` и `flasher.py`. - 2. Сформировать финальный `CHANGELOG.md`, зафиксировав переход на монолит V4, нативную обработку ошибок USB-шины и отказ от Zadig. - 3. Исключить инструкции по настройке Zadig из руководств `README.md` и `HOW_TO_FLASH.md`. Добавить описание ограничений первой версии (строго одна плата на стенде одновременно). - 4. Создать релизный тег в Git, соответствующий текущему значению версии в `pyproject.toml`. - ---- - -## 5. Бэклог и пост-релизные задачи (Версии v1.1+) - -Задачи, согласованные к реализации, но вынесенные за рамки стабильного релиза v1.0: - -1. **Этап 7 — Provisioning платы:** - * Считывание уникального аппаратного идентификатора кристалла `OCOTP_UNIQUE_ID` средствами SDK-модуля `fsl_ocotp`. - * Передача события формата `{"type":"provision_ready","chip_uid":"..."}` на хост по завершении матрицы тестов. - * Ожидание подтверждения `provision_ack` от TUI и последующая фиксация статуса успешного прохождения в первом секторе Flash-памяти за пределами исполняемой зоны XIP (проработка логики защиты от повторной перезаписи). -2. **Экспорт результатов (POST-1):** Создание обработчика для автоматической выгрузки результатов диагностики в структурированный JSON-файл с жесткой привязкой к считанному UID микроконтроллера. -3. **UID буфер обмена:** Реализация механизма копирования или выделения UID контроллера напрямую с экрана терминала `DiagScreen`. -4. **Циклический прогон тестов:** Добавление тумблера «Циклический режим» для непрерывного фонового тестирования неинтерактивных узлов платы (SDRAM, QSPI Flash, CAN, Opto) с целью выявления плавающих аппаратных дефектов и температурной нестабильности элементов. diff --git a/tools/service_tui/docs/RELEASE_ROADMAP.md b/tools/service_tui/docs/RELEASE_ROADMAP.md deleted file mode 100644 index 072a557..0000000 --- a/tools/service_tui/docs/RELEASE_ROADMAP.md +++ /dev/null @@ -1,467 +0,0 @@ -# service-tui — Roadmap до релиза v1 (фазы 4a → 4b → 5 → 6) - -> Продолжение `MONOLITH_APP_PLAN.md` (V4). Фазы 0–3 закрыты, Фаза 4 -> закрыта частично — деструктивные гейты на железе вскрыли пробел в -> типизации обрыва USB (см. Фаза 4a). Этот документ — дорожная карта -> оставшегося пути до релиза. -> -> Ветка: `feature-tui-monolith` от `dev`. Каждая фаза = коммит(ы) с -> зелёным гейтом; откат любой фазы не ломает предыдущие. - ---- - -## Статус на входе - -| Фаза | Статус | -| --- | --- | -| 0 — Спайк / де-риск | ✅ Закрыта (⚠В1/⚠В2/Р7/О1 сняты, golden-тест byte-exact) | -| 1 — Backend-модуль | ✅ Закрыта (`flash_backend.py`, `usb_ports.py`, 41 тест) | -| 2 — Пересадка `Flasher` | ✅ Закрыта (async-мост, гейт на железе) | -| 3 — Custom HAB in-process | ✅ Закрыта (де-факто вместе с фазой 1, подтверждена на железе) | -| 4 — Обработка отвала USB | ⚠️ **Частично** — see 4a | -| 4a — Добор типизации обрыва | ⏳ **Следующая** | -| 4b — Сокращение логов | ⏳ | -| 5 — Упаковка PyInstaller | ⏳ | -| 6 — Документация / релиз | ⏳ | - -### Почему Фаза 4 не закрыта - -Деструктивные гейты на железе (macOS) показали: **выдёргивание USB -проявляется тремя разными способами**, а код Фазы 4 корректно -типизирует только один. - -| Проявление в spsdk | Что реально прилетает | Фаза 4 (сейчас) | Пользователь видит | -| --- | --- | --- | --- | -| write-фаза, обрыв при записи | `SPSDKConnectionError` | обёрнут только вокруг `with SDP`/`with McuBoot`, но реальный путь `write_memory` прошёл мимо | «Непредвиденная ошибка» (safety net) | -| read-фаза после write | `SPSDKTimeoutError` (потомок `SPSDKError`, **не** `SPSDKConnectionError`) | не ловится обёрткой обрыва | «Непредвиденная ошибка» (safety net) | -| chip erase по таймауту | `flash_erase_all()` вернул `False` (без исключения) | ветка `if not ok: raise FlashBackendError(...)` | «flash_erase_all вернул False» | - -План Фазы 4 буквально требовал «`McuBootConnectionError`/**таймауты** → -`FlashBackendError(connection_lost=True)`». Таймаут (`SPSDKTimeoutError`) -и `False`-по-таймауту не были покрыты — значит Гейт 4 по букве не пройден. -Это добор, а не новая работа сверх плана. - -> **Важно (UX-надёжность уже работает):** safety net (`except Exception` -> в `flasher.py`) во всех трёх случаях корректно вернул `ok=False`, -> разблокировал кнопки, оставил приложение живым. Проблема -> исключительно в *формулировке* сообщения, не в устойчивости. - ---- - -## Принятые решения этого этапа - -| ID | Решение | -| --- | --- | -| **Р10** | Erase-таймаут (`False` без исключения) переклассифицируется в `ConnectionLostError` **вариантом B**: после `False` выполнить быстрый `detect_sdp()` — если устройство пропало с шины, это обрыв; если на месте — честная ошибка операции. | -| **Р11** | Троттлинг `write`-событий в `#flash-log` — каждые **10%** (≈10 строк вместо ~135). Прогресс-бар обновляется на **каждом** событии (плавность не теряется), в лог пишется прореженно. | -| **Р12** | Логирование: root/`spsdk`/`libusbsio` понижаются до `WARNING` по умолчанию; полный DEBUG доступен через env-переключатель (диагностика не теряется совсем). | -| **О2 (закрыт)** | Состав `firmware/` в релизе — только `firmware_test`; тип сборки (Debug/Release) управляется через `.env` (`FIRMWARE_BUILD_TYPE`), механизм уже реализован в `flasher.py`. | -| **POST-1** | Циклический прогон неинтерактивных тестов (SDRAM/NOR/OPTO/CAN) на `DiagScreen` — **отложен на пост-релиз**, вне `MONOLITH_APP_PLAN.md`. Зафиксирован, чтобы не потерять. | -| **Предложение 2** | Кнопка «Выйти из приложения» на `WaitingScreen` — принято, включается в Фазу 5 (UI-полировка перед упаковкой). | - ---- - -## Фаза 4a — Добор: корректная типизация обрыва USB - -**Цель:** все три проявления обрыва USB дают пользователю единое -понятное сообщение «Соединение с платой потеряно», а не «Непредвиденная -ошибка» / «flash_erase_all вернул False». - -### Файлы - -| Файл | Тип правки | -| --- | --- | -| `app/flash_backend.py` | правки: расширить обёртку обрыва на `SPSDKTimeoutError`; реализовать вариант B для erase | -| `tests/test_flash_backend.py` | правки: тесты на `SPSDKTimeoutError`-путь и на erase-переклассификацию | - -### Содержание - -1. **Обёртка обрыва расширяется** с `SPSDKConnectionError` на общий - родительский случай, покрывающий и `SPSDKTimeoutError`. Оба — - потомки `SPSDKError`, но `SPSDKTimeoutError` **не** наследует - `SPSDKConnectionError`, поэтому текущий `except SPSDKConnectionError` - его пропускает. Ловим оба явным кортежем - `(SPSDKConnectionError, SPSDKTimeoutError)` в трёх местах: - `load_flashloader`, `flash` (основная + ram_only ветки), `erase_chip`. - -2. **Вариант B для erase** (Р10): в `erase_chip` (и в `flash`, где - команды возвращают `False` по тем же причинам) — при `ok == False` - выполнить быстрый `detect_sdp()`; если устройство исчезло с шины → - `ConnectionLostError`, иначе → обычный `FlashBackendError` с прежним - текстом. Проверка `detect_sdp()` добавляется **только в error-путь**, - на happy path не влияет. - -3. **`_format_error_message` в `flasher.py` не трогается** — он уже - корректно даёт префикс «Соединение с платой потеряно» для любого - `connection_lost=True`. Достаточно, чтобы backend правильно поднял - `ConnectionLostError`. - -### Гейт 4a - -- [ ] Юнит-тест: `SPSDKTimeoutError` из `write_memory` → `ConnectionLostError` - (мок). -- [ ] Юнит-тест: `flash_erase_all` → `False` + `detect_sdp()==False` → - `ConnectionLostError`; `False` + `detect_sdp()==True` → - обычный `FlashBackendError` (мок). -- [ ] Существующие 41 тест зелёные (регрессии нет). -- [ ] **Железо (повтор деструктивных сценариев):** - - [ ] Выдернуть USB во время `write-memory` → в `#flash-log` - «Соединение с платой потеряно», не «Непредвиденная ошибка». - - [ ] Выдернуть во время chip erase → то же сообщение. - - [ ] Повторная вставка → прошивка успешна (порт не «занят»). -- [ ] macOS + Windows. - ---- - -## Фаза 4b — Сокращение логов - -**Цель:** лог-файл прошивки уменьшается на порядок; `#flash-log` в TUI -показывает осмысленный прогресс, а не ~135 однотипных строк. - -### Файлы - -| Файл | Тип правки | -| --- | --- | -| `app/main.py` | правки: уровни логгеров (Р12) + env-переключатель DEBUG | -| `app/screens/flash.py` | правки: троттлинг `write`-событий в `_on_progress` (Р11) | - -### Содержание - -1. **Уровни логгеров (Р12):** root по умолчанию `INFO`; `spsdk`, - `libusbsio`, `libusbsio.hidapi.dev`, `spsdk.mboot.protocol.bulk_protocol` - → `WARNING` (именно они дают портянки HID-байтов). Полный DEBUG - включается через переменную окружения (например - `SERVICE_LOG_LEVEL=DEBUG`) — точное имя согласовать, не изобретаю. - -2. **Троттлинг `#flash-log` (Р11):** в `flash.py::_on_progress` - прогресс-бар обновляется всегда, а `write_line` в лог для фазы - `write` — только при пересечении 10%-границы (0/10/20/…/100). - Остальные фазы (`configure`/`erase`/`fcb`/`reset`/`done`/`error`/ - `hab_build`) логируются как есть — их немного. - -### Гейт 4b - -- [ ] Лог-файл одной прошивки на порядок короче; DEBUG-портянок - spsdk/libusbsio нет при дефолтном уровне. -- [ ] `SERVICE_LOG_LEVEL=DEBUG` (или согласованное имя) возвращает - полный DEBUG — диагностика доступна. -- [ ] `#flash-log`: ~10 строк прогресса записи вместо ~135, бар - по-прежнему плавный. -- [ ] Регрессия: прошивка/erase/диагностика на железе работают. - ---- - -## Фаза 5 — Упаковка PyInstaller + UI-полировка - -**Цель:** один исполняемый бандл на Windows и macOS, проходящий полный -полевой цикл на чистой машине без Zadig/сети/Python. Плюс кнопка -«Выйти» на `WaitingScreen`. - -### Файлы - -| Файл | Тип правки | -| --- | --- | -| `tools/service_tui/service_tui.spec` | новый — PyInstaller spec | -| `app/screens/waiting.py` | правки: кнопка «Выйти из приложения» (Предложение 2) | -| just-рецепт | новый — имя задачи согласовать, **не изобретаю** | -| `app/app.tcss` | правки при необходимости — стиль кнопки Quit на Waiting | - -### Содержание spec (из плана V4, §Фаза 5) - -- `collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` при необходимости — - документированный NXP механизм для frozen); -- **`collect_dynamic_libs("libusbsio")`** — нативный HID-транспорт - (следствие Р7); `libusb-1.0.*` в бандле **отсутствует**; -- `datas`: `tools/host/dcd/{dcd.bin, w25q128_fdcb.bin, w25q512_fdcb.bin, - ivt_flashloader.bin}` → `data/`; -- `datas`: `firmware//firmware_test_hab.bin` (Type из `.env`, О2); -- `datas`: `pyproject.toml` (для `_read_app_version` во frozen); -- onedir (не onefile — onefile замедляет старт распаковкой); -- резолвер путей backend'а уже готов: frozen → `sys.executable.parent` - (`firmware_hab_path`, `_resolve_custom_binaries_dir`). - -Целевая структура бандла: - -``` -service-tui-vX.Y.Z-/ -├── service_tui[.exe] -├── _internal/ -│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data -│ └── ... ← рантайм PyInstaller, libusbsio -├── firmware/ -│ └── /firmware_test_hab.bin -└── custom_binaries/ ← пустая -``` - -### UI-полировка (Предложение 2) - -Кнопка «✕ Выйти из приложения» на `WaitingScreen`, симметрично -`FlashScreen`/`DiagScreen`/`PostFlashScreen` (`self.app.exit()`). - -### Гейт 5 (Windows + macOS) - -- [ ] Чистая Windows, **без Zadig, без сети, без Python/uv**: полный - полевой цикл — детект SDP → firmware_test → диагностика → - custom (W25Q128 и W25Q512) → chip erase. -- [ ] То же на macOS. -- [ ] Версия на `WaitingScreen` корректна во frozen. -- [ ] Порты резолвятся при перетыкании в другой физический USB-порт - (проверка Р8 на обеих ОС). -- [ ] Кнопка «Выйти» на `WaitingScreen` работает. -- [ ] M5StampPLC (нативный CDC `303A:4001`, драйверы не нужны — - подтверждено О1) виден во frozen-бандле. - ---- - -## Фаза 6 — Документация, CHANGELOG, финальная зачистка, релиз - -**Цель:** синхронизировать документацию с реальностью монолита, -провести отложенную зачистку комментариев/grep, собрать релизный -артефакт из тега. - -### Файлы - -| Файл | Тип правки | -| --- | --- | -| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | правки: **отложенная зачистка комментариев** (grep-cleanup Гейта 4 + актуализация docstring-провенансов) | -| `CHANGELOG.md` | правки | -| `RELEASE_PLAN.md` | правки: закрыть шаг 3 ссылкой на V4/этот roadmap | -| `docs/DEV_ARCH.md` | правки: §2 (убрать subprocess из диаграммы), §8.3 (новый конвейер) | -| `HOW_TO_FLASH.md` | правки | -| `tools/service_tui/README.md` | правки | -| `.env.example` | правки: по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | - -### Содержание - -1. **Отложенная зачистка (из Фазы 4, согласовано):** финальный проход - по всему коду — актуализировать docstring-провенансы («прямой порт - flash_usb.py», «subprocess-версия» и т.п.) под реальность монолита. - Цель grep Гейта 4 (`flash_usb\|uv run\|subprocess\|usb.core` пусто - в `app/`) — либо достигается, либо остаётся осознанно как - документация происхождения (решение по каждому вхождению). -2. **CHANGELOG:** монолит (flash_backend, отказ от venv/subprocess), - нативный детект без Zadig (Р7), кроссплатформенный резолв портов (Р8), - нативная обработка отвала USB, упаковка одним exe. -3. **Zadig-инструкция в доки НЕ добавляется** (Р7 отменил план - RELEASE_PLAN). M5 — нативный CDC, вендорский драйвер не нужен (О1). -4. **Разделение зафиксировать:** `flash_usb.py` — dev-CLI (just-рецепты), - `flash_backend.py` — production-TUI; независимые реализации (Р2). -5. **Golden-тест HAB** — отметить как обязательный при апгрейде spsdk. -6. **Ограничение «одна плата на столе»** (О3) — в README. -7. **POST-1** (циклический прогон) — зафиксировать в бэклоге/README как - запланированную пост-релизную фичу. -8. Тег релиза = версия из `pyproject.toml`. - -### Гейт 6 - -- [ ] Документация синхронизирована (железо подтверждено гейтами 4a/5). -- [ ] `just host::flash*`, `incoming`, `production` работают как раньше — - регрессия dev-пути. -- [ ] Релизный артефакт собран из тега; чек-лист Гейта 5 повторён на - релизном бинаре. -- [ ] POST-1 зафиксирован в бэклоге, не потерян. - ---- - -## Сводная последовательность и зависимости - -``` -4a ──► 4b ──► 5 ──► 6 ──► RELEASE v1 -│ │ │ │ -│ │ │ └── доки, зачистка, тег, регрессия dev-пути -│ │ └── PyInstaller (Win+macOS), кнопка Quit на Waiting -│ └── уровни логов + троттлинг #flash-log -└── типизация обрыва (SPSDKTimeoutError + erase вариант B) - -Блокеры перед фазами: - 4a: нет — старт сразу - 4b: нет — после 4a - 5: О2 закрыт ✅; согласовать имя just-задачи и env-переменной DEBUG - 6: все гейты 4a/4b/5 зелёные на железе -``` - -## Открытые мелочи (согласовать по ходу, не блокируют старт 4a) - -| Вопрос | Когда нужен | Предложение | -| --- | --- | --- | -| Имя env-переменной уровня лога | Фаза 4b | `SERVICE_LOG_LEVEL` (в стиле существующих `SERVICE_*`) | -| Имя just-задачи упаковки | Фаза 5 | согласовать по `Justfile`, не изобретаю | -| Формат имени релизного каталога | Фаза 5 | `service-tui-vX.Y.Z-` (из плана) | - ---- - -## Риски этого этапа - -| Риск | Фаза | Митигация | -| --- | --- | --- | -| `detect_sdp()` в error-пути erase сам упадёт/подвиснет (шина уже нестабильна) | 4a | обернуть проверку в try/except, при любой ошибке — считать «устройство пропало» (обрыв); проверка уже в error-пути, хуже не сделает | -| Троттлинг скроет полезную деталь при отладке | 4b | полный DEBUG остаётся через env-переключатель | -| PyInstaller не соберёт нативные libusbsio / data spsdk | 5 | документированный NXP путь (`collect_dynamic_libs`, `SPSDK_DATA_FOLDER`); риск на CI, не в поле | -| frozen-резолв путей разойдётся с onedir-структурой | 5 | резолвер уже написан и покрыт тестом `test_firmware_hab_path_frozen` | -| Регрессия dev-пути (`just host::flash*`) после зачистки | 6 | `flash_usb.py` не трогался ни в одной фазе (Р2); гейт 6 это проверяет | - ---- - -# Приложение: работа в новом треде - -Этот roadmap рассчитан на продолжение в **новом чате без контекста** -предыдущего. Ниже — всё, что нужно передать вместе с этим файлом, чтобы -новый тред стартовал без потерь. - -## A. Какой набор правил к чему применяется - -Проектные правила «Role & Hardware Context» (senior embedded C, i.MX -RT1052, LVGL, SDK HAL, C11, Doxygen, `.clang-tidy`/`.clang-format`, -CMake) написаны под **C/прошивочную** часть монорепо (`firmware_test`). - -**Вся работа этого roadmap (4a→4b→5→6) — Python/spsdk/Textual** в -`tools/service_tui`. Поэтому: - -| Правило | Применимо к Python-работе roadmap? | -| --- | --- | -| Unified diffs, не полные переписывания | ✅ Да | -| «Какой файл / какая функция затронуты» — первым | ✅ Да | -| ASK при неоднозначности/противоречии | ✅ Да | -| Не изобретать just-таски / пути / структуру | ✅ Да | -| Проверять существующие файлы перед правкой | ✅ Да | -| No malloc/free в драйверах и ISR | ❌ C-специфично | -| NXP SDK HAL вместо raw-регистров | ❌ C-специфично | -| Doxygen на public API | ❌ (Python — docstrings, уже используются) | -| `.clang-tidy`/`.clang-format` | ❌ (Python — стиль проекта: type hints, `from __future__ import annotations`) | -| CMake target_compile_options | ❌ Неприменимо | - -Когда/если roadmap коснётся C-части — C-правила снова в силе. - -## B. Первый вопрос на старте нового треда (не потерять) - -**Фаза 4a, вариант B (Р10):** `detect_sdp()` в error-пути `erase_chip` -предлагается обернуть в `try/except`, и **любую ошибку самой проверки** -(не только «устройство отсутствует») трактовать как обрыв — потому что -проверка и так выполняется только после уже случившегося сбоя, шина -нестабильна, и «не смог проверить» практически всегда означает «платы -нет». Требуется явное подтверждение этой трактовки перед написанием -кода Фазы 4a. (Альтернатива: ошибка самой проверки → обычный -`FlashBackendError`.) - -## C. Файлы, которые нужно предоставить — по фазам - -Пути относительно `tools/service_tui/`, если не указано иное. Пометка -**[есть в этом треде]** — файл уже фигурировал и его актуальная версия -известна; в новом треде его всё равно нужно приложить заново. - -### Фаза 4a — типизация обрыва - -| Файл | Зачем | -| --- | --- | -| `app/flash_backend.py` **[правится]** | основной файл фазы — обёртки обрыва + вариант B | -| `tests/test_flash_backend.py` **[правится]** | новые тесты на `SPSDKTimeoutError` и erase-переклассификацию | -| `app/flasher.py` | контекст: `_format_error_message` / `_run_flash_op` — убедиться, что `connection_lost` доходит до UI (не факт что правится) | -| `app/models.py` | контекст: `FlashProgress` | - -### Фаза 4b — логи - -| Файл | Зачем | -| --- | --- | -| `app/main.py` **[правится]** | уровни логгеров + env-переключатель DEBUG (Р12) | -| `app/screens/flash.py` **[правится]** | троттлинг `#flash-log` в `_on_progress` (Р11) | -| `.env` / `.env.example` (`tools/service_tui/`) | согласовать имя `SERVICE_LOG_LEVEL` с существующими переменными | - -### Фаза 5 — упаковка PyInstaller + UI - -| Файл | Зачем | -| --- | --- | -| `pyproject.toml` (`tools/service_tui/`) | зависимости, версия, `requires-python` — база для spec | -| `Justfile` + все `*.just` (корневой и подключаемые: `build.just`, `ci.just`, `host.just`) | **согласовать имя задачи упаковки, НЕ изобретать** — критично по правилу проекта | -| `app/main.py` | entry point для PyInstaller | -| `app/app.py` | `CSS_PATH="app.tcss"` — как резолвится во frozen | -| `app/app.tcss` | data-файл для бандла; правки под кнопку Quit | -| `app/screens/waiting.py` **[правится]** | кнопка «Выйти» (Предложение 2) | -| `app/flasher.py`, `app/flash_backend.py` | frozen-резолв путей (`firmware_hab_path`, `_resolve_custom_binaries_dir`) — проверить против структуры бандла | -| дерево `tools/host/dcd/` (список файлов) | что кладём в `datas` (`dcd.bin`, `*_fdcb.bin`, `ivt_flashloader.bin`) | -| `project_tree.txt` или `ls -R tools/service_tui` | реальная структура пакета `app/` для spec | -| существующий `.spec`, если уже есть | не изобретать заново | - -### Фаза 6 — документация и релиз - -| Файл | Зачем | -| --- | --- | -| `CHANGELOG.md` | дописать секцию монолита | -| `RELEASE_PLAN.md` | закрыть шаг 3 ссылкой на этот roadmap | -| `docs/DEV_ARCH.md` | §2 (диаграмма без subprocess), §8.3 (новый конвейер) | -| `HOW_TO_FLASH.md` | актуализировать под TUI-backend | -| `tools/service_tui/README.md` | ограничение О3, POST-1, разделение dev-CLI / production-TUI | -| `.env.example` | по Р9 (+`SERVICE_M5_VID/PID`, `FIRMWARE_BUILD_TYPE`, `SERVICE_LOG_LEVEL`) | -| `app/flash_backend.py`, `app/flasher.py`, `app/screens/flash.py`, `app/models.py` | финальная зачистка комментариев (grep-cleanup Гейта 4) | -| `tools/host/flash_usb.py` | сверка при зачистке — что dev-CLI и правда не тронут (Р2) | - -## D. Полный список актуальных файлов монолита (снимок на входе) - -Чтобы в новом треде можно было приложить всё разом, если удобнее не -дробить по фазам. Актуальные (пост-Фаза-4) версии: - -``` -tools/service_tui/ -├── pyproject.toml -├── app/ -│ ├── __init__.py -│ ├── app.py -│ ├── app.tcss -│ ├── main.py (точка входа — фактически в tools/service_tui/main.py, см. pyproject scripts) -│ ├── models.py -│ ├── flasher.py ← Фаза 2/4, актуальная версия -│ ├── flash_backend.py ← Фаза 1/4, актуальная версия (41 тест) -│ ├── usb_ports.py ← Фаза 1 -│ ├── firmware_client.py -│ ├── m5_client.py -│ ├── orchestrator.py -│ ├── boot_art.py -│ ├── widgets.py (или widgets/) -│ └── screens/ -│ ├── __init__.py -│ ├── waiting.py -│ ├── flash.py ← Фаза 4 (правлены комментарии) -│ ├── post_flash.py -│ ├── connection_watcher.py -│ └── diag/ -│ ├── __init__.py -│ ├── confirm_panel.py -│ ├── results.py -│ └── test_list.py -├── tests/ -│ ├── __init__.py -│ └── test_flash_backend.py ← 41 тест -├── spike/ (Фаза 0, в релиз не идёт) -│ ├── spike_hab.py -│ ├── spike_flash.py -│ └── spike_readback.py (диагностика Гейта 3, на будущее) -└── custom_binaries/ (пустая, для оператора) - -tools/host/ (dev-CLI, Р2 — НЕ трогается) -├── flash_usb.py -└── dcd/ - ├── ivt_flashloader.bin - ├── dcd.bin - ├── w25q128_fdcb.bin - └── w25q512_fdcb.bin -``` - -> Примечание: `main.py` в `pyproject.toml` прописан как -> `service-tui = "main:main"` — точка входа лежит в -> `tools/service_tui/main.py` (не в `app/`), а `app/app.py` содержит -> `ServiceApp`. Уточнить фактическое расположение при старте Фазы 4b/5. - -## E. Что уже решено и не пересматривается (сводка для нового треда) - -- **Р1–Р9** — см. `MONOLITH_APP_PLAN.md` (приложить его тоже). -- **Р10** — erase-таймаут → вариант B (detect_sdp после False). -- **Р11** — троттлинг лога 10%. -- **Р12** — уровни логов + env DEBUG. -- **О1** — M5 = нативный CDC `303A:4001`, драйверы не нужны. -- **О2** — `firmware/` = только firmware_test, тип сборки через `.env`. -- **О3** — одна плата на столе, ограничение v1. -- **POST-1** — циклический прогон тестов, после релиза. -- Публичный API `Flasher` заморожен; `flash.py`/`waiting.py`/`app.py` - меняются только там, где явно указано в roadmap. -- `flash_usb.py` (dev-CLI) не трогается ни в одной фазе. -- Порядок ревью: один файл за раз, полные файлы для новых/целиком - переписываемых, unified diff для точечных правок.