Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
347 lines
59 KiB
Python
347 lines
59 KiB
Python
"""Таблица наблюдаемых регистров 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 # Новое подключение должно подтвердить весь снимок таблицы заново.
|