Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
121 lines
18 KiB
Python
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 получает результат после полного освобождения ресурсов.
|