Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
82 lines
8.5 KiB
Python
82 lines
8.5 KiB
Python
"""Reusable background workers for dense plots and viewport measurements."""
|
||
from collections import deque # Кольцевые очереди живого графика требуют единого снимка между проходами амплитудной статистики и частоты.
|
||
try:
|
||
from PySide6.QtCore import QObject, QRunnable, Signal
|
||
except ImportError:
|
||
from PySide2.QtCore import QObject, QRunnable, Signal
|
||
from set_devices.plot_decimation import pixel_envelope, render_envelope, viewport_samples # Рабочее задание поддерживает точный прежний формат и компактную экранную огибающую для аналогового графика.
|
||
from set_devices.oscilloscope import measurements
|
||
from set_devices.background_work import WorkBudget # Даже фоновый Python-расчёт должен регулярно отдавать GIL многочисленным обработчикам интерфейса Qt.
|
||
|
||
class EnvelopeSignals(QObject):
|
||
ready = Signal(str, object, object)
|
||
|
||
|
||
class EnvelopeJob(QRunnable):
|
||
def __init__(self, key, geometry, timestamps, values, left, right, width, signals, compact=False): # Необязательный режим оставляет прежний контракт внешних пользователей, а аналоговый график включает два экстремума на столбец.
|
||
super().__init__()
|
||
self.args = (timestamps, values, left, right, width)
|
||
self.key, self.geometry, self.signals = key, geometry, signals
|
||
self.select = render_envelope if compact else pixel_envelope # Выбор алгоритма выполняется один раз при создании задания, не внутри многомиллионного обхода.
|
||
|
||
def run(self):
|
||
try:
|
||
points = tuple(self.select(*self.args)) # Фоновый результат сохраняет точные пики и разрывы в согласованном с рисователем экранном бюджете.
|
||
except Exception:
|
||
points = None
|
||
try:
|
||
self.signals.ready.emit(self.key, self.geometry, points)
|
||
except RuntimeError:
|
||
pass # The plot may have been closed while the worker was running.
|
||
|
||
|
||
class NavigationSignals(QObject):
|
||
ready = Signal(int, object, object)
|
||
|
||
|
||
class _NavigationSamples: # Повторяемая выборка окна позволяет измерять исходные ряды без копии миллионов пар координат.
|
||
def __init__(self, times, values, bounds, check_cancelled): # Храним источники задания; вызывающий код создаёт снимок изменяемых очередей перед численными проходами.
|
||
self.times, self.values, self.bounds, self.check_cancelled = times, values, bounds, check_cancelled # Ссылки сохраняют исходную точность и порядок без экранного сокращения.
|
||
self.count = 0 # Число отсчётов определяется при первом полном проходе и включает нечисловые амплитуды, как прежний список.
|
||
|
||
def __iter__(self): # Статистики и частота независимо обходят то же окно, не удерживая промежуточные пары в памяти.
|
||
count = 0 # Каждый проход считает только точки внутри включительных временных границ.
|
||
for index, (x, y) in enumerate(viewport_samples(self.times, self.values, self.bounds.left, self.bounds.right)): # Индексируемый источник ищет окно двоичным поиском; потоковый сохраняет прежний порядок.
|
||
if index % 1024 == 0: # Проверяем отмену через 1024 просмотренных отсчёта, даже когда неупорядоченный ряд лежит вне окна.
|
||
self.check_cancelled() # Не продолжаем чтение старого диапазона после нового жеста навигации.
|
||
if self.bounds.left <= x <= self.bounds.right: # Включаем обе границы и все дубликаты временных меток, как исходная реализация.
|
||
count += 1 # Счётчик соответствует прежнему len(points), независимо от конечности амплитуды.
|
||
yield x, y # Пара существует только на время текущего численного прохода и не копируется в большой список.
|
||
self.count = count # Публикуем количество только после полного прохода, чтобы отменённая выборка не выдавала частичный результат.
|
||
|
||
|
||
class NavigationJob(QRunnable):
|
||
def __init__(self, revision, series, bounds, emitter, cancelled=None): # Необязательное событие отмены сохраняет совместимость общих графиков и останавливает устаревшие измерения.
|
||
super().__init__()
|
||
self.revision, self.series, self.bounds, self.emitter = revision, series, bounds, emitter
|
||
self.cancelled = cancelled # Событие принадлежит одному заданию и не сбрасывается при запуске расчёта нового окна.
|
||
self.work_budget = WorkBudget(cancelled) # Короткие добровольные паузы предотвращают многосекундное ожидание GIL главным потоком.
|
||
|
||
def _check_cancelled(self): # Проверяем отмену на границах этапов и небольших блоков исходных отсчётов.
|
||
self.work_budget() # Одновременно прекращаем устаревшие измерения и регулярно освобождаем GIL для рисования и событий мыши.
|
||
|
||
def run(self):
|
||
stats = {}
|
||
try:
|
||
for key, times, values in self.series:
|
||
self._check_cancelled() # Не начинаем обход следующего канала после возобновления перемещения или изменения масштаба.
|
||
samples = _NavigationSamples(times, values, self.bounds, self._check_cancelled) # Повторяемый вид файлового окна устраняет копирование миллионов пар и сохраняет отмену чтения источника.
|
||
ordered = getattr(times, "ordered", isinstance(times, (list, tuple, range))) # Применяем тот же контракт упорядоченности, что и viewport_samples, включая смещённые файловые координаты.
|
||
points = tuple(samples) if isinstance(times, deque) or isinstance(values, deque) or not ordered else samples # Только живые очереди и неупорядоченный путь получают единый снимок для согласованных амплитуд и частоты.
|
||
result = measurements(points, self._check_cancelled) # Передаём отмену внутрь всех численных этапов, чтобы новый жест не ждал окончания полного среднего или СКЗ.
|
||
stats[key] = result, samples.count # Количество относится к завершённому проходу того же окна либо к первоначальному снимку живого ряда.
|
||
self._check_cancelled() # При отмене во время вычислений не продолжаем остальные каналы и не публикуем устаревшие значения.
|
||
self.emitter.ready.emit(self.revision, stats, self.bounds)
|
||
except Exception:
|
||
try:
|
||
self.emitter.ready.emit(self.revision, {}, self.bounds)
|
||
except RuntimeError:
|
||
pass
|
||
|
||
|