Extract reusable device protocols, ports and logic analyzers from SETGUI

This commit is contained in:
2026-09-23 20:11:37 +03:00
parent 80ba17d77d
commit 795a1279b1
63 changed files with 10181 additions and 6 deletions

View File

@@ -0,0 +1,177 @@
"""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)