"""Последовательная очередь логических операций одного соединения UART.""" # Все панели разделяют владение портом на протяжении многошагового действия. from collections import deque # Две очереди обеспечивают приоритет ручных действий без перестановки их порядка. from PySide6.QtCore import QObject, QTimer, Qt # Точный таймер сохраняет аппаратную паузу без преждевременного чтения памяти контроллера. class ExchangeDelay: # Пауза аппаратного обновления является частью той же операции, а не отдельным обменом UART. def __init__(self, milliseconds): # Время ожидания задаётся явно вызывающим алгоритмом в миллисекундах. self.milliseconds = milliseconds # Очередь сохраняет владение операцией, пока контроллер обновляет буфер результатов. class ExchangeQueue(QObject): # Один экземпляр привязан к протоколу и используется всеми рабочими панелями. def __init__(self, protocol): # Протокол остаётся ответственным за кодирование, CRC и таймаут отдельного пакета. super().__init__() # Таймеры очереди принадлежат объекту Qt с тем же временем жизни. self.protocol, self.active = protocol, None # Активная операция удерживает порт до завершения всех своих шагов. self.manual, self.background = deque(), deque() # Пользовательские действия выбираются раньше следующего фонового задания. self.timer = QTimer(self) # Таймер также позволяет дождаться ранее начатого внешнего обмена. self.timer.setSingleShot(True) # Каждое продолжение назначается однократно, исключая накопление событий. self.timer.timeout.connect(self._pump) # Новая операция выбирается только после освобождения соединения. def contains(self, key): # Периодический источник проверяет наличие своего предыдущего задания. return any(job["key"] == key for job in ([self.active] if self.active else []) + list(self.manual) + list(self.background)) # Проверка охватывает ожидание и выполнение, поэтому пропущенные циклы не копятся. def submit_external(self, start, pending, callback, *, key=None, background=False, immediate=False): # Прежние обработчики SETGUI выполняются под тем же владением соединением. def wait(done): # Адаптер удерживает очередь до завершения внешнего асинхронного обработчика. start() # Параметры внешней операции уже зафиксированы вызывающей панелью. def poll(): # Проверяем завершение через события Qt, не блокируя UART и интерфейс. if pending(): # Прежний обработчик ещё получает ответ или выполняет следующий шаг проверки записи. QTimer.singleShot(10, poll) # Период проверки в миллисекундах не порождает дубликатов команд. else: # Внешний обмен завершился и самостоятельно отобразил результат или ошибку. done(True, None) # Следующая логическая операция теперь может владеть портом. poll() # Синхронное действие без ожидаемого ответа освобождает очередь сразу. def steps(): # Общий исполнитель использует одинаковое завершение обычных и внешних операций. yield wait # Callable получает callback и удерживает очередь до его вызова. return None # Результат внешнего действия уже обработан его существующей панелью. accepted = self.submit(steps, None, callback, key=key, background=background) # Сохраняем приоритет и защиту от повторов для общего соединения. if accepted and immediate: # Прежние обработчики могут сохранить немедленную отправку при свободном соединении. self._pump() # Занятый порт всё равно оставляет действие ожидающим в общей очереди. return accepted # Возвращаем факт принятия задания либо подавления периодического дубликата. def submit(self, factory, controller, callback, *, key=None, background=False): # Фабрика создаёт генератор при начале операции, параметры передаются уже зафиксированными. if key is not None and self.contains(key): # Один периодический источник допускает только одно незавершённое повторение. return False # Не добавляем дубликат в очередь и не создаём отложенного повторения. job = {"factory": factory, "controller": controller, "callback": callback, "key": key, "cancelled": False, "waiting": False, "iterator": None} # Снимок контроллера принадлежит всей логической операции. (self.background if background else self.manual).append(job) # Фоновая операция не вытесняет уже ожидающее нажатие пользователя. self.timer.start(0) # Запуск откладывается до следующего события Qt, оставляя интерфейс отзывчивым. return True # Вызывающий код может отличить постановку задания от подавленного дубликата. def cancel(self, key): # Остановка задания удаляет будущие команды, сохраняя текущий пакет до ответа или таймаута. for pending in (self.manual, self.background): # Удаляем задания из обеих очередей независимо от их приоритета. removed = [job for job in pending if job["key"] == key] # Сохраняем callbacks отменённых заданий для завершения состояния владельца. pending_copy = [job for job in pending if job["key"] != key] # Остальные источники продолжают ожидать в прежнем порядке. pending.clear() # Пересобираем только контейнер очереди, не изменяя задания других панелей. pending.extend(pending_copy) # Возвращаем ожидающие операции, которые не принадлежат остановленному источнику. for job in removed: # Каждый владелец получает однозначное уведомление об отмене своего ожидания. job["callback"](False, "Остановлено") # Ни один пакет снятой операции ещё не был отправлен. if self.active and self.active["key"] == key: # Выполняющееся задание прекращаем после уже отправленного пакета. self.active["cancelled"] = True # Следующий шаг генератора не будет отправлен, даже при успешном ответе. if not self.active["waiting"]: # Между пакетами отмена может завершиться немедленно. self._complete(False, "Остановлено") # Освобождаем порт без искусственного транспортного таймаута. def clear(self): # Отключение соединения удаляет все задания и будущие продолжения. keys = {job["key"] for job in list(self.manual) + list(self.background) + ([self.active] if self.active else [])} # Снимок ключей не меняется при вызове callbacks отмены. for key in keys: # Применяем одинаковую отмену ко всем источникам данного соединения. self.cancel(key) # Активный пакет корректно завершится либо будет отменён транспортом при закрытии порта. def _transport_busy(self): # Учитываем прежние операции встроенного окна, начатые до использования очереди. host = getattr(self.protocol, "host", None) # В SETGUI протокол использует общий QSerialPort родительской вкладки. return bool(getattr(self.protocol, "_callback", None) or getattr(self.protocol, "callback", None) or getattr(self.protocol, "_loop", None) or (host and host._pending_address is not None)) # Занятый внешний обмен нельзя замещать новым callback. def _pump(self): # Выбираем операцию только между целостными чтениями или записями регистра. if self.active: # Пока генератор удерживает соединение, очередь не начинает другие операции. return # Продолжение активной операции приходит через её транспортный callback. if not self.manual and not self.background: # Пустая очередь не создаёт периодических событий сама по себе. return # Отсутствие заданий позволяет соединению оставаться полностью свободным. if self._transport_busy(): # Ждём завершения внешней операции вместо отказа пользователю. self.timer.start(10) # Короткое ожидание в миллисекундах не блокирует обработку UART и интерфейса. return # Параметры ожидающих нажатий при этом остаются неизменными. self.active = (self.manual if self.manual else self.background).popleft() # FIFO ручных действий имеет приоритет над следующим фоновым циклом. self._advance(None) # Первый шаг генератора отправляется только после получения владения соединением. def _advance(self, reply): # Генератор выдаёт следующий запрос только после завершения предыдущего. job = self.active # Сохраняем владельца, чтобы поздний ответ не продолжил другую операцию. if job is None: # Очередь могла быть очищена отключением соединения. return # Без владельца никакие команды не отправляются. delay = job.pop("delay_timer", None) # Завершившийся таймер аппаратного ожидания больше не нужен текущему генератору. if delay is not None: # Обычные UART-продолжения не создают отдельного таймера аппаратной паузы. delay.stop() # Завершённое или отменённое ожидание не назначает повторного события. delay.deleteLater() # Освобождаем QObject после обработки текущего события Qt. if job["cancelled"]: # Остановка отменяет все шаги, которые ещё не переданы контроллеру. self._complete(False, "Остановлено") # Текущий пакет к этому моменту уже получил ответ или таймаут. return # Не возобновляем остановленный генератор. try: # Ошибки параметров и результата завершают логическую операцию без повреждения очереди. if job["iterator"] is None: # Фабрика использует актуальную таблицу PBUS при зафиксированном действии пользователя. job["iterator"] = job["factory"]() # Генератор создаётся один раз на целую операцию. request = next(job["iterator"]) # Первое чтение не получает фиктивного ответа предыдущей операции. else: # Последующие шаги получают реальное подтверждение или считанное слово. request = job["iterator"].send(reply) # Запись подготовки и чтение результата не могут перемешаться с другим заданием. except StopIteration as completed: # Генератор закончил все шаги одной логической операции. self._complete(True, completed.value) # Возвращаем итог владельцу и освобождаем соединение. return # Завершённый генератор больше не используется. except (ValueError, RuntimeError, TypeError, KeyError) as error: # Неверные параметры или подтверждения не должны разрывать цикл событий Qt. self._complete(False, str(error)) # Сохраняем действительную причину ошибки в панели инициатора. return # Следующая ожидающая операция сможет продолжить обмен. if isinstance(request, ExchangeDelay): # После запуска PBUS аппаратуре даётся время обновить слова памяти до первого чтения. delay = QTimer(self) # Отдельный таймер принадлежит данной аппаратной паузе, а не планировщику выбора операций. delay.setSingleShot(True) # Одна пауза заканчивается одним продолжением генератора. delay.setTimerType(Qt.PreciseTimer) # Штатный грубый таймер может сработать раньше, что недопустимо перед чтением нового буфера PBUS. delay.timeout.connect(lambda: self._advance(None) if self.active is job else None) # Продолжение остаётся связано с владельцем всей перенастройки. job["delay_timer"] = delay # Остановка задания сможет немедленно отменить его ещё не завершённую паузу. delay.start(request.milliseconds) # Ожидаем заданное время в миллисекундах после подтверждения запуска контроллером. return # UART-пакет не отправляется; остановка может немедленно отменить эту паузу и все следующие команды. job["waiting"] = True # До вызова транспорта отмечаем ожидание, включая синхронный отказ отключённого порта. try: # Защищаем очередь также от исключения драйвера при отправке. if callable(request): # Прежняя операция SETGUI или RAW выполняется только после получения общего владения UART. request(lambda ok, result: self._reply(job, ok, result)) # Внешний обработчик завершает ожидание через тот же callback очереди. return # Не трактуем внешнее действие как адрес памяти PEEK/POKE. address, value = request # Формат PEEK/POKE остаётся прежним для обеих шин. self.protocol.request_async(address, value, job["controller"], lambda ok, result: self._reply(job, ok, result)) # Существующий протокол проверяет ответ и задаёт таймаут пакета. except (ValueError, RuntimeError, OSError) as error: # Ошибка порта завершает текущую операцию и освобождает очередь. self._reply(job, False, str(error)) # Используем тот же путь, что для асинхронного отказа обмена. def _reply(self, job, success, result): # Ответ всегда относится к владельцу пакета, а не к последнему нажатию. if self.active is not job: # Поздний callback закрытого соединения не меняет новое задание. return # Исключаем продвижение чужого генератора устаревшим ответом. job["waiting"] = False # Порт освободился после полного ответа либо обработанного таймаута. if job["cancelled"] or not success: # Остановка и реальная ошибка запрещают отправку следующих пакетов. self._complete(False, "Остановлено" if job["cancelled"] else result) # Отмена не маскируется как успешное чтение. return # Остальные команды остановленного задания не попадут в UART. QTimer.singleShot(0, lambda: self._advance(result) if self.active is job else None) # Даём обработать остановку между пакетами, удерживая владение операцией. def _complete(self, success, result): # Завершение освобождает соединение до уведомления владельца операции. job, self.active = self.active, None # Callback может поставить следующую команду макроса в общую очередь. delay = job.pop("delay_timer", None) # Пользовательская остановка может завершить операцию прямо во время аппаратной паузы. if delay is not None: # Завершение обычного пакета не содержит такого таймера. delay.stop() # Отменённая пауза больше не продолжит чтение не применённой таблицы. delay.deleteLater() # Освобождаем временный QObject вместе с завершённым заданием. job["callback"](success, result) # Результат отображается только инициатором данной операции. self.timer.start(0) # Между командами макроса выбираем ожидающее ручное действие раньше фонового продолжения. def exchange_queue(protocol): # Общая очередь создаётся лениво для существующих и тестовых протоколов. if not hasattr(protocol, "operation_queue"): # Панели одного соединения должны получать один и тот же экземпляр. protocol.operation_queue = ExchangeQueue(protocol) # Владение протоколом сохраняется без изменения формата пакетов. return protocol.operation_queue # Возвращаем единый планировщик ручного обмена и периодических заданий.