# Неблокирующая последовательность INITLOAD/LOAD/TFLASH для TMS. Ответы и таймауты управляют # переходами между этапами; размеры блоков и адреса берутся из протокола целевого контроллера. """Non-blocking port of Gui_Android's INITLOAD / LOAD / TFLASH workflow.""" from .qt_compat import QObject, QTimer, Signal from .qt_compat import QSerialPort from protocan import tms_firmware as wire class TmsBootloader(QObject): progress = Signal(int, str) finished = Signal(bool, str) request_sent = Signal(bytes) def __init__(self, parent=None): super().__init__(parent) self._serial = QSerialPort(self) self._serial.readyRead.connect(self._read) self._serial.errorOccurred.connect(self._error) self._owned_serial = self._serial self._owns_serial = True self._timeout = QTimer(self) self._timeout.setSingleShot(True) self._timeout.timeout.connect(lambda: self._finish(False, f"Тайм-аут {self._stage}")) self._settle = QTimer(self) self._settle.setSingleShot(True) self._settle.setInterval(100) self._settle.timeout.connect(self._accept) self._delay = QTimer(self) self._delay.setSingleShot(True) self._delay.setInterval(100) self._delay.timeout.connect(self._advance) self._active = False self._waiting = False self._rx = bytearray() self.readback = bytearray() self._stage = "" def start(self, image, port: str, baud: int, target: wire.TmsTarget): if self._active: return try: target.validate(len(image.data)) if not port or baud <= 0: raise ValueError("Неверные настройки COM-порта") except ValueError as error: self.finished.emit(False, str(error)) return self._image, self._target = image, target self._active = True self._serial = self._owned_serial self._owns_serial = True self._serial.setPortName(port) self._serial.setBaudRate(baud) self._serial.setDataBits(QSerialPort.DataBits.Data8) self._serial.setParity(QSerialPort.Parity.NoParity) self._serial.setStopBits(QSerialPort.StopBits.OneStop) self._serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) if not self._serial.open(QSerialPort.OpenModeFlag.ReadWrite): self._finish(False, self._serial.errorString()) return self._begin() def start_connected(self, image, serial, target: wire.TmsTarget): """Use an existing port; its owner must forward RX and handle errors.""" if self._active: return try: target.validate(len(image.data)) if not serial.isOpen() or serial.baudRate() <= 0: raise ValueError("Подключите COM-порт в настройках 2812") except ValueError as error: self.finished.emit(False, str(error)) return self._image, self._target = image, target self._serial = serial self._owns_serial = False self._active = True self._begin() def _begin(self): self.readback.clear() self._response = None self._steps = wire.programming_steps(self._image.data, self._target) self._advance() def _advance(self): if not self._active: return try: request, self._size, timeout, self._stage, percent = self._steps.send(self._response) except StopIteration: self._finish(True, "Образ загружен в RAM TMS" if self._target.load_only else "Запись Spartan-6 подтверждена платой" if self._target.kind == "spartan6" else "Прошивка записана; проверка отключена" if not self._target.verify else "Прошивка завершена, записанные данные проверены") return except (ValueError, RuntimeError) as error: self._finish(False, str(error)) return self._command = request[1] self._rx.clear() self._waiting = True self.progress.emit(percent, self._stage) # LOAD is one continuous write, without pauses inside its packet. if self._serial.write(request) != len(request): self._finish(False, "Ошибка передачи " + self._stage) return self.request_sent.emit(request) if self._active: self._timeout.start(timeout + int((len(request) + self._size) * 10000 / self._serial.baudRate())) def _read(self): self.receive(bytes(self._serial.readAll())) def receive(self, chunk): """Forward fragments from a shared port without reading it twice.""" if not self._waiting: return self._rx.extend(chunk) header = bytes((self._target.controller, self._command)) start = self._rx.find(header) if start < 0: self._rx[:] = self._rx[-1:] return del self._rx[:start] # Some devices send an ACK before an UPLOAD data response. if self._size > 6: for ack_size in (6, 4): if (self._rx[ack_size:ack_size + 2] == header and wire.normalize_reply(bytes(self._rx[:ack_size]), *header, 6)): del self._rx[:ack_size] break if len(self._rx) >= self._size: self._accept() else: self._settle.start() def _accept(self): if not self._waiting: return reply = wire.normalize_reply(bytes(self._rx[:self._size]), self._target.controller, self._command, self._size) if reply is None: if len(self._rx) >= self._size: self._finish(False, "Неверный CRC ответа " + self._stage) return self._timeout.stop() self._settle.stop() self._waiting = False if self._command == wire.CMD_UPLOAD and self._target.kind == "tms": data = wire.decode_upload_reply(reply, self._target.controller, self._size - 8) remaining = len(self._image.data) - len(self.readback) self.readback.extend(data[:remaining]) self._response = reply self._delay.start() def cancel(self): if self._active: self._finish(False, "Операция TMS отменена; запись Flash могла быть начата") def _error(self, error): if self._active and error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError): self._finish(False, self._serial.errorString()) def _finish(self, success, message): if not self._active: return self._active = self._waiting = False for timer in (self._timeout, self._settle, self._delay): timer.stop() if self._owns_serial and self._serial.isOpen(): self._serial.close() if not success: self.readback.clear() if success: self.progress.emit(100, message) self.finished.emit(success, message)