Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
137
python/set_devices/qt_ports/tms_bootloader.py
Normal file
137
python/set_devices/qt_ports/tms_bootloader.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""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)
|
||||
|
||||
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._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._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.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._response = None
|
||||
self._steps = wire.programming_steps(image.data, 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 "Прошивка завершена, записанные данные проверены")
|
||||
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
|
||||
if self._active:
|
||||
self._timeout.start(timeout + int(len(request) * 10000 / self._serial.baudRate()))
|
||||
|
||||
def _read(self):
|
||||
chunk = bytes(self._serial.readAll())
|
||||
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
|
||||
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._serial.isOpen():
|
||||
self._serial.close()
|
||||
if success:
|
||||
self.progress.emit(100, message)
|
||||
self.finished.emit(success, message)
|
||||
Reference in New Issue
Block a user