176 lines
26 KiB
Python
176 lines
26 KiB
Python
"""Общие поля конструктора CAN и SETCAN для адаптеров и обоих каналов DevBoard."""
|
||
from PySide6.QtCore import Signal # Сигнал изменений позволяет владельцу сбрасывать готовый кадр без зависимости от транспорта.
|
||
from PySide6.QtGui import QFont # Моноширинный шрифт сохраняет читаемость HEX-идентификатора и данных.
|
||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QComboBox, QSpinBox, QLineEdit, QLabel # Один набор виджетов определяет форму всех потребителей.
|
||
from protocan import protocan as pc # Сборка полей протокола выполняется существующими обёртками общего C-ядра.
|
||
from set_devices import can_transport as tr # Проверка CAN ID и разбор HEX используют действующий общий транспорт.
|
||
|
||
|
||
DESCRIPTION = "Обычный CAN: задайте ID, DLC и Data. Для сборки ID по полям протокола выберите SETCAN." # Одинаковое пояснение режимов используется во всех панелях.
|
||
|
||
|
||
class CanFrameFields(QWidget): # Компонент содержит только ввод кадра; подключение и отправка остаются у владельца.
|
||
changed = Signal() # Любое изменение ввода отменяет ранее собранный кадр в соответствующем канале.
|
||
|
||
def __init__(self, settings_factory, parent=None): # Редактор SETTINGS передаётся владельцем как визуальный компонент без копирования его реализации.
|
||
super().__init__(parent) # Родитель Qt управляет временем жизни всех полей формы.
|
||
layout = QVBoxLayout(self) # Единый вертикальный порядок исключает различия между вкладками.
|
||
layout.setContentsMargins(0, 0, 0, 0) # Внешние отступы задаёт панель приложения.
|
||
self.mode = QComboBox() # Переключатель сохраняет оба набора введённых значений.
|
||
self.mode.addItem("Обычный CAN — ID / DLC / Data", "can") # Исходный режим одинаков для всех трёх конструкторов.
|
||
self.mode.addItem("SETCAN — поля протокола", "setcan") # Протокольный режим всегда создаёт расширенный идентификатор.
|
||
mode_form = QFormLayout() # Первая строка остаётся видимой в обоих режимах.
|
||
mode_form.addRow("Режим", self.mode) # Подпись поясняет назначение переключателя.
|
||
layout.addLayout(mode_form) # Выбор режима предшествует зависимым полям.
|
||
self.can_panel = QWidget() # Контейнер скрывает вместе подписи и редакторы обычного CAN.
|
||
can_form = QFormLayout(self.can_panel) # Формат, идентификатор и длина выровнены общей формой.
|
||
can_form.setContentsMargins(0, 0, 0, 0) # Поля обоих режимов начинаются на одной вертикали.
|
||
self.format = QComboBox() # IDE задаётся явно независимо от численного значения идентификатора.
|
||
self.format.addItem("Стандартный — 11 бит (STD)", False) # STD соответствует диапазону CAN ID 0..0x7FF.
|
||
self.format.addItem("Расширенный — 29 бит (EXT)", True) # EXT соответствует диапазону CAN ID 0..0x1FFFFFFF.
|
||
self.can_id = QLineEdit("123") # Исходный пример идентификатора записан в HEX.
|
||
self.can_id.setToolTip("HEX, с префиксом 0x или без него. STD: 0..7FF; EXT: 0..1FFFFFFF.") # Указываем пределы CAN ID без неявного усечения.
|
||
self.dlc = QSpinBox() # Пользователь явно задаёт длину обычного CAN-кадра.
|
||
self.dlc.setRange(0, 8) # Классический CAN допускает от нуля до восьми байтов Data.
|
||
self.dlc.setToolTip("Количество байтов Data должно точно совпадать с DLC; дополнение и обрезка не выполняются.") # Описываем строгую проверку длины.
|
||
can_form.addRow("Формат ID (IDE)", self.format) # Формат отражает фактический флаг IDE.
|
||
can_form.addRow("CAN ID (HEX)", self.can_id) # Явно обозначаем систему счисления идентификатора.
|
||
can_form.addRow("DLC (0..8 байт)", self.dlc) # Длина дана в байтах полезной нагрузки.
|
||
layout.addWidget(self.can_panel) # Обычный CAN занимает то же место, что альтернативная форма SETCAN.
|
||
self.setcan_panel = QWidget() # Общий контейнер хранит поля составного 29-битного идентификатора.
|
||
form = QFormLayout(self.setcan_panel) # Подписи протокола выравниваются с остальными формами.
|
||
form.setContentsMargins(0, 0, 0, 0) # Убираем дополнительные внутренние отступы.
|
||
self.priority = QComboBox() # Приоритет соответствует старшему полю SETCAN ID.
|
||
self.priority.addItems(["0 — критический", "1 — стандартный"]) # Подписи сохраняют существующие значения приоритета протокола.
|
||
self.priority.setCurrentIndex(1) # По умолчанию используется стандартный приоритет.
|
||
self.route = QComboBox() # Направление отличает запрос мастера от сообщения устройства.
|
||
self.route.addItems(["0 — от мастера (PM)", "1 — от устройства"]) # Сохраняем совместимые значения поля ПМ.
|
||
self.dtype = QSpinBox() # Тип устройства выбирается отдельно от номера экземпляра.
|
||
self.dtype.setRange(0, 7) # Поле Device Type занимает три бита SETCAN ID.
|
||
self.dtype.setValue(1) # Сохраняем прежний исходный тип устройства.
|
||
self.did = QSpinBox() # Номер устройства задаёт адрес внутри выбранного типа.
|
||
self.did.setRange(0, 15) # Поле Device занимает четыре бита SETCAN ID.
|
||
self.did.setValue(2) # Сохраняем прежний исходный адрес устройства.
|
||
self.mtype = QComboBox() # Тип сообщения определяет подтип и назначение Body.
|
||
self.mtype.addItems([item.name for item in pc.MsgType]) # Каталог типов берётся из общего протокольного модуля.
|
||
self.mtype.setCurrentText(pc.MsgType.ANALOG.name) # Начальный протокольный пример остаётся аналоговым сообщением.
|
||
self.sub = QComboBox() # Набор подтипов меняется вместе с MsgType.
|
||
self.arg = QLineEdit("1") # Числовой аргумент поддерживает десятичный ввод и префикс 0x.
|
||
self.settings_body = settings_factory() # Используем переданный редактор байтов Z и Y без зависимости от приложения.
|
||
self.arg_panel = QWidget() # Альтернативные редакторы аргумента занимают одну строку.
|
||
arg_layout = QHBoxLayout(self.arg_panel) # Горизонтальная компоновка сохраняет левое выравнивание SETTINGS.
|
||
arg_layout.setContentsMargins(0, 0, 0, 0) # Исключаем сдвиг аргумента относительно остальных редакторов.
|
||
arg_layout.addWidget(self.arg) # Обычный аргумент виден для всех типов кроме SETTINGS.
|
||
arg_layout.addWidget(self.settings_body) # Два байта SETTINGS заменяют единое поле аргумента.
|
||
arg_layout.addStretch(1) # Свободная ширина остаётся справа от редакторов.
|
||
self.count = QSpinBox() # RegCount используется протокольными типами Modbus.
|
||
self.count.setRange(0, 15) # Поле количества занимает четыре бита Body Modbus.
|
||
self.count.setValue(1) # Исходный запрос относится к одному регистру.
|
||
self.sub_label = QLabel("Подтип") # Подпись уточняется при выборе типа сообщения.
|
||
self.arg_label = QLabel("SensorID / адрес") # Назначение аргумента зависит от MsgType.
|
||
rows = [("Priority", self.priority), ("ПМ", self.route), ("Device Type (0..7)", self.dtype), ("Device (0..15)", self.did), ("Msg Type", self.mtype), (self.sub_label, self.sub), (self.arg_label, self.arg_panel), ("RegCount (Modbus)", self.count)] # Единый порядок полей исключает расхождение форм.
|
||
for label, control in rows: # Все протокольные строки добавляются из одного визуального описания.
|
||
form.addRow(label, control) # Подпись связана с соответствующим редактором.
|
||
layout.addWidget(self.setcan_panel) # Протокольная форма показывается только в режиме SETCAN.
|
||
self.data = QLineEdit() # Полезная нагрузка общая для обоих способов ввода идентификатора.
|
||
self.data.setPlaceholderText("01 02 03 04 05 06 07 08") # Пример показывает формат ввода HEX-байтов.
|
||
data_form = QFormLayout() # Общая строка Data остаётся видимой при смене режима.
|
||
data_form.addRow("Data (HEX, до 8 байт)", self.data) # Указываем ограничение классического CAN.
|
||
layout.addLayout(data_form) # Данные завершают форму перед кнопками владельца.
|
||
for control in (self.mode, self.data): # Длинные поля получают достаточную ширину в обоих режимах.
|
||
control.setMaximumWidth(420) # Предел 420 пикселей сохраняет компактность исходного конструктора.
|
||
self.format.setMaximumWidth(340) # Название формата целиком помещается в строке шириной до 340 пикселей.
|
||
for control in (self.priority, self.route, self.mtype, self.sub, self.arg, self.can_id): # Одинаковая ширина полей ID задаётся в общем компоненте.
|
||
control.setMaximumWidth(220) # Предел 220 пикселей соответствует прежней форме SETCAN.
|
||
for control in (self.dtype, self.did, self.count, self.dlc): # Короткие числовые поля не растягиваются по ширине панели.
|
||
control.setMaximumWidth(100) # Предел 100 пикселей достаточен для всех допустимых значений.
|
||
font = QFont("Consolas", 10) # Шрифт с фиксированной шириной знаков упрощает чтение HEX.
|
||
for control in (self.can_id, self.data): # Числовой HEX и полезная нагрузка оформляются одинаково.
|
||
control.setFont(font) # Используем один шрифт для всех владельцев компонента.
|
||
self._subtype() # Настраиваем первоначальные подписи до вычисления ширины столбца.
|
||
labels = [part.labelForField(part.itemAt(row, QFormLayout.ItemRole.FieldRole).widget()) for part in (mode_form, can_form, form, data_form) for row in range(part.rowCount())] # Собираем подписи всех форм для общего выравнивания.
|
||
width = max(label.sizeHint().width() for label in labels) # Ширина столбца рассчитывается в пикселях по текущему шрифту Qt.
|
||
for label in labels: # Поля обоих режимов должны начинаться в одной позиции.
|
||
label.setMinimumWidth(width) # Самая длинная подпись задаёт единый левый столбец.
|
||
for container in (self, self.can_panel, self.setcan_panel, self.arg_panel): # Промежуточные контейнеры наследуют фон панели без тёмных полос.
|
||
container.setObjectName("canBuilderFields") # Локальный селектор ограничивает правило контейнерами формы.
|
||
container.setStyleSheet("QWidget#canBuilderFields { background: transparent; }") # Редакторы сохраняют собственный штатный фон.
|
||
self.mode.currentIndexChanged.connect(self._mode_changed) # Переключение режима одновременно меняет видимость и отменяет готовность кадра.
|
||
self.mtype.currentIndexChanged.connect(self._subtype) # Новый MsgType обновляет подтипы и редактор Body.
|
||
for control in (self.format, self.priority, self.route, self.mtype, self.sub): # Любой выбор из списка меняет будущий кадр.
|
||
control.currentIndexChanged.connect(lambda *_: self.changed.emit()) # Отбрасываем индекс Qt, поскольку владельцу нужен только сигнал устаревания результата.
|
||
for control in (self.can_id, self.arg, self.data): # Все текстовые поля участвуют в сборке кадра.
|
||
control.textChanged.connect(lambda *_: self.changed.emit()) # Отбрасываем текст сигнала Qt и отменяем старый кадр даже при неверном промежуточном вводе.
|
||
for control in (self.dlc, self.dtype, self.did, self.count, self.settings_body.z, self.settings_body.y): # Слежение включает оба байта SETTINGS.
|
||
control.valueChanged.connect(lambda *_: self.changed.emit()) # Отбрасываем числовой аргумент Qt, сохраняя единый сигнал необходимости новой сборки.
|
||
self._mode_changed() # Начальная видимость одинакова у всех потребителей.
|
||
|
||
def _mode_changed(self): # Режим определяет только видимость; введённые значения сохраняются.
|
||
raw = self.mode.currentData() == "can" # Полный CAN ID вводится только в обычном режиме.
|
||
self.can_panel.setVisible(raw) # Показываем формат, ID и DLC для обычного CAN.
|
||
self.setcan_panel.setVisible(not raw) # Альтернативно показываем составные поля SETCAN.
|
||
self.changed.emit() # Смена способа ввода отменяет ранее собранный кадр.
|
||
|
||
def _subtype(self): # Общие подписи и список подтипов используются всеми экземплярами формы.
|
||
msg_type = pc.MsgType[self.mtype.currentText()] # Значение выбирается из существующего общего каталога.
|
||
settings = msg_type == pc.MsgType.SETTINGS # Только SETTINGS редактируется отдельными байтами Z и Y.
|
||
self.arg.setVisible(not settings) # Единый числовой аргумент скрывается для SETTINGS.
|
||
self.settings_body.setVisible(settings) # Двухбайтовый редактор появляется вместо числового аргумента.
|
||
table = {pc.MsgType.BROADCAST: (pc.BroadcastType, "Broadcast.Type", "Body (0..15)"), pc.MsgType.DISCRETE: (pc.DiscreteType, "Discrete.Type", "Body (0..4095)"), pc.MsgType.ANALOG: (pc.AnalogType, "Analog.Type", "SensorID (0..4095)")} # Таблица определяет визуальные подписи существующих протокольных перечислений.
|
||
current = self.sub.currentText() # Сохраняем выбранный подтип, если он допустим для нового сообщения.
|
||
self.sub.clear() # Старые варианты не должны остаться в новом типе сообщения.
|
||
self.sub.setEnabled(msg_type in table) # Отдельный подтип доступен только у трёх перечисленных типов.
|
||
if msg_type in table: # Подписи и значения перечислений берутся из одного места.
|
||
enum, sub_label, arg_label = table[msg_type] # Получаем визуальное описание выбранного MsgType.
|
||
self.sub.addItems([item.name for item in enum]) # Значения берутся из общего протокольного каталога.
|
||
if current in [item.name for item in enum]: # Совместимый выбор пользователя сохраняется.
|
||
self.sub.setCurrentText(current) # Восстанавливаем допустимый подтип после заполнения списка.
|
||
else: # Типы без отдельного подтипа используют Body по собственному назначению.
|
||
sub_label = "—" # Подпись явно показывает отсутствие подтипа.
|
||
arg_label = "Body (обычно 0)" # Для PULSE сохраняем исходное описание аргумента.
|
||
if msg_type in (pc.MsgType.MODBUS_COIL, pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Все четыре вида Modbus задают начальный адрес.
|
||
arg_label = "StrAdr (0..4095)" # Двенадцатибитный адрес показан в прежних единицах регистров.
|
||
elif msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес.
|
||
arg_label = "Адрес регистра (0..65535)" # Полный 16-битный диапазон указан в подписи.
|
||
elif msg_type == pc.MsgType.ERROR: # Код ошибки вводится в отдельный байт Body.
|
||
arg_label = "ErrorCode (0..255)" # Показываем допустимый однобайтовый диапазон.
|
||
elif settings: # SETTINGS редактируется двухбайтовым компонентом.
|
||
arg_label = "Body: Z | Y" # Подпись соответствует порядку байтов протокола.
|
||
self.sub_label.setText(sub_label) # Одинаковая подпись подтипа обновляется во всех вкладках.
|
||
self.arg_label.setText(arg_label) # Аргумент всегда подписан по выбранному MsgType.
|
||
|
||
def body(self, msg_type, arg, count): # Общая диспетчеризация вызывает существующие обёртки C-ядра без второй реализации wire-кодека.
|
||
if msg_type == pc.MsgType.BROADCAST: # BROADCAST объединяет подтип и четырёхбитный аргумент.
|
||
return pc.merge_broadcast(pc.BroadcastType[self.sub.currentText()], arg & 0xF) # Маска 0xF сохраняет прежнее ограничение аргумента четырьмя битами.
|
||
if msg_type == pc.MsgType.DISCRETE: # Дискретное сообщение использует собственное перечисление подтипов.
|
||
return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем.
|
||
if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body.
|
||
return pc.merge_analog(pc.AnalogType[self.sub.currentText()], arg) # Существующий кодек задаёт расположение подтипа и SensorID.
|
||
if msg_type in (pc.MsgType.MODBUS_COIL, pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # У четырёх видов Modbus одинаковый формат Body.
|
||
return pc.merge_modbus(arg, count) # Адрес и количество объединяются общей функцией протокола.
|
||
if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника.
|
||
return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра.
|
||
return arg & 0xFFFF # Остальные типы сохраняют прежнее ограничение Body шестнадцатью битами.
|
||
|
||
def frame_values(self): # Возвращаем проверенные параметры CAN без упаковки конкретного транспорта.
|
||
data = tr.parse_hex(self.data.text()) # Общий разбор сохраняет поддержку пробелов, запятых и префиксов 0x.
|
||
if len(data) > 8: # Классический CAN не допускает более восьми байтов данных.
|
||
raise ValueError("данные длиннее 8 байт") # Сообщаем об ошибке до передачи в транспорт.
|
||
if self.mode.currentData() == "can": # Обычный режим использует полный идентификатор и явный DLC.
|
||
identifier = int(self.can_id.text().strip(), 16) # Поле ID всегда интерпретируется как HEX.
|
||
extended = bool(self.format.currentData()) # Малый числовой ID также может иметь выбранный EXT.
|
||
tr.validate_can_id(identifier, extended) # Проверка общего транспорта запрещает усечение ID до допустимого диапазона.
|
||
if len(data) != self.dlc.value(): # Количество введённых байтов обязано точно совпадать с DLC.
|
||
raise ValueError(f"DLC = {self.dlc.value()}, а Data содержит {len(data)} байт; количество байтов должно совпадать.") # Оператор видит обе длины и может исправить ввод.
|
||
return identifier, extended, data, None # У обычного CAN нет отдельного протокольного Body.
|
||
msg_type = pc.MsgType[self.mtype.currentText()] # SETCAN использует выбранный тип сообщения.
|
||
arg = self.settings_body.value() if msg_type == pc.MsgType.SETTINGS else int(self.arg.text().strip() or "0", 0) # SETTINGS читается из Z/Y, остальные аргументы поддерживают привычные числовые префиксы.
|
||
body = self.body(msg_type, arg, self.count.value()) # Один путь сборки Body применяется ко всем владельцам формы.
|
||
identifier = pc.ProtoCanId.build(self.priority.currentIndex(), self.route.currentIndex(), self.dtype.value(), self.did.value(), msg_type, body) # Идентификатор собирается существующей обёрткой общего C-ядра.
|
||
return identifier, True, data, body # SETCAN всегда передаётся с расширенным 29-битным идентификатором.
|
||
|
||
def bind(self, owner, prefix): # Сохраняем публичные имена полей приложения при переходе на общий компонент.
|
||
names = ("mode", "can_panel", "setcan_panel", "format", "can_id", "dlc", "priority", "route", "dtype", "did", "mtype", "sub", "arg", "settings_body", "count", "data", "sub_label", "arg_label") # Совместимые атрибуты позволяют не менять интеграцию транспорта и внешние потребители.
|
||
for name in names: # Каждое старое имя ссылается на настоящий виджет общего компонента.
|
||
setattr(owner, prefix + name, getattr(self, name)) # Префиксы build_, builder_ и rs485_builder_ разделяют три независимых состояния.
|