qt_device_adapter#

Qt adapter that bridges pure-Python device signals into ``pyqtSignal``s.

GUI code (e.g. live plotting, image previews) needs Qt signals so that emissions are delivered on the main thread with QueuedConnection semantics. Devices built on mesofield.devices.base.BaseDataProducer use psygnal and remain Qt-free. This module is the seam: attach an adapter to a device, expose the adapter’s pyqtSignal as an attribute on the device, and the GUI reads that attribute.

Three adapters live here:

  • QtDeviceAdapter — for single-channel serial-style devices. Bridges signals.data into serialDataReceived / serialSpeedUpdated.

  • build_channel_adapter() — for multi-channel devices (e.g. a lick detector plotting both lick events and capacitance). Bridges signals.data into one {channel}Updated pyqtSignal per channel, each fed by a payload extractor.

  • QtImageAdapter — for camera-shaped devices. Provides an image_ready(np.ndarray) pyqtSignal that the MDA viewer subscribes to. The device pushes frames into the adapter via adapter.emit_frame(frame).

class mesofield.gui.qt_device_adapter.QtDeviceAdapter[source]#

Bases: QObject

Bridges device.signals.data into Qt-friendly emissions.

Subscribes to the device’s signals.data and re-emits:

  • serialDataReceived(object) — the raw payload.

  • serialSpeedUpdated(float, float)(time_s, speed) whenever the payload is a dict carrying a "speed" key. time_s defaults to the device timestamp; if absent, the queue timestamp.

__init__(device)[source]#
Parameters:

device (Any)

Return type:

None

class mesofield.gui.qt_device_adapter.DeviceChannelSampler[source]#

Bases: object

Pull-based bridge from device.signals.data to live plots.

A fast serial device (e.g. ~1 kHz licks) emitting one Qt pyqtSignal per sample floods the GUI thread’s event queue: the queue grows faster than it drains regardless of how cheap the slot is, and the window stalls.

This sampler avoids Qt entirely on the hot path. It subscribes to the device’s signals.data (a psygnal, invoked on the device thread) and only appends each channel’s scalar to a bounded per-channel ring buffer – no Qt signal, no cross-thread event. The GUI side pulls a snapshot on its own redraw timer via provider(), so the GUI touches the data at a fixed ~30 Hz no matter how fast the device streams.

channel_sources maps each channel name to the payload-dict key (or a callable(payload) -> value) yielding that channel’s scalar. t is rebased to the first sample so traces start at ~0 regardless of the device’s absolute clock. max_points caps each buffer (and thus memory).

__init__(device, channel_sources, max_points=2000)[source]#
Parameters:
Return type:

None

snapshot(channel)[source]#

Thread-safe copy of one channel’s (times, values, count).

count is the total number of samples ever appended to this channel (monotonic), so the GUI can detect new data even once the ring buffer is saturated and its length stops changing.

Parameters:

channel (str)

Return type:

tuple[list, list, int]

provider(channel)[source]#

Return a zero-arg callable the GUI timer pulls for channel.

Parameters:

channel (str)

Return type:

Callable[[], tuple]

class mesofield.gui.qt_device_adapter.QtImageAdapter[source]#

Bases: QObject

Bridges per-frame ndarray emissions into a Qt image_ready signal.

The MDA gui’s static-viewer branch subscribes via preview = ImagePreview(image_payload=cam.image_ready, ...), which calls image_payload.connect(cb, type=QueuedConnection) – so image_ready MUST be a real pyqtSignal. Devices built on BaseDataProducer are non-Qt; they hold an instance of this adapter and expose its image_ready attribute as their own.

Usage:

class MyCam(BaseDataProducer):
    def __init__(self, cfg=None, **kwargs):
        super().__init__(cfg, **kwargs)
        self._qt_image_adapter = QtImageAdapter()
        self.image_ready = self._qt_image_adapter.image_ready

    def _run_loop(self):
        ...
        self._qt_image_adapter.emit_frame(frame)