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

145 lines
22 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.
"""Последовательная очередь логических операций одного соединения 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 # Возвращаем единый планировщик ручного обмена и периодических заданий.