Files
templates/python/pysetterminal/pbus_panel.py
Andrey a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00

347 lines
59 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Таблица наблюдаемых регистров PBUS внутри общей панели команд SETGUI."""
from PySide6.QtCore import Qt, QEvent, QTimer, QItemSelectionModel, QSize, QRect # Геометрия Qt позволяет компактно переносить связанные элементы в узком доке.
from PySide6.QtGui import QIntValidator # Проверяем четырёхбитные адреса платы и регистра ещё при вводе.
from PySide6.QtWidgets import QWidget, QLayout, QSizePolicy, QGroupBox, QVBoxLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QPlainTextEdit # Адаптивная строка сохраняет компактные действия независимо от ширины таблицы результатов.
from .bus_operations import table_entries_after, pbus_table_steps, pbus_control, pbus_result_address, PBUS_COMMAND, PBUS_SETTLE_MS, PBUS_ERROR_LIMIT # Полная конфигурация, адреса CD и остановка используют единый формат слов памяти TMS.
class _FlowLayout(QLayout): # Связанные элементы располагаются рядом и переносятся целиком при недостатке ширины.
def __init__(self, parent, spacing): # Промежуток задаётся в пикселях одинаково для горизонтального и вертикального переноса.
super().__init__(parent) # Компоновка принадлежит только своему компактному контейнеру.
self.items = [] # Храним порядок полей и действий без изменения их владельцев при переносе.
self.setContentsMargins(0, 0, 0, 0) # Контейнер не добавляет второго слоя отступов формы.
self.setSpacing(spacing) # Сохраняем небольшое постоянное расстояние между связанными элементами.
self.setProperty("fixedPanelLayout", True) # Общий менеджер панелей не должен заменять перенос элементов сплиттером.
def addItem(self, item): # Qt вызывает этот метод при добавлении виджета в адаптивную строку.
self.items.append(item) # Сохраняем порядок элементов, заданный формой пользователя.
def count(self): # Предоставляем Qt фактическое число элементов компоновки.
return len(self.items) # Перенос не создаёт дополнительных или скрытых копий действий.
def itemAt(self, index): # Менеджер геометрии получает элемент по индексу без удаления.
return self.items[index] if 0 <= index < len(self.items) else None # Невалидный индекс завершает стандартный обход компоновки.
def takeAt(self, index): # Qt извлекает элемент при удалении или уничтожении контейнера.
return self.items.pop(index) if 0 <= index < len(self.items) else None # Ссылки на элементы удаляются вместе с компоновкой.
def expandingDirections(self): # Свободное пространство не должно растягивать промежутки между действиями.
return Qt.Orientations(0) # Элементы используют собственные размеры, оставляя лишнюю ширину справа.
def hasHeightForWidth(self): # Высота зависит от числа строк после переноса по доступной ширине.
return True # Владелец учитывает дополнительную строку, не обрезая кнопку или результат.
def heightForWidth(self, width): # Измеряем необходимую высоту до назначения окончательной геометрии.
return self._arrange(QRect(0, 0, width, 0), False) # Расчёт не перемещает виджеты при измерении размеров.
def minimumSize(self): # Узкий док должен вмещать самый широкий отдельный элемент, а не сумму всех кнопок.
size = QSize(0, 0) # Начальная минимальная область не занимает места до добавления элементов.
for item in self.items: # Каждая кнопка или карточка переносится целиком и сохраняет читаемость.
size = size.expandedTo(item.minimumSize()) # Минимальная ширина равна самому широкому элементу, высота — минимальной строке.
return size # Перенос позволяет сохранить равные доли ширины команд и RAW-панели.
def sizeHint(self): # Естественный размер подстраивается к ширине, которую даст внешний контейнер.
return self.minimumSize() # Дополнительная высота вычисляется через heightForWidth при реальном размещении.
def setGeometry(self, rect): # Qt назначает доступную область адаптивной строки.
super().setGeometry(rect) # Сохраняем стандартную геометрию компоновки для родительского виджета.
self._arrange(rect, True) # Размещаем элементы по строкам внутри выделенной области.
def _arrange(self, rect, apply): # Один расчёт используется измерением и реальным размещением элементов.
left, x, y, row_height = rect.x(), rect.x(), rect.y(), 0 # Начинаем каждую строку у левого края контейнера.
for item in self.items: # Порядок управления сохраняется при любом размере дока.
size = item.sizeHint().expandedTo(item.minimumSize()) # Не сжимаем подписи, стрелки интервала или считанное слово.
if x > left and x + size.width() > left + rect.width(): # Элемент переносится, если целиком не помещается после предыдущего.
x, y, row_height = left, y + row_height + self.spacing(), 0 # Следующая строка начинается без пустого промежутка слева.
if apply: # Во время измерения геометрия виджетов остаётся неизменной.
item.setGeometry(QRect(x, y, size.width(), size.height())) # Поля и действия занимают свою естественную ширину рядом друг с другом.
x += size.width() + self.spacing() # Следующий элемент получает только небольшой постоянный промежуток.
row_height = max(row_height, size.height()) # Высота строки вмещает самый высокий её элемент.
return y + row_height - rect.y() # Внешняя компоновка получает общую высоту всех занятых строк.
class CompactControls(QWidget): # Компактная строка переносит связанные поля, карточки или действия без растягивания по краям окна.
def __init__(self, widgets, parent=None, spacing=8): # По умолчанию элементы разделены восьмью пикселями.
super().__init__(parent) # Контейнер остаётся частью существующего блока своей шины.
policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # Доступная ширина используется для переноса, высота соответствует содержимому.
policy.setHeightForWidth(True) # Узкий док выделяет высоту для следующей строки вместо увеличения минимальной ширины окна.
self.setSizePolicy(policy) # Внешняя область корректно учитывает адаптивную высоту формы.
layout = _FlowLayout(self, spacing) # Все элементы используют один расчёт переноса и минимального размера.
for widget in widgets: # Связанные действия добавляются в пользовательском порядке.
layout.addWidget(widget) # Перенос не изменяет обработчики команд и результаты операции.
class PbusPanel(QGroupBox): # Хранит один общий список источников PBUS и результаты их считывания.
def __init__(self, owner): # Владелец предоставляет единую очередь PBUS, SBUS и смешанных макросов.
super().__init__("PBUS (Параллельная шина)", owner) # Русская подпись обозначает правый блок существующей панели SETGUI.
self.owner, self.entries, self.applied = owner, [], False # Видимый список отдельно учитывает подтверждение его аппаратной конфигурации.
self.controller = None # Таблица считается применённой только для контроллера, которому отправлена конфигурация.
self._controls, self._applied_signature = {}, None # Последние подтверждённые управляющие слова хранятся отдельно для каждого контроллера.
self.table_errors = PBUS_ERROR_LIMIT # Начальный аппаратный порог равен десяти ошибкам, как в рабочем слове запуска 0x8Axx.
self.setObjectName("pbusGroup") # Стабильное имя сохраняет поиск панели в интеграциях и проверках интерфейса.
layout = QVBoxLayout(self) # Таблица и журнал получают свободную область независимо от ширины полей добавления.
layout.setContentsMargins(10, 14, 10, 10) # Отступы в пикселях совпадают с соседним блоком SBUS.
layout.setSpacing(8) # Параметры, действия и результаты образуют плотные связанные области.
layout.setProperty("fixedPanelLayout", True) # Общий менеджер панелей не должен превращать компактные формы в растягиваемые области.
controls = QWidget(self) # Адрес платы и регистр не делят столбцы с широкой таблицей результатов.
controls.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) # Контейнер ограничивается естественной шириной подписей и полей.
form = QGridLayout(controls) # Два компактных столбца сохраняют значения рядом с подписями.
form.setContentsMargins(0, 0, 0, 0) # Вложенная форма не добавляет второго уровня внешних отступов.
form.setHorizontalSpacing(10) # Подпись и её поле разделены десятью пикселями вместо ширины всей панели.
form.setVerticalSpacing(6) # Строки адреса и регистра остаются визуально связанными.
form.setProperty("fixedPanelLayout", True) # Форма сохраняет компактную геометрию при применении общей раскладки приложения.
self.controls = {} # Публичные элементы доступны владельцу без копирования их состояния.
for row, (name, caption) in enumerate((("tetrad_edit", "Адрес платы (0–15):"), ("register_edit", "Регистр (0–15):"))): # Для добавления достаточно указать источник данных, индексы буфера вычисляются автоматически.
field = QLineEdit("0") # Начальное значение позволяет добавить первый регистр без заполнения дополнительных аппаратных полей.
field.setValidator(QIntValidator(0, 15, field)) # Каждый адрес занимает одну тетраду дескриптора x_parallel_bus_add_table.
field.setFixedWidth(90) # Девяносто пикселей достаточно для адреса; поле не уезжает к правому краю широкого дока.
form.addWidget(QLabel(caption), row, 0) # Подпись объясняет допустимый адрес платы или регистра.
form.addWidget(field, row, 1) # Поле находится непосредственно рядом со своей подписью.
self.controls[name] = field # Сохраняем адресные поля для кнопки добавления и внешних проверок.
field.returnPressed.connect(self.add_register) # Enter в адресных полях добавляет регистр без перемещения к кнопке.
self.error_limit = QSpinBox(self) # Отдельный параметр правой колонки задаёт аппаратный порог ошибок PBUS.
self.error_limit.setRange(1, 15) # Порог занимает четыре бита управляющего слова; допустимы целые значения от одного до пятнадцати.
self.error_limit.setValue(PBUS_ERROR_LIMIT) # Значение десять соответствует шестнадцатеричному порогу 0xA.
self.error_limit.setFixedWidth(90) # Поле порога выровнено с существующими компактными адресными полями.
self.error_limit.setToolTip("Порог в битах 11:8 регистра 0x200C. 10 = 0xA. Применяется при добавлении, изменении таблицы или нажатии «Обновить».") # Пользователь видит единицы и момент аппаратного применения выбранного числа ошибок.
form.addWidget(QLabel("Порог ошибок (1–15):"), 2, 0) # Подпись отличает аппаратный порог от периода чтения и паузы после запуска.
form.addWidget(self.error_limit, 2, 1) # Настройка находится в блоке PBUS рядом с платой и регистром.
self.controls["error_limit"] = self.error_limit # Порог доступен владельцу как pbus_error_limit вместе с остальными элементами PBUS.
layout.addWidget(controls, 0, Qt.AlignLeft) # Компактная форма начинается слева и не растягивается вместе с таблицей.
self.add_btn, self.remove_btn = QPushButton("Добавить в таблицу"), QPushButton("Убрать из таблицы") # Подписи соответствуют работе со списком вместо ручного чтения и записи.
self.edit_btn = QPushButton("Изменить строку", self) # Адрес и регистр выделенной строки заменяются значениями полей без ручного удаления и добавления.
self.edit_btn.setFixedHeight(30) # Действие сохраняет существующую высоту кнопок управления таблицей.
self.edit_btn.setToolTip("Заменить выделенную строку адресом платы и регистром из полей выше.") # Пользователь видит, какие параметры будут применены к выбранному источнику.
self.edit_btn.clicked.connect(self.edit_selected) # Редактирование ставит полную перенастройку в ту же очередь памяти контроллера.
self.add_btn.clicked.connect(self.add_register) # Добавление само применяет таблицу и получает первое считанное значение.
self.remove_btn.clicked.connect(self.remove_selected) # Кнопка удаляет все выделенные строки независимо от текущей ячейки.
layout.addWidget(CompactControls((self.add_btn, self.edit_btn, self.remove_btn), self)) # Действия находятся рядом под адресом и переносятся в узком доке вместо сжатия RAW-панели.
self.auto_update = QCheckBox("Циклическое чтение") # Переключатель управляет запросами буфера, а не созданием независимого макроса.
self.auto_update.setChecked(True) # После явного добавления регистра таблица продолжает показывать текущие считанные значения.
self.interval = QSpinBox() # Интервал ограничивает частоту запросов ПК к контроллеру.
self.interval.setRange(1, 3600000) # Период чтения задаётся в миллисекундах от одной до одного часа. # Интервал от 100 мс до 60 с позволяет выбрать удобную частоту обновления без непрерывного цикла Qt.
self.interval.setValue(1000) # По умолчанию обновляем результаты раз в секунду, не перегружая последовательное соединение.
self.interval.setSuffix(" мс") # Единицы измерения видны непосредственно в поле интервала.
self.interval.setFixedWidth(120) # Сто двадцать пикселей вмещают число, единицы и стрелки темы без растягивания поля по всей панели.
self.startup_delay = QSpinBox(self) # Отдельная пауза относится только к аппаратному обновлению после запуска новой таблицы.
self.startup_delay.setRange(1, 60000) # Пользователь задаёт ожидание контроллера от одной миллисекунды до минуты.
self.startup_delay.setValue(PBUS_SETTLE_MS) # Начальное ожидание составляет сто миллисекунд и не заменяет период чтения.
self.startup_delay.setSuffix(" мс") # Поле явно обозначает единицу измерения задержки.
self.startup_delay.setFixedWidth(120) # Пауза занимает компактное место рядом со своей подписью.
self.startup_delay.setToolTip("После подтверждения запуска PBUS ждать аппаратного обновления памяти; прежние значения буфера до этого не показываются.") # Подсказка объясняет отличие от периода обычного опроса.
self.refresh_btn = QPushButton("Обновить") # Ручное обновление полезно при выключенном автоопросе или после подключения порта.
self.refresh_btn.clicked.connect(self.refresh) # Получаем все слова текущего списка одной последовательностью запросов.
for widget in (self.auto_update, self.interval, self.refresh_btn): # Размещаем все средства обновления в одной строке.
widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) # Переключатель и действия занимают необходимую ширину вместо распределения по краям окна.
layout.addWidget(CompactControls((self.auto_update, self.interval, self.refresh_btn), self)) # Средства обновления остаются компактной группой и переносятся при недостатке ширины.
layout.addWidget(CompactControls((QLabel("После запуска:"), self.startup_delay), self)) # Пауза настройки отделена от строки периодического чтения буфера.
self.table = QTableWidget(0, 5) # Одна строка соответствует одному добавленному регистру PBUS.
self.table.setObjectName("pbusResultsTable") # Имя позволяет проверять и находить новую таблицу результатов в SETGUI.
self.table.setHorizontalHeaderLabels(["Плата", "Регистр", "HEX", "DEC", "Состояние"]) # Показываем источник, оба представления слова и достоверность результата.
self.table.setSelectionBehavior(QAbstractItemView.SelectRows) # Выделение целой строки делает удаление регистра однозначным.
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) # Shift и Ctrl позволяют удалить несколько регистров одним действием.
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) # Изменение считанных значений не должно подменять аппаратный результат.
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) # Короткие адреса и значения занимают только необходимую ширину.
self.table.horizontalHeader().setStretchLastSection(True) # Состояние использует оставшуюся ширину, не обрезая описание ошибки.
self.table.setMinimumHeight(150) # Таблица остаётся пригодной для нескольких источников даже в компактной раскладке доков.
self.table.installEventFilter(self) # Клавиша Delete удаляет выбранные источники, когда таблица имеет фокус.
self.table.itemSelectionChanged.connect(self._selection_changed) # Доступность удаления соответствует текущему выделению строк.
layout.addWidget(self.table, 1) # Таблица результатов получает свободную высоту внутри блока PBUS.
self.result_label = QLabel("Добавьте регистры для считывания") # Начальный статус объясняет первое действие пользователя.
self.result_label.setWordWrap(True) # Диагностика остаётся внутри правого блока при изменении размера окна.
layout.addWidget(self.result_label) # Последнее состояние относится непосредственно к таблице PBUS.
self.output = QPlainTextEdit() # Собственный небольшой журнал сохраняет результаты изменения таблицы и пакеты ответов.
self.output.setReadOnly(True) # Пользователь может копировать ответы без изменения истории операций.
self.output.setMaximumBlockCount(200) # Ограничиваем историю, чтобы автообновление не увеличивало память бесконечно.
self.output.setFixedHeight(55) # Две-три строки журнала не вытесняют общую область макросов.
layout.addWidget(self.output) # Ответы PBUS сохраняются в правом блоке, без общей нижней панели результатов.
self.timer = QTimer(self) # Таймер принадлежит панели и прекращает существование вместе с ней.
self.timer.setTimerType(Qt.PreciseTimer) # Период циклического чтения не должен сокращаться из-за неточности таймера.
self.timer.timeout.connect(self._poll_tick) # Каждый такт читает только уже применённую аппаратную таблицу.
self.interval.valueChanged.connect(self._update_timer) # Новый интервал применяется к следующим циклам считывания.
self.auto_update.toggled.connect(self._auto_changed) # Пользователь может остановить обновление, не удаляя источники таблицы.
self.controls.update({"add_btn": self.add_btn, "remove_btn": self.remove_btn, "table": self.table, "result_label": self.result_label, "output": self.output}) # Экспортируем единственную таблицу и её действия для владельца.
self._selection_changed() # До выделения первой строки удаление недоступно.
def command(self, operation, **parameters): # Создаём одинаковые команды для ручных действий и общего исполнителя макросов.
return {"bus": "pbus", "type": operation, "tetrad": 0, "register": 0, "errors": self.error_limit.value(), "settle_ms": self.startup_delay.value(), **parameters} # Действие фиксирует текущие параметры таблицы и паузу при постановке в очередь.
def add_register(self): # Добавляем заданный адрес платы и регистра, не требуя номера строки буфера.
try: # Пустое поле не должно запускать аппаратную запись с неверным адресом.
command = self.command("add", tetrad=int(self.controls["tetrad_edit"].text()), register=int(self.controls["register_edit"].text())) # Фиксируем источник перед асинхронным обменом.
self.owner._start_commands([command], macro=False) # Таблица применяется через тот же транспорт, что SBUS и общие макросы.
except ValueError as error: # Ошибка ввода остаётся в области PBUS и не влияет на другой блок.
self.result_label.setText(f"PBUS: проверьте адрес и регистр: {error}") # Пользователь видит причину отказа непосредственно возле таблицы.
def edit_selected(self): # Одна выбранная строка заменяется целым снимком таблицы с сохранением остальных источников и CD.
selected = self.table.selectionModel().selectedRows() # Фиксируем выделение в момент нажатия до ожидания очереди.
if len(selected) != 1: # Множественное выделение не должно случайно заменить несколько регистров одним адресом.
return # Пользователь должен выбрать конкретную редактируемую строку.
try: # Проверяем новые адреса ещё до остановки аппаратного опроса.
entries = [dict(entry) for entry in self.entries] # Полный снимок сохраняет порядок и явные CD всех остальных записей.
entries[selected[0].row()].update(tetrad=int(self.controls["tetrad_edit"].text()), register=int(self.controls["register_edit"].text())) # Меняются только плата и регистр выбранной строки, индекс и назначение результата сохраняются.
self.replace_entries(entries) # Очередь выполнит остановку, перепись всех записей и запуск как одну операцию.
except (ValueError, TypeError) as error: # Неверный ввод не меняет подтверждённую аппаратную таблицу.
self.result_label.setText(f"PBUS: проверьте адрес и регистр: {error}") # Причина отказа остаётся в собственной области PBUS.
def remove_selected(self): # Удаляем все выделенные регистры кнопкой или клавишей Delete.
selected = sorted({index.row() for index in self.table.selectionModel().selectedRows()}) # Множественное выделение собираем один раз до переписи списка.
if not selected: # При отсутствии выделения список и аппаратная конфигурация остаются прежними.
return # Не создаём пустую аппаратную операцию удаления.
keys = [(self.entries[row]["tetrad"], self.entries[row]["register"]) for row in selected] # Удаление фиксируется по адресам, поэтому смещение индексов не удалит соседний регистр.
self.owner._start_commands([self.command("remove", remove_keys=keys)], macro=False) # Оставшиеся строки уплотняются и заново записываются в аппаратную таблицу.
def eventFilter(self, watched, event): # Обеспечиваем привычное удаление строк клавишей Delete в таблице результатов.
if watched is self.table and event.type() == QEvent.KeyPress and event.key() == Qt.Key_Delete: # Обрабатываем Delete только внутри своей таблицы PBUS.
self.remove_selected() # Используем то же действие, что кнопка удаления выделенных строк.
event.accept() # Исключаем дополнительную обработку Delete таблицей Qt.
return True # Событие полностью обработано удалением выбранных регистров.
return super().eventFilter(watched, event) # Остальные клавиши сохраняют штатное выделение и навигацию таблицы.
def _selection_changed(self): # Согласуем кнопку удаления с реальным выделением таблицы.
self.remove_btn.setEnabled(bool(self.table.selectionModel().selectedRows())) # Пользователь сразу видит, когда выбран регистр для удаления.
self.edit_btn.setEnabled(len(self.table.selectionModel().selectedRows()) == 1) # Изменение строки доступно для единственного выделенного источника, независимо от занятости UART.
def _render_entries(self): # Индексы видимой таблицы всегда соответствуют порядку аппаратного буфера после переписи.
selected = {(self.table.item(index.row(), 0).text(), self.table.item(index.row(), 1).text()) for index in self.table.selectionModel().selectedRows() if self.table.item(index.row(), 0) is not None} # Сохраняем выделение по адресам, чтобы обновление и уплотнение не выбирали другой регистр.
self.table.blockSignals(True) # Промежуточное заполнение не должно сбрасывать доступность удаления несколько раз за цикл.
self.table.setRowCount(len(self.entries)) # Добавление и удаление изменяют единую таблицу результатов.
self.table.clearSelection() # Старые числовые индексы после удаления могут относиться уже к другой плате.
for row, entry in enumerate(self.entries): # Каждая строка содержит стабильные адреса платы и регистра.
values = (str(entry["tetrad"]), str(entry["register"]), "—", "—", "Ожидает считывания") # До подтверждения новой таблицы не показываем старое слово как свежие данные.
for column, text in enumerate(values): # Заполняем источник, два представления значения и начальный статус.
self.table.setItem(row, column, QTableWidgetItem(text)) # Ячейки доступны для выделения и копирования без редактирования результата.
if (str(entry["tetrad"]), str(entry["register"])) in selected: # Сохраняем только те выделенные источники, которые ещё присутствуют в списке.
self.table.selectionModel().select(self.table.model().index(row, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) # Восстанавливаем также множественное выделение после фонового считывания.
self.table.blockSignals(False) # Возвращаем обработку пользовательского изменения выделения.
self._selection_changed() # Кнопка удаления снова соответствует восстановленному выделению.
def _signature(self, entries, errors): # Отпечаток содержит именно аппаратно значимые поля и порядок всей таблицы.
return (errors, tuple((entry["tetrad"], entry["register"], pbus_result_address(entry, index)) for index, entry in enumerate(entries))) # Различаем индекс записи и фактический адрес 20CD каждой строки.
def replace_entries(self, entries): # Изменение состава, порядка или адресов передаётся как полный снимок новой таблицы.
self.owner._start_commands([self.command("configure", entries=[dict(entry) for entry in entries])], macro=False) # Повторный макрос и фоновый опрос не смогут вмешаться в атомарную перепись всего снимка.
def sequence(self, command): # Одна логическая операция очереди охватывает остановку, все пары таблицы, запуск, паузу и первое чтение.
operation = command["type"] # Действие определяет изменение таблицы либо чтение уже подтверждённого буфера.
updated = table_entries_after(self.entries, command) # Полный снимок сохраняет явные CD и проверяет все источники до остановки.
if updated != self.entries: # Любое изменение источника, порядка или CD требует применения всей новой таблицы.
self.entries, self.applied = updated, False # Старый результат не подтверждает новые дескрипторы аппаратного опроса.
self._render_entries() # Очищаем отображение прежних слов ещё до начала первой управляющей записи.
controller, errors = self.owner._macro_controller, command.get("errors", self.table_errors) # Контроллер и аппаратный порог фиксируются для всей операции очереди.
signature = self._signature(self.entries, errors) # Сравниваем полный порядок и адреса, а не только длину списка.
configure = not self.applied or self.controller != controller or self._applied_signature != signature or operation in ("configure", "start") # Простое чтение не переписывает неизменённую применённую таблицу.
if command.get("read_only") and configure: # Периодическое обновление не имеет права автоматически переписывать неподтверждённую таблицу.
raise ValueError("PBUS: таблица изменена или не применена; выполните явное обновление") # Ошибка не отправляет ни управления, ни запуска контроллеру.
if configure: # До первого WriteMemory таблица перестаёт считаться успешно применённой.
self.applied, self.table_errors = False, errors # При отказе любая автоматическая попытка запуска останется запрещённой.
self._render_entries() # При явном повторном запуске старое слово также не выдаётся за подтверждение новой настройки.
self.result_label.setText("Применение таблицы; ожидание обновления аппаратного буфера…") # Состояние объясняет отсутствие прежних значений во время паузы.
def confirmed_control(value): # Вызывается только после успешного подтверждения соответствующей записи 0x200C.
self._controls[controller] = value # Следующая остановка сохранит реальные параметры прежней активной таблицы.
result = yield from pbus_table_steps(self.entries, errors, configure=configure, stop_control=self._controls.get(controller), settle_ms=command.get("settle_ms", self.startup_delay.value()), on_control=confirmed_control) # Существующий транспорт подтверждает каждый пакет, очередь удерживает всю перенастройку целиком.
result["controller"], result["signature"] = controller, signature # Применённость относится к точному снимку и тому контроллеру, который подтвердил обмен.
result["configured"], result["form_errors"] = configure, command.get("_form_errors", errors) # Сохраняем начало редактирования порога, чтобы не затереть более новое пользовательское значение завершением старой операции.
result["operation"], result["key"] = operation, (command["tetrad"], command["register"]) # Видимая строка добавления выбирается по исходным параметрам действия.
return result # Отображение подтверждается только после успешного чтения обновлённого аппаратного буфера.
def stop_sequence(self, command): # Остановка сохраняет всю текущую таблицу и аппаратные параметры, сбрасывая только бит запуска.
self.timer.stop() # Будущие периодические чтения остановленной шины больше не назначаются.
self._set_cycle_checked(False) # Неотправленные фоновые шаги снимаются обработчиком переключателя.
controller = self.owner._macro_controller # Используем тот же контроллер, что зафиксирован общей очередью для этого действия.
control = self._controls.get(controller, pbus_control(len(self.entries), self.table_errors)) & 0x7FFF # Подтверждённые порог и последний индекс сохраняются даже при изменённом списке интерфейса.
reply = yield PBUS_COMMAND, control # Ждём реального подтверждения остановки через существующий WriteMemory.
self._controls[controller] = control # Только подтверждённая запись переводит аппаратное состояние в остановленное.
return reply # Считанные значения прежней таблицы не переобъявляются результатом новой конфигурации.
def show_result(self, result): # Применяем считанные слова к тем строкам, чьи адреса подтверждены аппаратной конфигурацией.
self.entries = result["entries"] # Список из завершённого цикла соответствует порядку прочитанных слов буфера.
self.applied, self.controller = True, result["controller"] # Подтверждённая таблица допускает дальнейшее чтение без повторной записи дескрипторов.
self._applied_signature = result.get("signature", self._signature(self.entries, self.table_errors)) # Дальнейшие чтения разрешены только для того же состава, порядка и CD.
if result.get("configured") and self.error_limit.value() == result.get("form_errors"): # Явный порог выполненного макроса отражается в форме только при отсутствии более нового ввода пользователя.
self.error_limit.setValue(self.table_errors) # Поле показывает реально применённый порог, не заставляя следующее ручное действие возвращаться к старому значению.
self._render_entries() # Перестраиваем ячейки после добавления, удаления или смены контроллера.
for row, reply in enumerate(result["rows"]): # Каждое считанное слово отображается рядом с собственным источником.
value = reply["value"] # Получаем 16-битное слово из стандартного ответа PEEK транспорта TMS2812.
self.table.item(row, 2).setText(f"{value:04X}") # HEX занимает четыре цифры, как данные SBUS.
self.table.item(row, 3).setText(str(value)) # Десятичное значение помогает читать числовые регистры без ручного перевода.
self.table.item(row, 4).setText("OK") # Успешный цикл подтверждает достоверность слова для данного источника.
self.table.item(row, 2).setToolTip(reply.get("raw_response", {}).get("raw", b"").hex(" ").upper()) # Пакет конкретного чтения доступен в подсказке своей строки.
if result.get("operation") == "add" and not self.owner._macro_is_macro: # Ручное добавление позволяет сразу выделить новый источник для копирования или Delete.
for row, entry in enumerate(self.entries): # Находим добавленный регистр по адресу, независимо от числа уже наблюдаемых строк.
if (entry["tetrad"], entry["register"]) == result["key"]: # Выделяем именно выбранный источник, включая повторное добавление существующей строки.
self.table.selectRow(row) # Следующая клавиша Delete относится к добавленному или найденному регистру.
self.table.setFocus() # Удобное удаление не требует дополнительного щелчка после добавления кнопкой.
break # Источник уникален, поэтому поиск завершается после первой найденной строки.
self._update_timer() # Возобновляем обновление только для применённого непустого списка регистров.
def show_error(self, message): # Ошибка аппаратной записи или чтения не должна оставлять прежние слова выглядеть новыми.
self.applied = False # После ошибки явное обновление повторно применяет список перед считыванием.
self.timer.stop() # Исключаем автоматические повторные записи после ошибки или неопределённого исхода операции.
self._set_cycle_checked(False) # Состояние переключателя честно показывает остановку фоновых запросов.
for row in range(self.table.rowCount()): # Недостоверность отображается в каждой затронутой строке общей таблицы.
self.table.item(row, 2).setText("—") # HEX не сохраняет устаревшее слово после отказа обмена.
self.table.item(row, 3).setText("—") # Десятичное значение также очищается, чтобы представления оставались согласованными.
self.table.item(row, 4).setText("Ошибка") # Краткий статус остаётся виден рядом с адресом регистра.
self.table.item(row, 4).setToolTip(message) # Полная причина доступна для конкретной строки при наведении.
def refresh(self): # Обновление получает все текущие регистры, повторяя настройку только когда она не подтверждена.
self.owner._start_commands([self.command("poll")], macro=False) # Используем единую очередь, поэтому ручное обновление не вмешивается в другой макрос.
def _update_timer(self, *unused): # Таймер сохраняет период от начала чтения, не перезапускаясь после каждого результата.
if self.auto_update.isChecked() and self.entries and self.applied: # Автоматическое чтение разрешено только для подтверждённой непустой таблицы.
if not self.timer.isActive() or self.timer.interval() != self.interval.value(): # Завершение текущего чтения не сдвигает уже отсчитываемый период.
self.timer.start(self.interval.value()) # Новый интервал применяется при включении или изменении настройки.
else: # Пустая таблица и выключенное задание не создают новых чтений.
self.timer.stop() # Отменяем дальнейшие срабатывания таймера этого источника.
def _set_cycle_checked(self, checked): # Внутреннее изменение галочки не является новым нажатием пользователя и не порождает повторных управляющих записей.
previous = self.auto_update.blockSignals(True) # Отключение из обработки ошибки или закрытия не должно рекурсивно планировать WriteMemory.
self.auto_update.setChecked(checked) # Видимое состояние соответствует фактически разрешённым повторениям.
self.auto_update.blockSignals(previous) # Возвращаем прежний режим доставки пользовательских сигналов Qt.
self._update_timer() # Таймер согласуется с программно установленным состоянием.
if not checked: # Неотправленные периодические чтения больше не нужны выключенному источнику.
self.owner._cancel_source(self.owner._pbus_key) # Текущий UART-пакет завершится корректно, последующие шаги этого цикла не отправляются.
def _auto_changed(self, checked): # Пользовательский переключатель управляет также аппаратной остановкой PBUS.
self._update_timer() # Будущие программные повторы выключаются сразу, до ожидания общего порта.
if not checked: # Выключение галочки должно прекратить и обновление интерфейса, и опрос самой параллельной шины.
controller = self.owner.device_address() if callable(self.owner.device_address) else self.owner.device_address # Остановка относится к выбранному соединению контроллера.
active = bool(self._controls.get(controller, 0) & 0x8000) # Подтверждённый бит запуска показывает, что аппаратный PBUS ещё работает.
pending_configuration = any(run["controller"] == controller and any(command.get("bus") == "pbus" and command["type"] in ("add", "remove", "configure", "start") for command in run["jobs"][run["index"]:]) for run in self.owner._runs) # Уже принятая полная настройка может ещё завершить запуск перед очередью остановки.
self.owner._cancel_source(self.owner._pbus_key) # Снимаем ожидающий цикл и ещё не переданные чтения, не повреждая текущий пакет UART.
if active or pending_configuration: # Начальное выключение пустой панели не должно отправлять беспричинный управляющий запрос.
self.owner._start_commands([self.command("stop")], macro=False) # Очередь дождётся текущей логической операции и очистит только бит 15 подтверждённых параметров активной таблицы.
elif self.entries and self.applied: # Включение ранее остановленной настроенной таблицы должно возобновлять также аппаратный опрос.
if not self._controls.get(self.controller, 0) & 0x8000: # После аппаратной остановки одного чтения буфера недостаточно для возобновления обмена.
self.owner._start_commands([self.command("start")], macro=False) # Явное включение пользователя полностью применяет таблицу и выбранный порог перед началом чтения.
else: # Уже работающая применённая таблица не требует перезапуска для включения периодического чтения интерфейса.
self._poll_tick() # Первый обычный цикл читает только назначения результатов 20CD.
elif self.entries: # Неподтверждённая после ошибки таблица по-прежнему требует явного применения.
self.result_label.setText("Нажмите «Обновить», чтобы применить таблицу перед циклическим чтением.") # Выключенный аппаратный опрос не маскируется чтением старых значений памяти.
def _poll_tick(self): # Один источник добавляет не более одного незавершённого чтения таблицы PBUS.
controller = self.owner.device_address() if callable(self.owner.device_address) else self.owner.device_address # Адрес контроллера снимается для нового цикла.
if self.applied and controller != self.controller: # Таблица прежнего контроллера не применяется другому устройству автоматически.
self.timer.stop() # Смена адреса прекращает фоновые чтения, сохраняя известные параметры других контроллеров.
self.applied = False # Новый контроллер требует явного подтверждения своей таблицы.
self._set_cycle_checked(False) # Галочка отражает остановку опроса прежнего устройства.
self.result_label.setText("Адрес контроллера изменён. Нажмите «Обновить», чтобы применить таблицу.") # Сообщаем необходимое действие рядом с результатами.
return # Новый контроллер не получает неподтверждённую конфигурацию от таймера.
if self.applied and self._applied_signature != self._signature(self.entries, self.table_errors): # Изменение порядка, источников или CD не должно пройти незамеченным фоновым чтением.
self.applied = False # Старые подтверждения относятся к другому снимку таблицы.
self.timer.stop() # Периодическое обновление не пытается автоматически запустить новый список.
self._render_entries() # Прежние слова не показываются как результат изменённых строк.
self.result_label.setText("Таблица изменена. Нажмите «Обновить», чтобы применить её.") # Пользователь явно подтверждает аппаратную перенастройку.
if self.entries and self.applied and self.auto_update.isChecked(): # Чтение буфера использует только подтверждённые строки таблицы.
self.owner._start_commands([self.command("poll", read_only=True, errors=self.table_errors)], macro=False, background=True, key=self.owner._pbus_key) # Занятость ставит один цикл в очередь, повторные такты подавляются ключом задания.
def stop_updates(self): # Отключение и команда остановки запрещают будущие запросы данного источника.
self._set_cycle_checked(False) # Отключение соединения не создаёт новую команду остановки уже закрываемому порту.
self.timer.stop() # Больше не назначаем периодические чтения.
self.owner._cancel_source(self.owner._pbus_key) # Отменяем неотправленные шаги циклического чтения без повреждения UART.
self.applied = False # После нового подключения таблица требует явного применения.
self._controls.clear() # После отключения нельзя считать прежние управляющие слова состоянием нового соединения.
self._applied_signature = None # Новое подключение должно подтвердить весь снимок таблицы заново.