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