"""Non-blocking client for the STM32 ROM UART bootloader described by AN3155.""" from __future__ import annotations from collections import deque from typing import Callable from .qt_compat import QObject, QTimer, Signal from .qt_compat import QSerialPort from set_devices.firmware import FirmwareImage from protocan.stm32_boot import ( ACK, NACK, SYNC, address, command, erase_pages_payload, write_payload, ) class Stm32Bootloader(QObject): """Programs STM32 system Flash over the factory UART bootloader (8E1).""" progress = Signal(int, str) finished = Signal(bool, str) def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) self._serial = QSerialPort(self) self._serial.readyRead.connect(self._read_available) self._serial.errorOccurred.connect(self._serial_error) self._timer = QTimer(self) self._timer.setSingleShot(True) self._timer.timeout.connect(lambda: self._fail("Нет ответа STM32 bootloader")) self._rx = bytearray() self._steps: deque[tuple[bytes, int, str, Callable[[], None]]] = deque() self._waiting: tuple[int, str, Callable[[], None]] | None = None self._image: FirmwareImage | None = None self._offset = 0 self._cancelled = False def start(self, image: FirmwareImage, port_name: str, baud_rate: int) -> None: if self._serial.isOpen() or self._waiting is not None: self.finished.emit(False, "Прошивка STM32 уже выполняется") return if not port_name or baud_rate <= 0: self.finished.emit(False, "Неверные настройки UART") return if not 0x08000000 <= image.base_address <= 0x080FFFFF: self.finished.emit(False, "Для STM32 укажите адрес Flash, например 0x08000000") return if image.base_address + len(image.data) > 0x08100000: self.finished.emit(False, "Образ выходит за допустимый диапазон Flash STM32") return self._serial.setPortName(port_name) self._serial.setBaudRate(baud_rate) self._serial.setDataBits(QSerialPort.DataBits.Data8) self._serial.setParity(QSerialPort.Parity.EvenParity) self._serial.setStopBits(QSerialPort.StopBits.OneStop) self._serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) if not self._serial.open(QSerialPort.OpenModeFlag.ReadWrite): self.finished.emit(False, f"Не удалось открыть {port_name}: {self._serial.errorString()}") return self._image = image self._offset = 0 self._cancelled = False self._rx.clear() self._steps.clear() self.progress.emit(0, "Синхронизация с STM32 (BOOT0=1)") self._queue(SYNC, 1500, "синхронизации", self._erase) self._next() def cancel(self) -> None: if not self._serial.isOpen(): return self._cancelled = True self._finish(False, "Операция отменена") def _queue(self, packet: bytes, timeout_ms: int, stage: str, callback: Callable[[], None]) -> None: self._steps.append((packet, timeout_ms, stage, callback)) def _next(self) -> None: if self._waiting is not None or not self._steps or self._cancelled: return packet, timeout_ms, stage, callback = self._steps.popleft() if self._serial.write(packet) != len(packet): self._fail(f"Ошибка передачи на этапе {stage}") return self._waiting = (timeout_ms, stage, callback) self._timer.start(timeout_ms) def _erase(self) -> None: self.progress.emit(1, "Стирание Flash") self._queue(command(0x43), 1500, "команды Erase", self._erase_payload) self._next() def _erase_payload(self) -> None: assert self._image is not None first = (self._image.base_address - 0x08000000) // 1024 last = (self._image.base_address + len(self._image.data) - 1 - 0x08000000) // 1024 pages = list(range(first, last + 1)) self._queue(erase_pages_payload(pages), 20000, "стирания страниц Flash", self._write_next) self._next() def _write_next(self) -> None: image = self._image if image is None: return if self._offset >= len(image.data): self.progress.emit(100, "Запуск приложения") self._queue(command(0x21), 1500, "команды Go", self._go_address) self._next() return chunk = image.data[self._offset:self._offset + 256] # STM32 Flash is programmed by words; erased bytes safely pad the tail. if len(chunk) & 3: chunk += b"\xff" * (4 - (len(chunk) & 3)) self._queue(command(0x31), 1500, "команды Write Memory", self._write_address) self._next() def _write_address(self) -> None: assert self._image is not None self._queue(address(self._image.base_address + self._offset), 1500, "адреса блока", self._write_data) self._next() def _write_data(self) -> None: assert self._image is not None chunk = self._image.data[self._offset:self._offset + 256] actual = len(chunk) if len(chunk) & 3: chunk += b"\xff" * (4 - (len(chunk) & 3)) def accepted() -> None: self._offset += actual percent = int(self._offset * 100 / len(self._image.data)) self.progress.emit(percent, f"Записано {self._offset} байт") self._write_next() self._queue(write_payload(chunk), 2500, "записи блока", accepted) self._next() def _go_address(self) -> None: assert self._image is not None self._queue(address(self._image.base_address), 1500, "адреса запуска", lambda: self._finish(True, "Прошивка STM32 завершена, приложение запущено")) self._next() def _read_available(self) -> None: self._rx.extend(bytes(self._serial.readAll())) while self._waiting is not None and self._rx: response = self._rx.pop(0) timeout_ms, stage, callback = self._waiting if response not in (ACK, NACK): continue self._timer.stop() self._waiting = None if response == NACK: self._fail(f"STM32 отклонил операцию на этапе {stage}") return callback() def _serial_error(self, error: QSerialPort.SerialPortError) -> None: if error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError): self._fail(self._serial.errorString()) def _fail(self, message: str) -> None: self._finish(False, message) def _finish(self, success: bool, message: str) -> None: self._timer.stop() self._waiting = None self._steps.clear() self._image = None if self._serial.isOpen(): self._serial.close() self.progress.emit(100 if success else 0, message) self.finished.emit(success, message)