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