Объедини реализацию DEIF в C99-ядре, чтобы Python и Android использовали один кодек и общие эталонные кадры. Добавь две GAS-проекции единой карты STM Invertor и аппаратные порты эмулятора и загрузчика для платы HOPE STM32G474VET6. Проверки: сборка MSVC Debug, 24 теста CTest, 30 тестов Python и 29 дополнительных проверок, синтаксис 16 файлов Python.
182 lines
7.8 KiB
Python
182 lines
7.8 KiB
Python
"""Async COM adapter for direct RTU or automatic emulator discovery.
|
|
|
|
Panels retain their register/TMS decoders. Only this boundary translates them
|
|
to SET v2 on the wire. No protocol cache survives closing/reopening a port.
|
|
"""
|
|
import time
|
|
|
|
from .qt_compat import QObject, QTimer, Signal, QSerialPort # Общий порт не зависит от импортов SETGUI и поддерживает Qt6/Qt5.
|
|
from set_devices.emulator_protocol import EmulatorProtocol # Автомат SET v2/RTU находится рядом с остальными переносимыми моделями templates.
|
|
|
|
|
|
def response_timeout(port, milliseconds):
|
|
return port.response_timeout(milliseconds) if isinstance(port, EmulatorSerialPort) else milliseconds
|
|
|
|
|
|
def cancel_pending(port):
|
|
if isinstance(port, EmulatorSerialPort):
|
|
port.cancel_pending()
|
|
|
|
|
|
class EmulatorSerialPort(QObject):
|
|
readyRead = Signal()
|
|
errorOccurred = Signal(object)
|
|
protocolChanged = Signal(str)
|
|
wireEvent = Signal(str, bytes)
|
|
PROBE_MS = 300
|
|
|
|
def __init__(self, parent=None, port=None, *, protocol_mode='auto', rtu_suffix=b'', mcu="STM32F407"): # Профиль MCU фиксирует проверяемую модель прошивки, сохраняя все старые параметры порта.
|
|
super().__init__(parent)
|
|
self._port = port if port is not None else QSerialPort(self)
|
|
self._port.readyRead.connect(self._receive)
|
|
self._port.errorOccurred.connect(self.errorOccurred.emit)
|
|
self._probe = QTimer(self)
|
|
self._probe.setSingleShot(True)
|
|
self._probe.timeout.connect(self._probe_timeout)
|
|
self._protocol_mode = protocol_mode
|
|
self._rtu_suffix = bytes(rtu_suffix)
|
|
self._mcu = mcu # Аппаратная привязка сохраняется между закрытием и повторным открытием того же COM-адаптера.
|
|
self.protocol = EmulatorProtocol(protocol_mode, mcu=mcu) # Первичная конфигурация использует ожидаемую модель выбранного MCU.
|
|
self._rx = bytearray()
|
|
self._error = ''
|
|
self._deadline = 0
|
|
|
|
def __getattr__(self, name):
|
|
port = self.__dict__.get('_port')
|
|
if port is None:
|
|
raise AttributeError(name)
|
|
return getattr(port, name)
|
|
|
|
def open(self, mode):
|
|
self._probe.stop()
|
|
self._deadline = 0
|
|
self.protocol = EmulatorProtocol(self._protocol_mode, mcu=self._mcu) # Повторное открытие начинает новый handshake с той же аппаратной привязкой.
|
|
self._rx.clear()
|
|
self._error = ''
|
|
return self._port.open(mode)
|
|
|
|
def configure_protocol(self, mode, *, rtu_suffix=b''):
|
|
"""Choose the wire format before opening a new connection."""
|
|
if self.isOpen():
|
|
raise ValueError('Сначала отключите COM-порт для смены протокола')
|
|
protocol = EmulatorProtocol(mode, mcu=self._mcu) # Смена wire-формата не должна сбрасывать выбранный MCU на F407.
|
|
self.cancel_pending()
|
|
self._protocol_mode = mode
|
|
self._rtu_suffix = bytes(rtu_suffix)
|
|
self.protocol = protocol
|
|
self._rx.clear()
|
|
|
|
def close(self):
|
|
self.cancel_pending()
|
|
self._port.close()
|
|
self._rx.clear()
|
|
|
|
def set_mcu(self, mcu): # Профиль разрешается менять только до открытия физического соединения.
|
|
if self.isOpen(): # Открытая транзакция должна завершиться с первоначальной идентичностью устройства.
|
|
raise ValueError("Сначала отключите COM-порт для смены MCU") # Предотвращаем подмену цели незавершённого обмена.
|
|
protocol = EmulatorProtocol(self._protocol_mode, mcu=mcu) # Неизвестный профиль отвергается до изменения сохранённых параметров.
|
|
self._mcu = mcu # Следующее открытие порта проверит DeviceInfo именно этой платы.
|
|
self.protocol = protocol # Старое состояние обнаружения не переносится на другой MCU.
|
|
|
|
def cancel_pending(self):
|
|
self._probe.stop()
|
|
self._deadline = 0
|
|
self.protocol.cancel()
|
|
|
|
def response_timeout(self, milliseconds):
|
|
return milliseconds + (2 * self.PROBE_MS if self.protocol.mode == 'auto' else 0)
|
|
|
|
def errorString(self):
|
|
return self._error or self._port.errorString()
|
|
|
|
def clear(self, *args):
|
|
self.cancel_pending()
|
|
self._rx.clear()
|
|
return self._port.clear(*args)
|
|
|
|
def bytesAvailable(self):
|
|
return len(self._rx)
|
|
|
|
@property
|
|
def supports_wave_blocks(self):
|
|
return self.protocol.mode == 'v2'
|
|
|
|
def readAll(self):
|
|
result = bytes(self._rx)
|
|
self._rx.clear()
|
|
return result
|
|
|
|
def write(self, data):
|
|
self._error = ''
|
|
self._rx.clear()
|
|
try:
|
|
packet = self.protocol.begin(bytes(data))
|
|
return len(data) if self._send(packet) else -1
|
|
except ValueError as error:
|
|
self._fail(str(error))
|
|
return -1
|
|
|
|
def _send(self, packet):
|
|
self._probe.stop()
|
|
self._deadline = 0
|
|
if self.protocol.phase in ('ping', 'info'):
|
|
self._deadline = time.monotonic() + self.PROBE_MS / 1000
|
|
self._probe.start(self.PROBE_MS)
|
|
if self.protocol.mode == 'rtu':
|
|
packet += self._rtu_suffix
|
|
written = self._port.write(packet)
|
|
if written > 0:
|
|
self.wireEvent.emit('TX', packet[:written])
|
|
if written != len(packet):
|
|
self._fail('Не удалось передать запрос целиком')
|
|
return False
|
|
return True
|
|
|
|
def _receive(self):
|
|
raw = bytes(self._port.readAll())
|
|
if not raw:
|
|
return
|
|
self.wireEvent.emit('RX', raw)
|
|
try:
|
|
old_mode = self.protocol.mode
|
|
outgoing, incoming = self.protocol.feed(raw)
|
|
if self.protocol.mode != old_mode:
|
|
self.protocolChanged.emit('SET v2')
|
|
if outgoing and not self._send(outgoing):
|
|
return
|
|
if incoming:
|
|
self._rx.extend(incoming)
|
|
self.readyRead.emit()
|
|
except ValueError as error:
|
|
self._fail(str(error))
|
|
|
|
def _probe_timeout(self):
|
|
if not self._deadline:
|
|
return
|
|
if time.monotonic() < self._deadline:
|
|
self._probe.start(max(1, int((self._deadline-time.monotonic())*1000)+1))
|
|
return
|
|
try:
|
|
packet = self.protocol.fallback()
|
|
self.protocolChanged.emit('RTU')
|
|
self._send(packet)
|
|
except ValueError as error:
|
|
self._fail(str(error))
|
|
|
|
def _fail(self, message):
|
|
self._error = message
|
|
self.cancel_pending()
|
|
self.errorOccurred.emit(QSerialPort.SerialPortError.ReadError)
|
|
|
|
def waitForReadyRead(self, milliseconds):
|
|
# Used only in WaveTransfer's worker. No GUI event loop is blocked.
|
|
deadline = time.monotonic() + milliseconds / 1000
|
|
while not self._rx and not self._error and time.monotonic() < deadline:
|
|
if self._deadline and time.monotonic() >= self._deadline:
|
|
self._probe_timeout()
|
|
remaining = max(1, int((deadline-time.monotonic())*1000))
|
|
self._port.waitForReadyRead(min(remaining, 20))
|
|
if self._error:
|
|
raise OSError(self._error)
|
|
return bool(self._rx)
|