Files
templates/python/set_devices/qt_ports/deif_emulator.py
T
Andrey 41e24629ce Расширить DEIF, загрузку CAN и общую обработку сигналов
Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
2026-10-24 15:40:51 +03:00

121 lines
18 KiB
Python

"""Асинхронное управление общим DEIF через SET v2 или RTU.""" # Обмен не зависит от приложения-потребителя и аппаратного порта MCU.
import struct # FC03 и FC06 используют порядок байтов Modbus big-endian внутри общего транспорта.
from .qt_compat import QObject, QTimer, Signal, QSerialPort # Поддерживает одинаковую машину обмена для Qt5 и Qt6.
from .emulator_serial import EmulatorSerialPort # Автомат обнаруживает протокол и проверяет модель выбранной платы.
from .stm_settings import request, decode_config # Настройки STM читаются тем же кодеком, что явный выбор эмулятора.
from set_devices.tms_terminal import crc16_modbus # CRC проверяется также при транспортировке RTU-команды внутри SET v2.
from set_devices.deif_emulator import DEIF_BASE, DEIF_WORDS, decode_snapshot # Адреса, сигнатура и интерпретация снимка общие для всех MCU.
class DeifEmulatorClient(QObject): # Каждая операция заканчивается закрытием COM, не удерживая общий порт между действиями.
finished = Signal(bool, str, object) # Возвращает подтверждённый снимок либо ошибку без ложного успеха записи.
def __init__(self, parent=None, *, serial_factory=EmulatorSerialPort): # Подмена фабрики предназначена для проверки на реальном C-ядре без оборудования.
super().__init__(parent) # QObject освобождает таймеры вместе с владельцем панели.
self.serial = serial_factory(self) # Один адаптер хранит выбранный MCU и выполняет безопасный handshake.
self.serial.readyRead.connect(self._receive) # Разбор допускает произвольную фрагментацию USB/UART.
self.serial.errorOccurred.connect(self._error) # Ошибка открытия или передачи завершает только текущую операцию.
self.timeout = QTimer(self) # Ограничение ожидания применяется отдельно к каждому запросу.
self.timeout.setSingleShot(True) # Тайм-аут не создаёт повторных записей со сторонними эффектами.
self.timeout.timeout.connect(lambda: self._finish(False, "Тайм-аут DEIF; состояние записи проверьте повторным чтением")) # Потерянный ответ не означает, что команда не исполнилась.
self.delay = QTimer(self) # Следующий запрос отправляется вне стека readyRead.
self.delay.setSingleShot(True) # Каждое подтверждение разрешает ровно один следующий шаг.
self.delay.timeout.connect(self._next) # Последовательные FC06 не перемешиваются с финальной проверкой.
self.active = False # Новая операция разрешена только после полного завершения предыдущей.
def start(self, port, unit, baud, timeout, writes=()): # Профиль связи передаётся потребителем явно, без глобального COM и адреса.
if self.active: # Повторное нажатие не должно сбросить ожидающую подтверждения запись.
return # Текущая операция сохраняет первоначальный профиль.
self.writes = tuple((int(offset), int(value) & 0xFFFF) for offset, value in writes) # RAW I16 кодируется теми же 16 битами без потери знака.
if any(offset not in (2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and not 32 <= offset < 44 for offset, _value in self.writes): # Только документированные writable слова версии 1 допускаются в очередь.
self.finished.emit(False, "Регистр DEIF недоступен для записи", None) # Локальная ошибка обнаруживается до открытия физического порта.
return # Чужие регистры и конфигурация STM не могут попасть в эту транзакцию.
self.unit, self.timeout_ms = unit, timeout # Адрес управления STM по умолчанию 16; тайм-аут задаётся в мс.
self.active, self.stage, self.rx = True, "config", bytearray() # Первая команда только читает действующий режим MCU.
self.serial.setPortName(port) # Выбор COM не открывает соединение до настройки формата.
self.serial.setBaudRate(baud) # USART использует выбранную скорость; для CDC это поле совместимости.
self.serial.setDataBits(QSerialPort.DataBits.Data8) # Восемь бит данных соответствуют аппаратным портам STM.
self.serial.setParity(QSerialPort.Parity.NoParity) # Проверку целостности выполняет CRC, формат UART без чётности.
self.serial.setStopBits(QSerialPort.StopBits.OneStop) # Один стоп-бит завершает формат 8N1.
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # RTS/CTS прошивкой не используются.
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite): # COM может быть занят другой программой.
self._finish(False, self.serial.errorString()) # Ошибка сообщает реальную причину открытия.
return # Ни чтение, ни запись при закрытом порте не выполняются.
self._send(3, 0x1210, 10) # Адрес и длина блока настроек STM взяты из set_emu_server.
def _send(self, function, address, value): # Запрос отправляется один раз; автоматического повтора записи нет.
self.packet = request(self.unit, function, address, value) # Общий кодек добавляет правильный CRC16.
self.rx.clear() # Байты предыдущего подтверждённого ответа не примешиваются к новому.
self.timeout.start(self.serial.response_timeout(self.timeout_ms)) # Первый обмен учитывает время определения SET v2/RTU.
if self.serial.write(self.packet) != len(self.packet): # Частичная постановка запроса в очередь не считается отправкой.
self._finish(False, "Не удалось передать запрос DEIF") # Операция останавливается без повторной передачи.
def _receive(self): # Накапливает только ответ текущего FC03 или FC06.
chunk = bytes(self.serial.readAll()) # Готовые байты забираются даже при отменённой операции.
if not self.active or not self.timeout.isActive(): # Поздний ответ не возобновляет отменённую последовательность.
return # Команда остаётся завершённой до нового явного действия пользователя.
self.rx.extend(chunk) # USB может разрезать ответ по любым границам.
function = self.packet[1] # Функция текущего запроса определяет размер ответа.
while len(self.rx) >= 3: # Заголовок содержит unit, function и byte count либо exception code.
if self.rx[0] != self.unit or self.rx[1] not in (function, function | 0x80): # Чужие либо старые кадры не подтверждают текущую операцию.
del self.rx[0] # Сдвиг на байт позволяет восстановиться после шумового префикса.
continue # Повторно ищет подходящий заголовок в оставшихся байтах.
expected = self.packet[5] * 2 if function == 3 else 0 # Все чтения короче 256 слов, старший байт количества равен нулю.
size = 5 if self.rx[1] & 0x80 else expected + 5 if function == 3 else 8 # Exception имеет 5 байт, FC06 echo — 8, FC03 — данные плюс заголовок и CRC.
if len(self.rx) < size: # Незавершённый ответ остаётся в буфере до readyRead или тайм-аута.
return # Никаких записей до полного подтверждения не выполняется.
raw = bytes(self.rx[:size]) # Фиксирует кадр до удаления из транспортного буфера.
del self.rx[:size] # Следующий шаг начнёт новый обмен с чистым буфером.
self.timeout.stop() # Полный ответ больше не ожидает истечения тайм-аута.
try: # Ошибки протокола должны возвращаться через finished, не покидая слот Qt.
if crc16_modbus(raw): # Нулевая CRC полного кадра подтверждает целостность.
raise ValueError("Неверная CRC ответа DEIF") # Повреждённый кадр не подтверждает запись.
if raw[1] & 0x80: # Старая прошивка либо неверный режим возвращает Modbus exception.
raise ValueError("STM отклонила DEIF, код %d; проверьте режим и версию прошивки" % raw[2]) # Показывает реальный код без предположения об успешном исполнении.
if function == 6 and raw != self.packet or function == 3 and raw[2] != expected: # Echo и фактическая длина должны точно совпасть с запросом.
raise ValueError("Неверное подтверждение DEIF") # Ответ другой команды не продвигает очередь.
self._accept(raw) # Только проверенный ответ может разрешить следующий шаг.
except ValueError as error: # Локальная проверка сигнатуры или роли имеет тот же путь завершения.
self._finish(False, str(error)) # Все таймеры и COM закрываются одинаково при любой ошибке.
return # За один сигнал обрабатывается только текущий запрос последовательности.
def _accept(self, raw): # Порядок: конфигурация, сигнатура DEIF, записи, итоговый снимок.
if self.stage == "config": # Чтение действующего режима предшествует любым командам DEIF.
self.mode = decode_config(raw)[8] # Общий декодер проверяет сигнатуру STM и допустимые режимы.
if self.writes and self.mode not in (4, 5): # Выбор режима выполняется отдельной явной транзакцией STM.
raise ValueError("Выберите DEIF Master или DEIF Slave в настройках STM и примените режим") # Чтение остаётся доступным для диагностики любой прошивки с DEIF.
self.stage = "probe" # Теперь нужно проверить версию и роль отдельного окна DEIF.
self.steps = [(3, DEIF_BASE, DEIF_WORDS)] # Одно чтение возвращает атомарный снимок 52 слов.
elif self.packet[1] == 3: # Окно читается перед записями и после них.
snapshot = decode_snapshot(struct.unpack(">%dH" % DEIF_WORDS, raw[3:-2]), self.mode) # Совместимость подтверждается данными реального MCU.
if self.stage == "probe" and self.writes: # Совместимый сервис разрешает подготовленные пользователем команды.
self.stage = "write" # С этой точки любой отказ может означать частично применённый профиль.
self.steps = [(6, DEIF_BASE + offset, value) for offset, value in self.writes] + [(3, DEIF_BASE, DEIF_WORDS)] # Финальное чтение обязательно даже после успешных echo.
else: # Чтение без записей и финальная проверка завершают транзакцию.
if any(snapshot.words[offset] != value for offset, value in dict(self.writes).items()): # Повторная запись того же регистра проверяется по последнему значению, включая временную сеть и нулевую маску при настройке.
raise ValueError("DEIF не подтвердил новые значения; повторите чтение состояния") # ACK передачи сам по себе не является проверкой применения.
self._finish(True, "Состояние DEIF прочитано и проверено", snapshot) # Только этот путь публикует подтверждённый снимок формы.
return # Завершённая операция не назначает следующий запрос.
self.delay.start(20) # Пауза 20 мс не блокирует GUI и совпадает с шагом клиента настроек STM.
def _next(self): # Слот таймера продвигает ровно одну команду подтверждённой последовательности.
if self.active and self.steps: # Отмена удаляет разрешение последующих запросов.
self._send(*self.steps.pop(0)) # Следующая запись выполняется только после echo предыдущей.
def _error(self, error): # Обрабатывает ошибки последовательного порта в том же GUI-потоке.
if self.active and error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError): # Собственный таймер уже отвечает за тайм-аут транзакции.
self._finish(False, self.serial.errorString()) # Не скрывает причину отключения или отказа COM.
def cancel(self): # Отмена доступна пользователю без смены режима на MCU.
if self.active: # Повторная отмена не создаёт повторных сигналов завершения.
self._finish(False, "Обмен DEIF отменён; проверьте состояние повторным чтением") # Уже подтверждённые записи не откатываются и не повторяются.
def _finish(self, ok, message, snapshot=None): # Единое завершение используется при успехе, отмене и ошибке.
if not self.active: # Поздняя ошибка Qt после close не должна затронуть новую операцию.
return # finished публикуется ровно один раз за запуск.
self.active = False # Состояние сбрасывается раньше закрытия COM и внешних обработчиков.
self.timeout.stop() # Ожидание текущего ответа больше не действует.
self.delay.stop() # Следующие записи не отправляются после ошибки.
self.serial.close() # Общий COM освобождается для логгера и других панелей.
self.finished.emit(ok, message, snapshot) # UI получает результат после полного освобождения ресурсов.