# firmware_test: service-tui is ready for packing, docs updated
This commit is contained in:
parent
2a6256e745
commit
e50b2f35aa
15 changed files with 606 additions and 59 deletions
|
|
@ -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_<n>, uart_<n>
|
||||
│ ├── pyocd_utils.py ← FLEXRAM init, ELF loader, run_from_vectors
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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="Соединение с платой потеряно")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
tools/production/custom_binaries/TFT_BOOTLOADER_NEW.bin
Normal file
BIN
tools/production/custom_binaries/TFT_BOOTLOADER_NEW.bin
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
tools/production/custom_binaries/TFT_BOOTLOADER_OLD.bin
Normal file
BIN
tools/production/custom_binaries/TFT_BOOTLOADER_OLD.bin
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -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 <yaml> -o <out>,
|
||||
│ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath
|
||||
│ │ резолвится от этой директории, как в build.just)
|
||||
│ └── стриминг stdout nxpimage в progress_cb (не только logger.debug —
|
||||
│ иначе во время сборки лог FlashScreen выглядит «зависшим»)
|
||||
└── flash_usb.py --bin-path <hab_bin> --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 <fcb_path> — буквальная запись 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` для этих чипов корректно в принципе — вопрос
|
||||
снят с повестки архитектурным решением, а не исследован до конца.
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "service-tui"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
description = "TUI сервисного инженера для диагностики платы MIMXRT1052"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "service-tui"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "pyinstaller" },
|
||||
|
|
|
|||
Loading…
Reference in a new issue