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

141 lines
6.9 KiB
Python

"""SET v2/RTU session for emulator panels; no I/O or Qt dependencies.
Only discovery may fall back to RTU. An application command is sent once;
failure after choosing v2 must never resend a write using another protocol.
"""
import secrets
import struct
from setprotocol.core import (Frame, FrameFlag, FrameParser, MessageType, Status,
DeviceInfo, build_frame, decode_response)
from set_devices.tms_terminal import crc16_modbus
from set_devices.mcu_profiles import EMULATOR_PROFILES # Проверка DeviceInfo использует модель конкретной платы из общего аппаратного профиля.
REG_REQUEST = 0x1000
TMS_REQUEST = 0x1001
WAVE_BLOCK = 0x1002
MODEL = 'STM32F407-EMU-EXPERIMENT'
def with_crc(body):
return body + struct.pack('<H', crc16_modbus(body))
class EmulatorProtocol:
def __init__(self, mode='auto', *, mcu="STM32F407"): # Необязательный MCU сохраняет старый API и задаёт ожидаемую идентичность до прикладных команд.
if mode not in ('auto', 'rtu'):
raise ValueError('Неизвестный режим протокола: ' + str(mode))
self.mode = mode
self.model = EMULATOR_PROFILES[mcu]["model"] # F407 и G474RB должны подтвердить собственную прошивку, а не только общий формат SET v2.
self.phase = None
self.parser = FrameParser()
self.sequence = secrets.randbelow(65535)
self.pending = b''
self.node = None
self.expected = None
def _frame(self, kind, payload=b'', destination=0):
self.sequence = (self.sequence + 1) & 0xFFFF
self.expected = (self.sequence, kind, destination)
return build_frame(Frame(kind, self.sequence, payload,
FrameFlag.ACK_REQUIRED, 0, destination))
def begin(self, raw):
self.pending = bytes(raw)
if self.mode == 'rtu':
return self.pending
if self.mode == 'v2':
return self._application()
self.parser.reset()
self.phase = 'ping'
return self._frame(MessageType.PING)
def fallback(self):
if self.phase != 'ping' or self.mode != 'auto':
raise ValueError('SET v2 обнаружен, но устройство не подтвердило совместимость')
self.mode, self.phase = 'rtu', None
self.parser.reset()
return self.pending
def cancel(self):
self.pending = b''
self.phase = None
self.expected = None
self.parser.reset()
def _application(self):
raw = self.pending
if not raw or crc16_modbus(raw):
raise ValueError('Неверный исходный запрос эмулятора')
self.phase = 'reply'
if len(raw) >= 9 and raw[1] == 16:
return self._frame(WAVE_BLOCK, raw, raw[0])
if len(raw) == 8 and raw[1] in (3, 6):
unit, fn, address, value = struct.unpack('>BBHH', raw[:6])
return self._frame(REG_REQUEST, struct.pack('<BHH', fn, address, value), unit)
if len(raw) == 18 and raw[1] == 65:
return self._frame(TMS_REQUEST, raw, self.node)
raise ValueError('Эта команда пока не поддерживается эмулятором SET v2')
def feed(self, chunk):
if self.mode == 'rtu':
return b'', bytes(chunk)
for frame in self.parser.feed(chunk):
if self.expected is None:
continue
seq, kind, destination = self.expected
if (frame.sequence != seq or frame.message_type != kind or
frame.destination != 0 or not frame.flags & FrameFlag.RESPONSE or
frame.flags & (FrameFlag.EVENT | FrameFlag.MORE) or
frame.source in (0, 65535) or
(destination and frame.source != destination)):
continue
status, body = decode_response(frame)
if bool(frame.flags & FrameFlag.ERROR) != bool(status):
raise ValueError('Несогласованные status/ERROR в ответе SET v2')
if self.phase == 'ping':
if status or len(body) != 4:
raise ValueError('Неверный PING SET v2')
self.node, self.phase = frame.source, 'info'
return self._frame(MessageType.DEVICE_INFO, destination=self.node), b''
if self.phase == 'info':
if status or DeviceInfo.decode(body).model != self.model: # Запись разрешается только после подтверждения аппаратного профиля выбранного MCU.
raise ValueError('SET v2: подключено другое устройство, команды эмулятора не отправлены')
self.mode = 'v2'
return self._application(), b''
if self.phase == 'reply':
response = self._reply(status, body)
self.phase, self.expected = None, None
return b'', response
return b'', b''
def _reply(self, status, body):
raw = self.pending
if raw[1] == 16:
if status:
raise ValueError(f'SET v2 отклонил блок генератора: status={status}')
# The shared C waveform codec validates CRC, address, count and echo.
return body
if raw[1] == 65:
if status == Status.UNSUPPORTED:
raise ValueError('Обновите прошивку STM: требуется CMD_STD через SET v2')
if status:
raise ValueError(f'SET v2 отклонил CMD_STD: status={status}')
if len(body) != 101 or body[:2] != raw[:2] or crc16_modbus(body[:-1]):
raise ValueError('Неверный ответ CMD_STD внутри SET v2')
return body
if status:
error = {Status.UNSUPPORTED: 1, Status.NOT_FOUND: 2,
Status.INVALID_ARGUMENT: 3, Status.INVALID_LENGTH: 3,
Status.BUSY: 6}.get(status, 4)
return with_crc(bytes([raw[0], raw[1] | 128, error]))
count = int.from_bytes(raw[4:6], 'big') if raw[1] == 3 else 2
if len(body) != 2 + 2 * count or struct.unpack_from('<H', body)[0] != count:
raise ValueError('SET v2: неверная длина ответа регистров')
words = struct.unpack_from(f'<{count}H', body, 2)
if raw[1] == 6:
if words != struct.unpack('>HH', raw[2:6]):
raise ValueError('SET v2: неверное подтверждение записи')
return raw
return with_crc(bytes([raw[0], 3, count * 2]) + struct.pack(f'>{count}H', *words))