diff options
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/control_panel.py | 70 |
1 files changed, 62 insertions, 8 deletions
diff --git a/ui/control_panel.py b/ui/control_panel.py index f0e18ef..dafeabf 100644 --- a/ui/control_panel.py +++ b/ui/control_panel.py @@ -21,6 +21,7 @@ from __future__ import annotations from typing import Optional import math as _math +import threading from typing import Any from PyQt6.QtWidgets import ( @@ -36,6 +37,57 @@ from devices.device_registry import DeviceRegistry from devices.base_device import DeviceStatus +# ── Background write dispatch ─────────────────────────────────────────────── +# +# write_channel() is synchronous and, for some protocols (e.g. CML — see +# api_layers/protocols/cml.py), does multiple blocking serial round-trips +# per call — each one can cost up to the serial port's full read timeout if +# the device doesn't reply to that particular command, so a single write +# can take seconds. Control widgets call this directly from Qt signal +# handlers (slider valueChanged fires on every tick of a drag), so on the +# GUI thread that means each tick blocks the whole app. +# +# Writes are coalesced per (device, channel) — not just queued — keyed in +# this dict rather than a plain FIFO queue: a slider drag fires far more +# ticks than a multi-second write can keep up with, and a strict FIFO would +# just work through an ever-growing backlog of stale values (observed: 30+ +# queued while only ~4 had drained), so the motor keeps "moving" to +# seconds-old positions long after the user stopped touching the slider — +# looks exactly like a freeze. Re-writing the same key updates its pending +# value in place without changing its position, so the worker always sends +# the latest value once it's free, never a backlog of old ones. + +_write_lock = threading.Lock() +_write_cond = threading.Condition(_write_lock) +_pending: "dict" = {} # (device_id, channel_id) -> (dev, channel_id, value) + + +def _enqueue_write(dev, channel_id, value): + with _write_cond: + _pending[(dev.info.device_id, channel_id)] = (dev, channel_id, value) + _write_cond.notify() + + +def _write_worker(): + while True: + with _write_cond: + while not _pending: + _write_cond.wait() + key = next(iter(_pending)) + dev, channel_id, value = _pending.pop(key) + try: + ok = dev.write_channel(channel_id, value) + if not ok: + print(f"[Control] write_channel({channel_id}, {value}) " + f"returned False on {dev.info.device_id} — " + f"check device type and channel ID") + except Exception as e: + print(f"[Control] write_channel({channel_id}, {value}) raised: {e}") + + +threading.Thread(target=_write_worker, daemon=True, name="ControlWrite").start() + + # ══════════════════════════════════════════════════════════════════════════════ # Base widget # ══════════════════════════════════════════════════════════════════════════════ @@ -176,8 +228,13 @@ class ControlWidget(QFrame): DigitalIODevice → write_channel() → ArduinoLayer.digital_write() → W:Dxx:val AnalogInputDevice (arduino backend) → write_channel() → ArduinoLayer.digital_write() Any device with no write support → logs a warning, emits signal only + + The actual write is dispatched to a background worker (_enqueue_write) + rather than called directly here — some protocols (CML) do multiple + blocking serial round-trips per write, and this runs on the GUI + thread (slider valueChanged fires on every tick of a drag), so a + direct call here would freeze the whole app on every tick. """ - written = False if self.registry and self.device_id and self.channel_id: dev = self.registry.get_instance(self.device_id) if dev: @@ -189,13 +246,10 @@ class ControlWidget(QFrame): print(f"[Control] write_channel({self.channel_id}, {value}) skipped on " f"{self.device_id} — device status is {dev.status.value}, not connected") else: - ok = dev.write_channel(self.channel_id, value) - if ok: - written = True - else: - print(f"[Control] write_channel({self.channel_id}, {value}) " - f"returned False on {self.device_id} — " - f"check device type and channel ID") + _enqueue_write(dev, self.channel_id, value) + else: + print(f"[Control] write_channel({self.channel_id}, {value}) skipped — " + f"device '{self.device_id}' not found in registry") self.value_changed.emit(self.channel_id, value) if self._on_action_fn is not None: |
