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,170 @@
"""Transactional STM configuration extension (FC03/FC06, 0x1210)."""
import struct
from .qt_compat import QObject, QTimer, Signal
from .qt_compat import QSerialPort
from set_devices.tms_terminal import crc16_modbus
BAUDRATES = (9600, 19200, 38400, 57600, 115200)
def request(unit, function, register, value):
body = struct.pack(">BBHH", unit, function, register, value)
return body + struct.pack("<H", crc16_modbus(body))
def decode_config(raw):
if len(raw) != 25:
raise ValueError("Нужна прошивка STM с выбором режима эмуляции")
words = struct.unpack(">10H", raw[3:-2])
if words[:2] != (0x5343, 2):
raise ValueError("Прошивка STM не поддерживает настройку связи (нужна новая версия)")
address, tms, rate = words[2:5]
if not 1 <= address <= 247 or not 1 <= tms <= 255 or address == tms or rate >= len(BAUDRATES) or words[8] > 1 or words[9] > 1:
raise ValueError("STM вернула неверные параметры связи")
return words
class StmSettingsClient(QObject):
finished = Signal(bool, str, object)
def __init__(self, parent=None):
super().__init__(parent)
self.serial = QSerialPort(self)
self.serial.readyRead.connect(self._receive)
self.serial.errorOccurred.connect(self._error)
self.timeout = QTimer(self)
self.timeout.setSingleShot(True)
self.timeout.timeout.connect(lambda: self._finish(False, "Тайм-аут настройки STM"))
self.delay = QTimer(self)
self.delay.setSingleShot(True)
self.delay.timeout.connect(self._next)
self.active = False
self.committed = False
self.rx = bytearray()
def start(self, port, unit, baud, timeout, desired=None):
if self.active:
return
if desired is not None:
addr, tms, rate, mode = desired
if not 1 <= addr <= 247 or not 1 <= tms <= 255 or addr == tms or rate not in BAUDRATES or mode not in (0, 1):
self.finished.emit(False, "Адреса УМП и 2812 должны различаться; проверьте диапазоны и скорость", None)
return
self.unit, self.baud, self.timeout_ms = unit, baud, timeout
self.desired = desired
self.active, self.committed = True, False
self.stage = "probe"
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._send(3, 0x1210, 10)
def _send(self, function, register, value):
self.rx.clear()
self.packet = request(self.unit, function, register, value)
self.timeout.start(self.timeout_ms)
if self.stage == "commit":
# If ACK is lost, the board may nevertheless switch settings.
self.committed = True
if self.serial.write(self.packet) != len(self.packet):
self._finish(False, "Ошибка передачи настройки STM")
def _receive(self):
chunk = bytes(self.serial.readAll())
if not self.active or not self.timeout.isActive():
return
self.rx.extend(chunk)
function = self.packet[1]
while len(self.rx) >= 2:
if self.rx[0] != self.unit or self.rx[1] not in (function, function | 0x80):
del self.rx[0]
continue
size = 5 if self.rx[1] & 0x80 else (25 if function == 3 else 8)
if len(self.rx) < size:
return
raw = bytes(self.rx[:size])
del self.rx[:size]
if crc16_modbus(raw):
self._finish(False, "Неверная CRC ответа STM")
return
if raw[1] & 0x80:
self._finish(False, "STM отклонила настройку, код %d. Для старой прошивки требуется обновление." % raw[2])
return
if (function == 6 and raw != self.packet) or (function == 3 and raw[2] != 20):
self._finish(False, "Неверный ответ настройки STM")
return
self.timeout.stop()
try:
self._accept(raw)
except ValueError as error:
self._finish(False, str(error))
return
def _accept(self, raw):
if self.stage == "probe":
words = decode_config(raw)
if self.desired is None:
self._finish(True, "Настройки STM прочитаны", (words[2], words[3], BAUDRATES[words[4]], words[8]))
return
addr, tms, baud, mode = self.desired
self.staged = (addr, tms, BAUDRATES.index(baud), mode)
self.steps = [(6, 0x1218, 0), (6, 0x1215, addr), (6, 0x1216, tms),
(6, 0x1217, self.staged[2]), (6, 0x1219, mode), (3, 0x1210, 10)]
self.stage = "stage"
elif self.stage == "stage" and self.packet[1] == 3:
words = decode_config(raw)
if (*words[5:8], words[9]) != self.staged:
raise ValueError("STM не подтвердила подготовленные настройки")
self.stage = "commit"
self.steps = [(6, 0x1218, 0xA55A)]
elif self.stage == "commit":
self.stage = "switch"
self.delay.start(700)
return
elif self.stage == "verify":
words = decode_config(raw)
if (*words[2:5], words[8]) != self.staged:
raise ValueError("Новые настройки STM не подтвердились")
self._finish(True, "Настройки применены в STM и проверены. Действуют до перезагрузки платы.", self.desired)
return
self.delay.start(20)
def _next(self):
if not self.active:
return
if self.stage == "switch":
self.unit, _tms, baud, _mode = self.desired
if not self.serial.setBaudRate(baud):
self._finish(False, "Не удалось переключить скорость COM")
return
self.stage = "verify"
self._send(3, 0x1210, 10)
else:
self._send(*self.steps.pop(0))
def _error(self, error):
if self.active and error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError):
self._finish(False, self.serial.errorString())
def cancel(self):
if self.active:
self._finish(False, "Настройка прервана")
def _finish(self, ok, message, values=None):
if not self.active:
return
self.active = False
self.timeout.stop()
self.delay.stop()
if self.serial.isOpen():
self.serial.close()
if not ok and self.committed:
message += " Возможно, STM уже применила новые значения: адрес УМП %d, адрес 2812 %d, %d бод, режим %d (0=УМП, 1=2812). Повторите чтение по ним или перезагрузите плату." % self.desired
self.finished.emit(ok, message, values)