"""Таблица наблюдаемых регистров 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 # Новое подключение должно подтвердить весь снимок таблицы заново.