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

82 lines
8.5 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.
"""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