feat(set-protocol): Добавь DEIF XDi, STM Invertor и порт HOPE G474
Объедини реализацию DEIF в C99-ядре, чтобы Python и Android использовали один кодек и общие эталонные кадры. Добавь две GAS-проекции единой карты STM Invertor и аппаратные порты эмулятора и загрузчика для платы HOPE STM32G474VET6. Проверки: сборка MSVC Debug, 24 теста CTest, 30 тестов Python и 29 дополнительных проверок, синтаксис 16 файлов Python.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import ctypes # Привязка передаёт готовые слова общему C-кодеку без повторения wire-формата на Python.
|
||||
from .native import _load_library # Стандартный поиск сохраняет существующий механизм доставки SETProtocol.
|
||||
class StmInvertorCodec: # Один экземпляр владеет только сигнатурами функций общей библиотеки.
|
||||
def __init__(self, library=None): # Явная DLL позволяет новой панели не заменять библиотеку работающего приложения.
|
||||
self.lib = library if library is not None else _load_library() # Нет Python fallback при отсутствии нового C-кодека.
|
||||
self.lib.set_invertor_gas_limit.argtypes = [ctypes.c_uint] # Тип сообщения передаётся без вычисления геометрии карты в Python.
|
||||
self.lib.set_invertor_gas_limit.restype = ctypes.c_uint # Границы определяются профилем C из Templates.
|
||||
self.lib.set_invertor_gas_build.argtypes = [ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint16), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint8)] # Типы совпадают с публичным C ABI общего адаптера.
|
||||
self.lib.set_invertor_gas_build.restype = ctypes.c_int # C возвращает фактический DLC либо отрицательную ошибку.
|
||||
self.lib.set_invertor_gas_decode.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint16)] # Декодер проверяет ID, тип, направление, длину и маску.
|
||||
self.lib.set_invertor_gas_decode.restype = ctypes.c_int # Число принятых wire-слов приходит только из C.
|
||||
def limit(self, message_type): # Интерфейс не хранит отдельную копию константы 250 или 750.
|
||||
return int(self.lib.set_invertor_gas_limit(message_type)) # Передаём актуальную границу виджету адреса.
|
||||
def build(self, message_type, address, words=()): # Пустой набор слов означает чтение DLC0 в обоих протоколах.
|
||||
if not 0 <= address <= 0xFFFF or len(words) > 4 or any(not 0 <= word <= 0xFFFF for word in words): # Проверяем представимость Python-чисел перед сужением до типов ABI.
|
||||
raise ValueError('Адрес или значение вне диапазона ABI') # Не допускаем тихого усечения и циклического переноса ctypes.
|
||||
source = (ctypes.c_uint16 * len(words))(*words) if words else None # Значения остаются словами, endian выбирает общий кодек.
|
||||
identifier = ctypes.c_uint32(); data = (ctypes.c_uint8 * 8)() # Локальные буферы имеют размеры Classic CAN и не переживают вызов.
|
||||
length = self.lib.set_invertor_gas_build(message_type, address, source, len(words), ctypes.byref(identifier), data) # Ни CAN ID, ни маска не строятся здесь вручную.
|
||||
if length < 0: # C отклоняет неверную геометрию, длину блока и маску.
|
||||
raise ValueError('GAS: неверный адрес, длина блока или маска Word0') # Пользователь видит причину отказа до передачи на шину.
|
||||
return identifier.value, bytes(data[:length]) # Транспорт получает полностью готовые ID и октеты.
|
||||
def decode(self, identifier, flags, payload): # Входной кадр проверяется до публикации значений интерфейсу.
|
||||
if not 0 <= identifier <= 0x1FFFFFFF or len(payload) > 8: # Не допускаем сужения большого CAN ID и переполнения временного буфера.
|
||||
return None # Кадры другого транспорта не считаются ответом инвертора.
|
||||
data = (ctypes.c_uint8 * 8)(*payload); metadata = (ctypes.c_uint * 2)(); words = (ctypes.c_uint16 * 4)() # ABI работает с фиксированными массивами без указателей на объекты Python.
|
||||
count = self.lib.set_invertor_gas_decode(identifier, flags, data, len(payload), metadata, words) # Wire-разбор остаётся единственной C-реализацией.
|
||||
return (metadata[0], metadata[1], tuple(words[:count])) if count > 0 else None # Недопустимый кадр не изменяет отображённые значения.
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Готовые операции DEIF для GUI без знания wire-разметки и адресных масок.""" # Все числовые правила реализованы единственным C99-ядром templates.
|
||||
import ctypes as C # Преобразование фиксированных ABI-структур не является вторым кодеком.
|
||||
from dataclasses import dataclass # Именованные поля исключают индексы метаданных в GUI.
|
||||
from .deif_native import get_deif_native, Frame # Общий декодер и builders используются независимо от приложения.
|
||||
from .can_transport import build_frame # Транспорт получает уже готовые CAN-кадры.
|
||||
@dataclass(frozen=True) # Неизменяемые описания безопасно используются диагностикой и редакторами.
|
||||
class SelectorInfo: # Это скалярный результат C, а не wire-структура.
|
||||
node: int; index: int; subindex: int; dam: bool; legacy_key: str # Последний элемент сохраняет только формат прежних ключей настроек Desktop.
|
||||
@dataclass(frozen=True) # Описание входного кадра отделено от его транспортного контейнера.
|
||||
class PacketInfo: # GUI не обращается к смещениям payload.
|
||||
kind: int; node: int; index: int; subindex: int; raw: int; status: int; dam: bool # Все значения получены из C-декодера.
|
||||
@property # Диагностический ключ независим от адресного байта SAM/DAM.
|
||||
def object_key(self): # Для TX-эхо достаточно индекса и подиндекса готового объекта.
|
||||
return self.index, self.subindex # Это идентичность объекта, а не повторная распаковка CAN-data.
|
||||
def selector_info(selector): # Редактор получает имена полей вместо ручных масок.
|
||||
raw = bytes.fromhex(selector) if isinstance(selector, str) else bytes(selector) # HEX — только формат хранения пользовательской настройки.
|
||||
if len(raw) != 4: return None # Буфер неправильного размера не передаётся C.
|
||||
metadata = (C.c_int32 * 4)() # Node, index, subindex и DAM определены публичным ABI.
|
||||
if not get_deif_native().selector_info(raw, metadata): return None # Некорректная привязка не становится источником на рисунке.
|
||||
return SelectorInfo(metadata[0], metadata[1], metadata[2], bool(metadata[3]), raw[1:].hex()) # Срез используется только для совместимости непрозрачного старого ключа настроек.
|
||||
def readdress_selector(selector, node, dam=None): # Меняет адрес пользовательской привязки общим алгоритмом C.
|
||||
if not selector: return "" # Явно отключённое поле остаётся отключённым.
|
||||
raw = bytes.fromhex(selector) if isinstance(selector, str) else bytes(selector) # Порт преобразует текст настройки в буфер ABI.
|
||||
output = (C.c_uint8 * 4)() # Готовый селектор создаёт C-код.
|
||||
if len(raw) != 4 or not get_deif_native().readdress(raw, node, 2 if dam is None else int(bool(dam)), output): raise ValueError("Неверный селектор или Node-ID DEIF") # Индекс не пересобирается в Python.
|
||||
return bytes(output).hex(" ").upper() # Формат вывода совместим с прежним редактором HEX.
|
||||
def route_parameters(node, mode, identifier, operation=0): # Согласует параметры редактора с C-правилами адресации.
|
||||
output = (C.c_uint32 * 3)() # COB-ID, Node и адресный байт возвращаются одним вызовом.
|
||||
if not get_deif_native().route(node, mode == "dam", identifier, operation, output): return None # Неверный диапазон не вызывает произвольную перенастройку привязок.
|
||||
return tuple(output) # GUI хранит и отображает готовые значения.
|
||||
def data_node(identifier, fallback=1): # Получает узел из CAN ID без повторения маски в GUI.
|
||||
result = route_parameters(fallback or 1, "sam", identifier, 2) # Legacy SAM сохраняет явно выбранный источник.
|
||||
return result[1] if result else fallback # Вне диапазона данных остаётся пользовательское исходное значение.
|
||||
def packet_info(frame): # Метаданные для подтверждения команд и TX-эхо.
|
||||
code, metadata = get_deif_native().inspect_frame(frame.can_id, frame.data, frame.ide, frame.rtr) # Wire-разбор находится в C.
|
||||
return PacketInfo(metadata[0], metadata[1], metadata[2], metadata[3], metadata[4], metadata[7], bool(metadata[8])) if code > 0 else None # Некорректный кадр не получает выдуманных полей.
|
||||
def heartbeat_description(identifier, data, extended=False, rtr=False): # Приложение не содержит собственного классификатора CANopen error control.
|
||||
code, _ = get_deif_native().inspect_frame(identifier, data, extended, rtr) # Проверку COB-ID, DLC и состояния выполняет C.
|
||||
if code not in (2, -3): return None # Обычные MPDO не попадают в поле heartbeat.
|
||||
from .deif_xdi import decode # Общий локализованный formatter используется обоими CAN-мониторами и прибором.
|
||||
return decode(build_frame(identifier, bytes(data), ide=extended, rtr=rtr)).summary # Готовая подпись не подтверждает получение конкретного измерения.
|
||||
def _frame(output): # Общий адаптер переводит структуру C в существующий транспортный контейнер.
|
||||
return build_frame(output.can_id, bytes(output.data[:output.dlc]), ide=False) # Идентификатор, длина и данные уже вычислены ядром.
|
||||
def service_frame(node, value, nmt=False): # Строит heartbeat либо локальную NMT-команду через C.
|
||||
output = Frame() # Память результата принадлежит Python-порту.
|
||||
if not get_deif_native().service(bool(nmt), node, value, C.byref(output)): raise ValueError("Неверный Node-ID или CANopen-сервис") # Ни COB-ID, ни служебные байты не собираются в GUI.
|
||||
return _frame(output) # Физическую отправку выполняет выбранный приложением транспорт.
|
||||
def angle_setpoint_frame(actual): # Формирует отдельный кадр задания с тем же значением фактического азимута.
|
||||
source, output = Frame(), Frame() # Оба контейнера имеют стабильный layout ABI.
|
||||
source.can_id, source.dlc, source.data[:] = actual.can_id, len(actual.data), bytes(actual.data) # Это копирование готового CAN-кадра, не декодирование его полей.
|
||||
if not get_deif_native().angle_command(C.byref(source), C.byref(output)): raise ValueError("Кадр не содержит поддерживаемый азимут DEIF") # Проверку семейства и сборку 0x07 выполняет C.
|
||||
return _frame(output) # Штатный передатчик получает второй независимый MPDO.
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Независимая от GUI Python-модель индикатора поверх общего C99-ядра DEIF.""" # Настройки и транспортные имена остаются в порту, wire-правила находятся в C.
|
||||
import ctypes as C # Обмен с ABI использует фиксированные массивы и caller-owned память.
|
||||
from dataclasses import dataclass # Привязка является простой моделью пользовательской настройки.
|
||||
from time import monotonic # Порт передаёт часы ядру явно в миллисекундах.
|
||||
from .deif_native import get_deif_native, Frame # Единственная реализация кодека и автомата загружается без Qt.
|
||||
|
||||
FIELDS = ("power", "rpm", "angle", "power_percent", "rpm_percent", "angle_setpoint") # Порядок полей публичного ABI C; это имена модели, не wire-селекторы.
|
||||
STATUS = {1: "ниже диапазона", 2: "выше диапазона", 3: "ошибка данных"} # Локализованные подписи готовых статусов C.
|
||||
|
||||
|
||||
@dataclass # Привязки можно сохранять в существующих пользовательских профилях JSON.
|
||||
class Binding: # Четыре HEX-байта и коэффициент поступают из настроек либо C-каталога.
|
||||
selector: str = "" # Пустая строка означает явно отключённое поле.
|
||||
scale: float = 1.0 # Коэффициент применяется только в общем C-ядре.
|
||||
|
||||
|
||||
def _milliseconds(now): # Перевод единиц системных часов относится к платформенному порту.
|
||||
return max(0, round((monotonic() if now is None else now) * 1000)) # ABI использует целые монотонные миллисекунды для одинаковых границ на всех платформах.
|
||||
|
||||
|
||||
def validate_deif_transmit(frame): # Сохраняет прежний публичный Python API ограниченного маршрута MASTER.
|
||||
if not get_deif_native().validate(frame.can_id, bytes(frame.data), len(frame.data), int(bool(frame.ide)) | (int(bool(frame.rtr)) << 1), 0): # Проверка адреса, объектов и размеров полностью выполняется в C.
|
||||
raise ValueError("DEIF: разрешены только данные, яркость, Color и heartbeat; команды настройки через Node-ID запрещены") # Текст остаётся локализованной ответственностью приложения.
|
||||
|
||||
|
||||
def brightness_frame(node_id, group, percent, can_id=None, mode="sam"): # Готовит один кадр без отправки или изменения прибора.
|
||||
return get_deif_native().make_frame(6, node_id, percent, 0 if can_id is None else can_id, mode, group) # Поле ABI 6 обозначает яркость, исходный COB-ID и U8 определяет C.
|
||||
|
||||
|
||||
def colour_palette_frame(node_id, group, palette, can_id=None, mode="sam"): # Готовит независимый от яркости кадр палитры.
|
||||
return get_deif_native().make_frame(7, node_id, palette, 0 if can_id is None else can_id, mode, group) # Поле ABI 7 обозначает Color; исходный COB-ID и кодирование Day/Night/Dusk находятся в C.
|
||||
|
||||
|
||||
class DeifDisplay: # Пассивная модель пригодна для Desktop, CLI и тестов без Qt.
|
||||
services = False # Наблюдатель не объявляет себя узлом CAN и никогда не выдаёт heartbeat.
|
||||
expiry_ms = 5000 # Прежний режим наблюдения скрывает показания после пяти секунд без обновления.
|
||||
|
||||
def __init__(self, source="DevBoard", can_id=0x201, power_unit="RAW", device_node=None, bindings=None, xdi_net=True, self_start=True, dam_pdo=2, dimmer_group=1, source_node=1): # Дополнительные параметры передаются серверным портом, прежние профили остаются совместимыми.
|
||||
self.source, self.can_id, self.power_unit, self.device_node = source, can_id, power_unit, device_node # Только имя транспорта и UI-единица остаются вне C.
|
||||
self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self._source_node = xdi_net, self_start, dam_pdo, dimmer_group, source_node # Параметры профиля передаются C без копии правил протокола.
|
||||
self._api, self._context, self._route, self._bindings_signature = get_deif_native(), None, None, None # Один контекст на прибор исключает смешивание показаний MASTER и SLAVE.
|
||||
self.bindings = {} # Контейнер поддерживает прямое редактирование существующим диалогом привязок.
|
||||
self._ensure_context() # Исходные селекторы и коэффициенты создаются единственным C-каталогом.
|
||||
if bindings is None: # Новый профиль получает штатные привязки, включая азимут.
|
||||
for index, key in enumerate(FIELDS[:5]): # Отдельное задание угла автоматически производно от привязки азимута в C.
|
||||
selector, factor = (C.c_uint8 * 4)(), C.c_double() # Порт принимает готовые значения без разбора endian.
|
||||
self._api.get_binding(self._context, index, selector, C.byref(factor)) # Настройки по умолчанию читаются из C-профиля.
|
||||
self.bindings[key] = Binding(bytes(selector).hex(" ").upper(), factor.value) # HEX нужен только редактору настроек.
|
||||
else: # Существующая библиотека сохраняет пользовательские объекты и масштабы.
|
||||
self.bindings = {key: Binding(item.selector, item.scale) for key, item in bindings.items()} # Словари двух приборов не должны делить изменяемые Binding.
|
||||
|
||||
def _ensure_context(self): # Изменение адреса требует нового C-контекста, а смена имени транспорта — нет.
|
||||
route = (self.can_id, self.device_node, self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self.services, self._source_node) # Изменение источника также требует согласованной переинициализации.
|
||||
if route == self._route: # Повторное чтение снимка не переинициализирует данные.
|
||||
return # Сохраняет сроки и очередь работающего узла.
|
||||
size = self._api.state_size() # Размер private C-структуры сообщает библиотека.
|
||||
self._context = (C.c_uint64 * ((size + 7) // 8))() # uint64 обеспечивает необходимое выравнивание на Windows и ARM.
|
||||
source = self._source_node # Источник профиля не вычисляется из имени GUI или физического Node-ID SLAVE.
|
||||
if not self._api.init(self._context, size, self.device_node or 0, source, bool(self.xdi_net), bool(self.self_start), self.dam_pdo, self.dimmer_group, bool(self.services), self.can_id): # C проверяет reserved Node, RPDO и сетевые группы.
|
||||
raise ValueError("DEIF: неверный профиль SLAVE, Node-ID или RPDO") # Частично инициализированный узел не используется.
|
||||
self._route, self._bindings_signature = route, None # Привязки повторно синхронизируются после изменения профиля.
|
||||
|
||||
def _sync_bindings(self): # Прямые изменения словаря UI передаются в C только при фактическом изменении настройки.
|
||||
self._ensure_context() # Проверяет, не был ли изменён физический адрес либо CAN ID наблюдения.
|
||||
signature = tuple((key, binding.selector, binding.scale) for key, binding in self.bindings.items()) # Это сравнение настроек, а не интерпретация wire-формата.
|
||||
if signature == self._bindings_signature: # Частые тики и приём кадров не должны сбрасывать значения.
|
||||
return # Состояние C остаётся прежним.
|
||||
for index, key in enumerate(FIELDS): # Порядок соответствует публичным field IDs ядра.
|
||||
if index == 5 and key not in self.bindings: # Производное задание азимута настраивает C при изменении поля angle.
|
||||
continue # Не отключает автоматически созданную независимую привязку 0x07.
|
||||
binding = self.bindings.get(key, Binding()) # Отсутствующее поле явно отключается.
|
||||
selector = bytes.fromhex(binding.selector) if binding.selector else None # HEX является форматом хранения настроек, а не CAN-декодером.
|
||||
if selector is not None and len(selector) != 4: # Порт проверяет размер буфера до передачи указателя C.
|
||||
raise ValueError("Селектор DEIF должен содержать четыре байта") # Некорректная пользовательская настройка не создаёт чтение за пределами буфера.
|
||||
if not self._api.bind(self._context, index, selector, binding.scale): # Интерпретация селектора и применение масштаба находятся в C.
|
||||
raise ValueError("DEIF: неверная привязка или масштаб") # Нечисловой коэффициент не скрывается как отсутствие пакета.
|
||||
self._bindings_signature = signature # Последующая обработка кадра использует уже настроенный контекст.
|
||||
|
||||
def bind_default_angle(self): # Миграция старого профиля с пустым азимутом не требует знания адресов в GUI.
|
||||
self._sync_bindings() # Источник и режим берутся из уже настроенных остальных полей.
|
||||
selector, factor = (C.c_uint8 * 4)(), C.c_double() # C возвращает готовую библиотечную привязку.
|
||||
if not self._api.default_angle(self._context, selector, C.byref(factor)): # Ни SAM/DAM, ни индекс не вычисляются в Python.
|
||||
raise ValueError("Не удалось восстановить привязку азимута DEIF") # Ошибка миграции остаётся видимой.
|
||||
self.bindings["angle"] = Binding(bytes(selector).hex(" ").upper(), factor.value) # Обновляется только отсутствующая настройка азимута.
|
||||
self._sync_bindings() # Новая привязка применяется сразу в текущем сеансе.
|
||||
|
||||
def reset(self): # Сохраняет API очистки показаний при смене пользовательского канала.
|
||||
self._sync_bindings() # Настройки должны соответствовать очищаемому контексту.
|
||||
self._api.reset_values(self._context) # Очистка выполняется общим ядром.
|
||||
|
||||
def accept(self, source, identifier, data, extended=False, rtr=False, now=None): # Порт связывает транспортное имя с независимой моделью прибора.
|
||||
if source != self.source or not 0 <= identifier <= 0x1FFFFFFF or len(data) > 8: # Имя канала относится к приложению, границы — к безопасному ABI.
|
||||
return False # Кадры другого транспорта не обновляют C-контекст.
|
||||
self._sync_bindings() # Диалог мог изменить привязки с момента предыдущего кадра.
|
||||
accepted = bool(self._api.accept(self._context, identifier, bytes(data), len(data), int(bool(extended)) | (int(bool(rtr)) << 1), _milliseconds(now))) # Единственное место протокольной обработки находится в C.
|
||||
return accepted and (self.services or not self._api.inspect_frame(identifier, data, extended, rtr)[1][7]) # Пассивный API сохраняет прежний признак отказа только для текущего ошибочного I16, не для других полей.
|
||||
|
||||
def _snapshots(self, now=None): # Снимок переводит готовые C-поля в контейнеры Python без повторения алгоритма.
|
||||
self._sync_bindings() # Требуется также после прямого редактирования масштаба в настройках.
|
||||
result, moment = {}, _milliseconds(now) # Все поля одного снимка читаются в один момент времени.
|
||||
for index, key in enumerate(FIELDS): # Порядок ABI не зависит от порядка ключей пользовательского словаря.
|
||||
value, metadata = C.c_double(), (C.c_int64 * 4)() # C выдаёт масштабированное значение и флаги seen/valid/stale.
|
||||
self._api.read(self._context, index, moment, self.expiry_ms, C.byref(value), metadata) # Свежесть и служебные статусы вычисляются ядром.
|
||||
result[key] = (value.value, metadata[0] / 1000, metadata[2], metadata[3]) # Деление переводит только единицы системных часов порта.
|
||||
return result # Снимок не раскрывает внутренний layout C.
|
||||
|
||||
@property # Совместимый read-only снимок прежнего словаря показаний.
|
||||
def values(self): # Значения остаются в C, Python не хранит вторую копию состояния.
|
||||
return {key: item[0] for key, item in self._snapshots().items() if item[3] & 2} # Готовый valid-флаг исключает служебные I16.
|
||||
|
||||
@property # Временные отметки нужны существующей отрисовке и диагностике.
|
||||
def received(self): # Даже invalid является принятым обновлением объекта, а не отсутствием связи.
|
||||
return {key: item[1] for key, item in self._snapshots().items() if item[3] & 1} # Решение о наличии сообщения уже принято C-ядром.
|
||||
|
||||
def value(self, key, now=None): # Сохраняет интерфейс получения одного свежего показания.
|
||||
item = self._snapshots(now).get(key) # Масштаб и свежесть получены из общего C-снимка.
|
||||
return item[0] if item and item[3] & 2 and not item[3] & 4 else None # Python лишь отображает valid/stale флаги ABI.
|
||||
|
||||
def angle_selection(self, now=None): # Одно правило азимута используется и пассивным монитором, и активным SLAVE.
|
||||
self._sync_bindings() # Профиль мог быть загружен из старого пользовательского файла.
|
||||
value = C.c_double() # Владение выходным скаляром остаётся у порта.
|
||||
kind = self._api.angle(self._context, _milliseconds(now), self.expiry_ms, C.byref(value)) # C выбирает фактический угол либо принятое сетевое задание.
|
||||
return (value.value if kind else None, kind == 2) # Второй элемент нужен только для подписи «задание» в GUI.
|
||||
|
||||
@property # Список источников нужен только заголовку прибора, не алгоритму приёма.
|
||||
def sam_sources(self): # UI получает готовые адреса без масок и смещений селектора.
|
||||
from .deif_controls import selector_info # Разбор адреса селектора делегируется общему C-порту.
|
||||
descriptors = [selector_info(binding.selector) for binding in self.bindings.values() if binding.selector] # Сохраняет индивидуальные привязки каждого поля.
|
||||
return sorted({item.node for item in descriptors if item is not None and not item.dam}) # DAM-получатель не выдаётся за источник SAM.
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Регистровый контракт аппаратного эмулятора DEIF из set_emu_deif.h.""" # Один контракт используется всеми MCU и настольными клиентами.
|
||||
from dataclasses import dataclass # Неизменяемый снимок отделяет подтверждённое состояние платы от редактируемой формы.
|
||||
|
||||
DEIF_BASE = 0x1600 # Начало окна управления DEIF в set_emu_server.c, адрес измеряется в 16-битных словах.
|
||||
DEIF_WORDS = 52 # SET_DEIF_WORDS версии 1 включает профиль, RAW, привязки OD и диагностику.
|
||||
DEIF_FIELDS = ("Угол, RAW", "Обороты, RAW", "Обороты %, RAW", "Мощность, RAW", "Мощность %, RAW", "Задание угла, RAW") # Порядок соответствует raw[6] переносимой C-модели; физические единицы зависят от словаря прибора.
|
||||
DEIF_INDICES = (0x3001, 0x3081, 0x3091, 0x3421, 0x3431, 0x3001) # Начальные OD экземпляра 1 взяты из set_deif_init; последний объект имеет подиндекс 7.
|
||||
|
||||
|
||||
@dataclass(frozen=True) # Клиент публикует только целиком проверенный снимок без последующих изменений.
|
||||
class DeifSnapshot: # Тип не зависит от Qt, HAL, конкретного MCU и способа доставки регистров.
|
||||
words: tuple # Содержит 52 беззнаковых слова, прочитанных одной командой FC03.
|
||||
mode: int # Режим из конфигурации STM: 4 Master, 5 Slave либо иной остановленный режим.
|
||||
|
||||
@property # Роль берётся из ответа платы, а не из ранее выбранного пункта GUI.
|
||||
def role(self): # Возвращает 1 Master либо 2 Slave согласно set_emu_deif.h.
|
||||
return self.words[3] # Слово 3 не совпадает с номерами режимов 4 и 5 блока STM.
|
||||
|
||||
@property # Проверка запущенного состояния нужна для запрета изменения сетевого профиля.
|
||||
def running(self): # Возвращает логическое состояние модели без запуска либо остановки.
|
||||
return bool(self.words[2]) # Слово 2 равно 1 только после подтверждённой команды пуска.
|
||||
|
||||
@property # Знаковое представление сохраняет отрицательные RAW и служебные коды DEIF.
|
||||
def raw(self): # Возвращает шесть значений I16 в порядке DEIF_FIELDS.
|
||||
return tuple(value if value < 0x8000 else value - 0x10000 for value in self.words[16:22]) # Старший бит задаёт знак 16-битного дополнения до двух.
|
||||
|
||||
|
||||
def decode_snapshot(words, mode): # Проверяет совместимость прошивки до разрешения любых записей DEIF.
|
||||
values = tuple(words) # Не сохраняет ссылку на изменяемый транспортный буфер.
|
||||
if len(values) != DEIF_WORDS or values[:2] != (0x4446, 1): # Сигнатура DF и версия 1 заданы set_deif_read, не совпадают с конфигурацией STM.
|
||||
raise ValueError("Нужна прошивка с сервисом DEIF Master/Slave версии 1") # Старая прошивка не получает команд в неизвестное окно.
|
||||
if values[2] not in (0, 1) or values[3] not in (1, 2): # Повреждённые running и role не должны превращаться в разрешение записи.
|
||||
raise ValueError("STM вернула неверное состояние DEIF") # Отказ сохраняет форму неподтверждённой.
|
||||
if mode in (4, 5) and values[3] != mode - 3: # Конфигурация и окно состояния должны описывать одну роль текущего экземпляра.
|
||||
raise ValueError("Роль DEIF не совпадает с режимом STM; повторите чтение") # Между чтениями другая сессия могла изменить режим платы.
|
||||
return DeifSnapshot(values, mode) # Единственная точка создания проверенного снимка для UI и тестов.
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Тонкий ctypes-порт единственного C99-ядра DEIF из templates/c/set-protocol.""" # В Python нет второго кодека или автомата NMT.
|
||||
import ctypes as C # Фиксированные типы повторяют только публичный ABI.
|
||||
import os # Путь дополнительной библиотеки передаёт приложение, не общая библиотека.
|
||||
from pathlib import Path # Дополнительный нативный компонент может лежать рядом с основной библиотекой любого приложения.
|
||||
from protocan.native import _load_library # Стандартный механизм загрузки используется при отсутствии явного пути DEIF.
|
||||
|
||||
|
||||
class Frame(C.Structure): # Структура содержит готовые STD-кадры, а не wire-структуру транспорта.
|
||||
_fields_ = [("can_id", C.c_uint32), ("dlc", C.c_uint32), ("data", C.c_uint8 * 8)] # Layout размером 16 байт совпадает с deif_xdi.h.
|
||||
|
||||
|
||||
class NativeDeif: # Экземпляр владеет только сигнатурами C, состояние выделяется отдельными моделями.
|
||||
def __init__(self): # Отсутствие общего ядра является ошибкой сборки, алгоритмического fallback нет.
|
||||
self.lib = C.CDLL(os.environ["SETPROTOCOL_DEIF_LIBRARY"]) if os.environ.get("SETPROTOCOL_DEIF_LIBRARY") else _load_library() # Выбор пути относится к загрузке, не к протоколу.
|
||||
if not hasattr(self.lib, "setp_deif_version"): # Во время обновления основная DLL может оставаться занятой работающим приложением.
|
||||
primary = Path(self.lib._name) # Путь сообщает стандартный загрузчик, структура gui_desktop здесь неизвестна.
|
||||
self.lib = C.CDLL(str(primary.with_name(primary.stem + "_deif" + primary.suffix))) # Это дополнительная сборка того же C-ядра, а не алгоритмический Python fallback.
|
||||
signatures = { # Сигнатуры скалярного ABI не зависят от внутренних C-структур.
|
||||
"version": ([], C.c_int), "state_size": ([], C.c_size_t), # Проверка версии и caller-owned памяти.
|
||||
"catalog": ([C.c_uint, C.POINTER(C.c_int32)], C.c_int), # Каталог числовых индексов и разрешений.
|
||||
"selector": ([C.c_uint, C.c_uint, C.c_uint, C.c_uint, C.c_void_p], C.c_int), # C формирует готовый селектор.
|
||||
"inspect": ([C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.POINTER(C.c_int32)], C.c_int), # Единственный разбор входящего wire-кадра.
|
||||
"make": ([C.c_uint, C.c_uint, C.c_uint, C.c_int32, C.c_uint32, C.c_uint, C.POINTER(Frame)], C.c_int), # Единственная сборка MPDO.
|
||||
"master": ([C.c_uint, C.POINTER(C.c_int32), C.c_uint32, C.c_uint, C.c_uint, C.POINTER(Frame)], C.c_int), # Единственный порядок пяти объектов цикла.
|
||||
"service": ([C.c_uint, C.c_uint, C.c_uint, C.POINTER(Frame)], C.c_int), "angle_command": ([C.POINTER(Frame), C.POINTER(Frame)], C.c_int), # Служебные кадры и задание азимута собираются общим C-кодом.
|
||||
"selector_info": ([C.c_void_p, C.POINTER(C.c_int32)], C.c_int), "route": ([C.c_uint, C.c_uint, C.c_uint32, C.c_uint, C.POINTER(C.c_uint32)], C.c_int), # Редакторы не разбирают адресный байт или COB-ID самостоятельно.
|
||||
"readdress": ([C.c_void_p, C.c_uint, C.c_uint, C.c_void_p], C.c_int), # Перенос селектора сохраняет ручной объект без масок и endian в UI.
|
||||
"validate": ([C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.c_uint], C.c_int), # Изолированные выходы MASTER и SLAVE.
|
||||
"init": ([C.c_void_p, C.c_size_t] + [C.c_uint] * 7 + [C.c_uint32], C.c_int), # Семь параметров профиля и отдельный CAN ID наблюдателя.
|
||||
"bind": ([C.c_void_p, C.c_uint, C.c_void_p, C.c_double], C.c_int), # Селектор и библиотечный масштаб передаются без интерпретации.
|
||||
"get_binding": ([C.c_void_p, C.c_uint, C.c_void_p, C.POINTER(C.c_double)], C.c_int), # Исходные значения профиля приходят из единственного C-каталога.
|
||||
"default_angle": ([C.c_void_p, C.c_void_p, C.POINTER(C.c_double)], C.c_int), # Общая миграция отсутствующей привязки.
|
||||
"start": ([C.c_void_p, C.c_uint64, C.c_uint], C.c_int), "stop": ([C.c_void_p], None), "reset_values": ([C.c_void_p], None), # Жизненный цикл без системных часов C.
|
||||
"accept": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.c_uint64], C.c_int), # C проверяет NMT, адресацию, объекты и длину.
|
||||
"poll": ([C.c_void_p, C.c_uint64, C.POINTER(Frame), C.c_size_t], C.c_size_t), # Порт владеет выходным буфером событий.
|
||||
"read": ([C.c_void_p, C.c_uint, C.c_uint64, C.c_uint, C.POINTER(C.c_double), C.POINTER(C.c_int64)], C.c_int), # Готовое значение и временные метаданные индикатора.
|
||||
"lost": ([C.c_void_p, C.c_uint64], C.c_uint), # Маска устаревших входов вычисляется в C.
|
||||
"angle": ([C.c_void_p, C.c_uint64, C.c_uint, C.POINTER(C.c_double)], C.c_int), "meta": ([C.c_void_p, C.POINTER(C.c_int64)], C.c_int), # Единый выбор азимута и снимок состояния.
|
||||
} # Эти объявления описывают ABI, а не копию алгоритма.
|
||||
for name, (arguments, result) in signatures.items(): # Все отсутствующие символы выявляются при загрузке расширения.
|
||||
function = getattr(self.lib, "setp_deif_" + name) # Названия совпадают с публичным заголовком templates.
|
||||
function.argtypes, function.restype = arguments, result # ctypes не должен угадывать размеры указателей и целых.
|
||||
setattr(self, name, function) # Короткие имена оставляют порты моделей читаемыми.
|
||||
if self.version() != 1: # Несовместимый ABI не используется с предположением о layout.
|
||||
raise RuntimeError("Нужна общая библиотека DEIF ABI 1") # Сообщение указывает на необходимость пересборки нативного ядра.
|
||||
|
||||
def selector_bytes(self, field, node, instance=1, dam=False): # Возвращает готовый C-селектор для настроек UI.
|
||||
output = (C.c_uint8 * 4)() # Размер мультиплексора задан публичным контрактом.
|
||||
if not self.selector(field, node, instance, bool(dam), output): # Проверка диапазонов находится в C.
|
||||
raise ValueError("DEIF: неверный Node-ID, объект или экземпляр") # Недопустимая привязка не сохраняется в настройках.
|
||||
return bytes(output) # Порт преобразует только тип контейнера.
|
||||
|
||||
def inspect_frame(self, identifier, data, extended=False, rtr=False): # Передаёт целый CAN-кадр общему декодеру.
|
||||
if not 0 <= identifier <= 0x1FFFFFFF or len(data) > 8: # Проверяет представимость на границе ABI до сужения Python-чисел.
|
||||
return -1, [0] * 12 # Переполнение нельзя превратить в другой допустимый кадр.
|
||||
output = (C.c_int32 * 12)() # Индексы метаданных определены в контракте C-декодера.
|
||||
code = self.inspect(identifier, bytes(data), len(data), int(bool(extended)) | (int(bool(rtr)) << 1), output) # Флаги относятся к типу CAN-кадра, не к содержимому протокола.
|
||||
return code, list(output) # Локализованный текст формируется поверх готовых полей C.
|
||||
|
||||
def make_frame(self, field, node, value, identifier, mode="sam", instance=1): # Возвращает кадр существующего транспортного API.
|
||||
from .can_transport import build_frame # Пакет транспорта не импортируется до фактической сборки кадра.
|
||||
if mode not in ("sam", "dam") or type(instance) is not int or not 0 <= instance <= 4294967295 or type(value) is not int or not -2147483648 <= value <= 2147483647 or type(node) is not int or not 0 <= node <= 127 or type(identifier) is not int or not 0 <= identifier <= 0x1FFFFFFF: # Порт проверяет только типы и представимость ABI.
|
||||
raise ValueError("DEIF: неверный Node-ID, режим, CAN ID или значение") # Значения не усекаются до вызова C.
|
||||
output = Frame() # Память выходного CAN-кадра принадлежит Python.
|
||||
if not self.make(field, node, instance, value, identifier, mode == "dam", C.byref(output)): # Wire-байты и границы протокола определяются исключительно C.
|
||||
raise ValueError("DEIF: Node-ID, RPDO, экземпляр или значение вне диапазона") # Причина отклонения остаётся видимой в интерфейсе.
|
||||
return build_frame(output.can_id, bytes(output.data[:output.dlc]), ide=False) # Транспорт AA55/адаптер получает уже готовые данные STD.
|
||||
|
||||
|
||||
_native = None # Общая библиотека загружается один раз на процесс.
|
||||
def get_deif_native(): # Ленивое получение не требует Qt и пригодно для CLI.
|
||||
global _native # Экземпляр хранит сигнатуры, состояния приборов остаются раздельными.
|
||||
if _native is None: # Первая реальная операция DEIF проверяет наличие нового нативного ядра.
|
||||
_native = NativeDeif() # Отсутствие DLL или ABI не скрывается Python-реализацией протокола.
|
||||
return _native # Все модели используют один и тот же проверенный C-код.
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Серверный Python-порт DEIF: протокольное состояние целиком принадлежит общему C99.""" # Пакет не импортирует gui_desktop и пригоден без Qt.
|
||||
import ctypes as C # Структуры ABI содержат только готовые кадры и скаляры.
|
||||
from time import monotonic # Порт поставляет монотонные часы, чтобы C оставался независим от ОС.
|
||||
from .deif_display import DeifDisplay, FIELDS, STATUS, _milliseconds # Пассивный и активный режимы используют одинаковые снимки индикаторов.
|
||||
from .deif_native import Frame, get_deif_native # Нет Python-копии адресации, NMT, сроков или SDO.
|
||||
|
||||
|
||||
def validate_slave_transmit(frame, node): # Серверный маршрут имеет отдельный контракт по собственному Node-ID.
|
||||
if type(node) is not int or not 1 <= node <= 127 or not get_deif_native().validate(frame.can_id, bytes(frame.data), len(frame.data), int(bool(frame.ide)) | (int(bool(frame.rtr)) << 1), node): # C разрешает только собственные heartbeat, EMCY и ответы SDO.
|
||||
raise ValueError("Разрешены только heartbeat, EMCY и ответы SDO своего SLAVE") # Недопустимый кадр не попадёт в физический транспорт.
|
||||
|
||||
|
||||
class DeifSlave(DeifDisplay): # Отличие от монитора состоит в включённых сервисах C и сохранении последнего числа при Data lost.
|
||||
services, expiry_ms = True, 0 # Частоту heartbeat и срок Data lost определяет C, нулевой expiry сохраняет последнее показание.
|
||||
|
||||
def __init__(self, node=11, source_node=1, xdi_net=True, self_start=True, dam_pdo=2, bindings=None, dimmer_group=1): # Сохраняет существующий Python API модели SLAVE.
|
||||
if any(type(value) is not int for value in (node, source_node, dam_pdo, dimmer_group)): # Порт проверяет типы до преобразования unsigned ABI.
|
||||
raise ValueError("Параметры профиля DEIF должны быть целыми") # Логические и дробные адреса не превращаются в другой узел.
|
||||
super().__init__(source="Локальная симуляция", device_node=node, bindings=bindings, source_node=source_node, xdi_net=xdi_net, self_start=self_start, dam_pdo=dam_pdo, dimmer_group=dimmer_group) # Все ограничения и исходные привязки создаются C-ядром.
|
||||
|
||||
def start(self, now=None, reset_values=True): # Явное включение не использует физический транспорт напрямую.
|
||||
self._sync_bindings() # Последние параметры UI должны соответствовать создаваемому сеансу.
|
||||
self._api.start(self._context, _milliseconds(monotonic() if now is None else now), bool(reset_values)) # Boot-up и self-start формируются общим автоматом C.
|
||||
|
||||
def stop(self): # Выключение прекращает в том числе невыданные служебные события.
|
||||
self._api.stop(self._context) # Никакого отдельного Python-таймера heartbeat здесь нет.
|
||||
|
||||
def accept(self, source, identifier, data, extended=False, rtr=False, now=None): # Транспортное имя фильтруется базовым портом, байты принимает C.
|
||||
return super().accept(source, identifier, data, extended, rtr, monotonic() if now is None else now) # Явные часы позволяют одинаково проверять границы Python и JNI.
|
||||
|
||||
def tick(self, now=None): # Порт регулярно выгружает готовые события и передаёт их выбранному транспорту.
|
||||
from .can_transport import build_frame # Формат транспорта не смешивается с форматом DEIF.
|
||||
self._sync_bindings() # Прямое изменение профиля не должно оставлять старые привязки.
|
||||
output = (Frame * 16)() # Вместимость соответствует ограниченному пакету событий публичного ABI.
|
||||
count = self._api.poll(self._context, _milliseconds(monotonic() if now is None else now), output, len(output)) # Все сроки и NMT-переходы обслуживаются C.
|
||||
return [build_frame(frame.can_id, bytes(frame.data[:frame.dlc]), ide=False) for frame in output[:count]] # Python меняет только тип контейнера готовых кадров.
|
||||
|
||||
def lost_fields(self, now=None): # Локализованный список строится поверх готовой маски C.
|
||||
self._sync_bindings() # Непривязанное поле не должно участвовать в предупреждении.
|
||||
mask = self._api.lost(self._context, _milliseconds(monotonic() if now is None else now)) # Независимые сроки по 3000 мс определяются общим ядром.
|
||||
return [key for index, key in enumerate(FIELDS) if mask & (1 << index)] # Соответствие field ID и имени не повторяет алгоритм свежести.
|
||||
|
||||
def _meta(self): # Набор скаляров не раскрывает внутреннюю структуру C.
|
||||
output = (C.c_int64 * 10)() # Число элементов закреплено в deif_xdi.h.
|
||||
self._api.meta(self._context, output) # Состояние хранится только в одном месте.
|
||||
return list(output) # Python-порт не пересчитывает NMT или периоды.
|
||||
|
||||
active = property(lambda self: bool(self._meta()[0])) # Флаг нужен жизненному циклу страницы.
|
||||
state = property(lambda self: self._meta()[1]) # Готовый CANopen NMT-код используется в локализованной подписи.
|
||||
started = property(lambda self: self._meta()[2] / 1000) # Секунды сохраняют совместимость прежнего Python API.
|
||||
last_heartbeat = property(lambda self: self._meta()[3] / 1000) # Преобразуется только единица системного времени.
|
||||
heartbeat_ms = property(lambda self: self._meta()[4]) # Значение стандартного объекта 0x1017 выдаёт C.
|
||||
accepted = property(lambda self: self._meta()[5]) # Счётчик принятия не имитируется количеством тиков GUI.
|
||||
brightness = property(lambda self: self._meta()[7]) # Текущий процент подсветки из C-профиля.
|
||||
palette = property(lambda self: self._meta()[8]) # Day/Night/Dusk хранится как код общего ядра.
|
||||
invalid = property(lambda self: {key: STATUS[item[2]] for key, item in self._snapshots().items() if item[2]}) # Только текст статуса локализуется в Python.
|
||||
+88
-150
File diff suppressed because one or more lines are too long
@@ -0,0 +1,140 @@
|
||||
"""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))
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
|
||||
"""Асинхронное управление общим 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 получает результат после полного освобождения ресурсов.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,181 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,36 @@
|
||||
from .qt_compat import Signal # Общий сигнал Qt работает в портах PySide6 и PySide2 без зависимости от SETGUI.
|
||||
try: # Настольные потребители могут использовать любую из поддерживаемых версий Qt.
|
||||
from PySide6.QtWidgets import QComboBox, QTabWidget # Qt6 предоставляет контейнер общих панелей и явный выбор MCU.
|
||||
from PySide6.QtCore import Qt, QSignalBlocker # Блокировка сигнала предотвращает рекурсивное переключение профиля.
|
||||
except ImportError: # Порт Windows 7 использует тот же API поверх PySide2.
|
||||
from PySide2.QtWidgets import QComboBox, QTabWidget # Состав и поведение панелей сохраняются в Qt5.
|
||||
from PySide2.QtCore import Qt, QSignalBlocker # Оба поколения Qt поддерживают блокировку сигналов комбобокса.
|
||||
from set_devices.mcu_profiles import EMULATOR_PROFILES, EMULATOR_TOOLS # Модели и аппаратные привязки задаёт общая библиотека.
|
||||
|
||||
|
||||
class EmulatorWorkspace(QTabWidget): # Потребитель предоставляет UI-панели, а templates выбирает их состав для конкретного MCU.
|
||||
mcu_changed = Signal(str) # Приложение обновляет локальные настройки и подписи после явного выбора аппаратного профиля.
|
||||
|
||||
def __init__(self, parent=None): # Контейнер не открывает COM/CAN и не создаёт экземпляров аппаратных моделей.
|
||||
super().__init__(parent) # Qt владеет переданными страницами в течение жизни рабочей области.
|
||||
self.mcu = "STM32F407" # Сохраняем профиль прежнего эмулятора для существующих пользователей и вызовов.
|
||||
self.mcu_selector = QComboBox(self) # Общая панель явно показывает MCU, которому предназначены команды.
|
||||
for key, profile in EMULATOR_PROFILES.items(): # В списке появляются только платы с реализованными портами set_emu_server.
|
||||
self.mcu_selector.addItem(profile["title"], key) # Стабильный ключ отделяет совместимость устройства от отображаемой подписи.
|
||||
self.setCornerWidget(self.mcu_selector, Qt.Corner.TopRightCorner) # Выбор MCU доступен рядом с вкладками общих инструментов.
|
||||
self.mcu_selector.currentIndexChanged.connect(lambda index: self.select_mcu(self.mcu_selector.itemData(index))) # Пользовательский выбор проходит тот же метод, что переход из меню конкретного MCU.
|
||||
self._emulator_pages = {} # Реестр хранит единственные экземпляры переданных панелей, сохраняя общие транспорты приложения.
|
||||
|
||||
def add_emulator_page(self, key, widget): # Приложение связывает собственный виджет с публичным ключом общего сервиса.
|
||||
self._emulator_pages[key] = widget # Повторное использование панели не копирует её обработчики или состояние модели.
|
||||
self.addTab(widget, EMULATOR_TOOLS[key]) # Название инструмента берётся из общего каталога templates.
|
||||
|
||||
def select_mcu(self, key): # Смена профиля меняет только UI и настройки; команды активации остаются явными действиями пользователя.
|
||||
profile = EMULATOR_PROFILES[key] # Неизвестный MCU отвергается до изменения текущей рабочей области.
|
||||
self.mcu = key # Текущий ключ используется потребителем для выбора настроек и аппаратных параметров.
|
||||
with QSignalBlocker(self.mcu_selector): # Программный переход из раздела MCU не вызывает повторный вход через комбобокс.
|
||||
self.mcu_selector.setCurrentIndex(self.mcu_selector.findData(key)) # Видимый выбор всегда совпадает с применённым профилем.
|
||||
for name, widget in self._emulator_pages.items(): # Доступность каждого инструмента определяется сервисами выбранного порта.
|
||||
self.setTabVisible(self.indexOf(widget), name in profile["tools"]) # Новая плата может подключать подмножество тех же панелей без изменения их реализации.
|
||||
self.mcu_changed.emit(key) # Локальный адаптер обновляет настройки после завершения общего переключения.
|
||||
return True # Вызывающая навигация может отличить выполненный переход от отказа занятого адаптера.
|
||||
@@ -7,6 +7,7 @@ import struct
|
||||
|
||||
from .qt_compat import QObject, QTimer, Signal
|
||||
from .qt_compat import QSerialPort
|
||||
from set_devices.mcu_profiles import EMULATOR_MODES # Допустимые номера режимов берутся из общего каталога, включая обе роли DEIF.
|
||||
from set_devices.tms_terminal import crc16_modbus
|
||||
|
||||
BAUDRATES = (9600, 19200, 38400, 57600, 115200)
|
||||
@@ -24,7 +25,7 @@ def decode_config(raw):
|
||||
if words[0] != 0x5343 or words[1] not in (2, 3):
|
||||
raise ValueError("Прошивка STM не поддерживает настройку связи (нужна новая версия)")
|
||||
address, tms, rate = words[2:5]
|
||||
if not 1 <= address <= 247 or not 1 <= tms <= 255 or address == tms or rate >= len(BAUDRATES) or words[8] > (3 if words[1] == 3 else 1) or words[9] > (3 if words[1] == 3 else 1):
|
||||
if not 1 <= address <= 247 or not 1 <= tms <= 255 or address == tms or rate >= len(BAUDRATES) or words[8] not in (EMULATOR_MODES if words[1] == 3 else (0, 1)) or words[9] not in (EMULATOR_MODES if words[1] == 3 else (0, 1)): # Версия 3 C-сервера поддерживает режимы 0…5; версия 2 сохраняет только УМП и 2812.
|
||||
raise ValueError("STM вернула неверные параметры связи")
|
||||
return words
|
||||
|
||||
@@ -52,7 +53,7 @@ class StmSettingsClient(QObject):
|
||||
return
|
||||
if desired is not None:
|
||||
addr, tms, rate, mode = desired
|
||||
if not 1 <= addr <= 247 or not 1 <= tms <= 255 or addr == tms or rate not in BAUDRATES or mode not in (0, 1, 2, 3):
|
||||
if not 1 <= addr <= 247 or not 1 <= tms <= 255 or addr == tms or rate not in BAUDRATES or mode not in EMULATOR_MODES: # Номера 4 и 5 выбирают DEIF Master/Slave того же общего C-сервера.
|
||||
self.finished.emit(False, "Адреса УМП и 2812 должны различаться; проверьте диапазоны и скорость", None)
|
||||
return
|
||||
self.unit, self.baud, self.timeout_ms = unit, baud, timeout
|
||||
@@ -120,7 +121,7 @@ class StmSettingsClient(QObject):
|
||||
return
|
||||
addr, tms, baud, mode = self.desired
|
||||
if mode >= 2 and words[1] < 3:
|
||||
raise ValueError("Обновите прошивку STM: для отдельных режимов IGBT и ЦАП нужна версия настроек 3")
|
||||
raise ValueError("Обновите прошивку STM: для IGBT, ЦАП и DEIF нужна версия настроек 3") # Сообщение учитывает обе роли DEIF и возможное применение настроек при потере ответа.
|
||||
self.staged = (addr, tms, BAUDRATES.index(baud), mode)
|
||||
self.steps = [(6, 0x1218, 0), (6, 0x1215, addr), (6, 0x1216, tms),
|
||||
(6, 0x1217, self.staged[2]), (6, 0x1219, mode), (3, 0x1210, 10)]
|
||||
@@ -173,5 +174,5 @@ class StmSettingsClient(QObject):
|
||||
if self.serial.isOpen():
|
||||
self.serial.close()
|
||||
if not ok and self.committed:
|
||||
message += " Возможно, STM уже применила новые значения: адрес УМП %d, адрес 2812 %d, %d бод, режим %d (0=УМП, 1=2812, 2=IGBT, 3=ЦАП). Повторите чтение по ним или перезагрузите плату." % self.desired
|
||||
message += " Возможно, STM уже применила новые значения: адрес УМП %d, адрес 2812 %d, %d бод, режим %d (0=УМП, 1=2812, 2=IGBT, 3=ЦАП, 4=DEIF Master, 5=DEIF Slave). Повторите чтение по ним или перезагрузите плату." % self.desired # Сообщение учитывает обе роли DEIF и возможное применение настроек при потере ответа.
|
||||
self.finished.emit(ok, message, values)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Общие DEIF-векторы проверяют ctypes теми же данными, что C и Android/JVM.""" # Тесты не требуют Qt или структуры Desktop-приложения.
|
||||
from pathlib import Path # Fixture входит в общий репозиторий templates.
|
||||
import pytest # Параметризация даёт независимый результат для каждой эталонной строки.
|
||||
from set_devices.deif_native import get_deif_native # Python не использует альтернативный декодер при отсутствии C.
|
||||
from set_devices.deif_display import DeifDisplay # Пассивная модель является общим API библиотеки.
|
||||
FIXTURE = Path(__file__).resolve().parents[2] / 'c/set-protocol/tests/fixtures/deif-xdi-v1.tsv' # Точный общий источник тестов для трёх портов.
|
||||
CASES = [line.split() for line in FIXTURE.read_text(encoding='utf-8').splitlines() if line and not line.startswith('#')] # Здесь разбирается текст fixture, а не протокол CAN.
|
||||
@pytest.mark.parametrize('row', CASES) # Каждый ошибочный или валидный wire-кадр проверяется отдельно.
|
||||
def test_ctypes_matches_c_and_jni_vectors(row): # Числовой результат должен совпасть с независимым описанием ожидаемого кадра.
|
||||
identifier, payload, flags, result, raw, index, sub = row # Столбцы fixture не зависят от внутреннего layout C.
|
||||
flags = int(flags) # EXT/RTR являются входными метаданными теста.
|
||||
code, meta = get_deif_native().inspect_frame(int(identifier, 16), bytes.fromhex(payload), bool(flags & 1), bool(flags & 2)) # Wire-разбор выполняется единственным C-кодеком.
|
||||
assert (code, meta[4], meta[2], meta[3]) == (int(result), int(raw), int(index, 16), int(sub, 16)) # RAW и адрес объекта совпадают с C- и Kotlin-проверкой.
|
||||
def test_passive_angle_is_received_without_gui_or_simulation(): # Ошибка со снимка воспроизводится без запуска серверных сервисов.
|
||||
model = DeifDisplay(source='CAN / мост', device_node=13) # Физический адрес SLAVE не становится источником SAM.
|
||||
assert model.accept('CAN / мост', 0x201, bytes.fromhex('01 01 30 07 84 03 00 00'), now=10) # Точный кадр пользователя несёт RAW 900.
|
||||
assert model.angle_selection(now=10) == (90.0, True) # Сетевое задание видно как 90°, с явной пометкой.
|
||||
assert model.value('angle', now=10) is None # Принятое задание не превращается в фиктивный фактический объект 0x02.
|
||||
assert model.angle_selection(now=15.001) == (None, False) # При полном молчании наблюдатель уважает прежний срок свежести пять секунд.
|
||||
Reference in New Issue
Block a user