pymmcore-plus 0.15.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. pymmcore_plus/__init__.py +25 -0
  2. pymmcore_plus/_ipy_completion.py +363 -0
  3. pymmcore_plus/core/_constants.py +4 -0
  4. pymmcore_plus/core/_mmcore_plus.py +26 -16
  5. pymmcore_plus/core/_sequencing.py +1 -1
  6. pymmcore_plus/core/events/_deprecated.py +67 -0
  7. pymmcore_plus/core/events/_protocol.py +15 -5
  8. pymmcore_plus/core/events/_psygnal.py +33 -4
  9. pymmcore_plus/core/events/_qsignals.py +34 -6
  10. pymmcore_plus/experimental/unicore/__init__.py +7 -3
  11. pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
  12. pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +23 -27
  13. pymmcore_plus/experimental/unicore/core/_unicore.py +90 -23
  14. pymmcore_plus/experimental/unicore/devices/_camera.py +10 -5
  15. pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
  16. pymmcore_plus/experimental/unicore/devices/_properties.py +1 -1
  17. pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
  18. pymmcore_plus/experimental/unicore/devices/_slm.py +1 -1
  19. pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
  20. pymmcore_plus/experimental/unicore/devices/_state.py +1 -1
  21. {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/METADATA +2 -2
  22. {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/RECORD +26 -22
  23. /pymmcore_plus/experimental/unicore/devices/{_device.py → _device_base.py} +0 -0
  24. {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/WHEEL +0 -0
  25. {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/entry_points.txt +0 -0
  26. {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,10 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from psygnal import Signal, SignalGroup, SignalInstance
2
4
 
3
5
  from pymmcore_plus.mda import MDAEngine
4
6
 
7
+ from ._deprecated import DeprecatedSignalProxy
5
8
  from ._prop_event_mixin import _DevicePropertyEventMixin
6
9
 
7
10
 
@@ -22,14 +25,19 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
22
25
  exposureChanged = Signal(str, float)
23
26
  SLMExposureChanged = Signal(str, float)
24
27
 
28
+ # https://github.com/micro-manager/mmCoreAndDevices/pull/659
29
+ imageSnapped = Signal(str) # on snapImage()
30
+ # when (Continuous)SequenceAcquisition is stopped
31
+ sequenceAcquisitionStopped = Signal(str)
32
+ if TYPE_CHECKING: # see deprecated impl below
33
+ sequenceAcquisitionStarted = Signal(str)
34
+
25
35
  # added for CMMCorePlus
26
- imageSnapped = Signal() # whenever snapImage is called
27
36
  mdaEngineRegistered = Signal(MDAEngine, MDAEngine)
28
37
  continuousSequenceAcquisitionStarting = Signal()
29
38
  continuousSequenceAcquisitionStarted = Signal()
30
- sequenceAcquisitionStarting = Signal(str, int, float, bool)
31
- sequenceAcquisitionStarted = Signal(str, int, float, bool)
32
- sequenceAcquisitionStopped = Signal(str)
39
+ if TYPE_CHECKING:
40
+ sequenceAcquisitionStarting = Signal(str)
33
41
  autoShutterSet = Signal(bool)
34
42
  configGroupDeleted = Signal(str)
35
43
  configDeleted = Signal(str, str)
@@ -44,3 +52,24 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
44
52
  @property
45
53
  def sLMExposureChanged(self) -> SignalInstance:
46
54
  return self.SLMExposureChanged
55
+
56
+ if not TYPE_CHECKING:
57
+ _sequenceAcquisitionStarting = Signal(str)
58
+ _sequenceAcquisitionStarted = Signal(str)
59
+
60
+ # Deprecated signal wrappers for backwards compatibility
61
+ @property
62
+ def sequenceAcquisitionStarting(self) -> SignalInstance:
63
+ return DeprecatedSignalProxy(
64
+ self._sequenceAcquisitionStarting,
65
+ current_n_args=1,
66
+ deprecated_posargs=(-1, 0, False),
67
+ )
68
+
69
+ @property
70
+ def sequenceAcquisitionStarted(self) -> SignalInstance:
71
+ return DeprecatedSignalProxy(
72
+ self._sequenceAcquisitionStarted,
73
+ current_n_args=1,
74
+ deprecated_posargs=(-1, 0, False),
75
+ )
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Optional
2
2
 
3
3
  from qtpy.QtCore import QObject, Signal
4
4
 
5
+ from ._deprecated import DeprecatedSignalProxy
5
6
  from ._prop_event_mixin import _PropertySignal
6
7
 
7
8
  if TYPE_CHECKING:
@@ -25,17 +26,23 @@ class QCoreSignaler(QObject):
25
26
  SLMExposureChanged = Signal(str, float)
26
27
  sLMExposureChanged = SLMExposureChanged # alias
27
28
 
29
+ # https://github.com/micro-manager/mmCoreAndDevices/pull/659
30
+ imageSnapped = Signal(str) # on snapImage()
31
+ # when (Continuous)SequenceAcquisition is stopped
32
+ sequenceAcquisitionStopped = Signal(str)
33
+ if TYPE_CHECKING: # see deprecated impl below
34
+ sequenceAcquisitionStarted = Signal(str)
35
+
28
36
  # added for CMMCorePlus
29
- imageSnapped = Signal() # on snapImage()
30
37
  mdaEngineRegistered = Signal(object, object) # new engine, old engine
31
38
  # when continuousSequenceAcquisition is started
32
39
  continuousSequenceAcquisitionStarting = Signal()
33
40
  continuousSequenceAcquisitionStarted = Signal()
34
- # when SequenceAcquisition is started
35
- sequenceAcquisitionStarting = Signal(str, int, float, bool)
36
- sequenceAcquisitionStarted = Signal(str, int, float, bool)
37
- # when (Continuous)SequenceAcquisition is stopped
38
- sequenceAcquisitionStopped = Signal(str)
41
+
42
+ if TYPE_CHECKING:
43
+ # when SequenceAcquisition is started
44
+ sequenceAcquisitionStarting = Signal(str)
45
+
39
46
  autoShutterSet = Signal(bool)
40
47
  configGroupDeleted = Signal(str)
41
48
  configDeleted = Signal(str, str)
@@ -77,3 +84,24 @@ class QCoreSignaler(QObject):
77
84
  """
78
85
  # type ignored: can't use _DevicePropertyEventMixin due to metaclass conflict
79
86
  return _PropertySignal(self, device, property)
87
+
88
+ if not TYPE_CHECKING:
89
+ _sequenceAcquisitionStarting = Signal(str)
90
+ _sequenceAcquisitionStarted = Signal(str)
91
+
92
+ # Deprecated signal wrappers for backwards compatibility
93
+ @property
94
+ def sequenceAcquisitionStarting(self):
95
+ return DeprecatedSignalProxy(
96
+ self._sequenceAcquisitionStarting,
97
+ current_n_args=1,
98
+ deprecated_posargs=(-1, 0, False),
99
+ )
100
+
101
+ @property
102
+ def sequenceAcquisitionStarted(self):
103
+ return DeprecatedSignalProxy(
104
+ self._sequenceAcquisitionStarted,
105
+ current_n_args=1,
106
+ deprecated_posargs=(-1, 0, False),
107
+ )
@@ -1,16 +1,20 @@
1
1
  from .core._unicore import UniMMCore
2
- from .devices._camera import Camera
3
- from .devices._device import Device
2
+ from .devices._camera import CameraDevice
3
+ from .devices._device_base import Device
4
+ from .devices._generic_device import GenericDevice
4
5
  from .devices._properties import PropertyInfo, pymm_property
6
+ from .devices._shutter import ShutterDevice
5
7
  from .devices._slm import SLMDevice
6
8
  from .devices._stage import StageDevice, XYStageDevice, XYStepperStageDevice
7
9
  from .devices._state import StateDevice
8
10
 
9
11
  __all__ = [
10
- "Camera",
12
+ "CameraDevice",
11
13
  "Device",
14
+ "GenericDevice",
12
15
  "PropertyInfo",
13
16
  "SLMDevice",
17
+ "ShutterDevice",
14
18
  "StageDevice",
15
19
  "StateDevice",
16
20
  "UniMMCore",
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, TypeVar, cast
7
7
 
8
8
  from pymmcore_plus.core._constants import DeviceInitializationState, DeviceType
9
9
 
10
- from .devices._device import Device
10
+ from .devices._device_base import Device
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from collections.abc import Iterator
@@ -22,6 +22,11 @@ class BufferSlot(NamedTuple):
22
22
  nbytes_total: int # full span in the pool (data + padding)
23
23
 
24
24
 
25
+ # TODO: version that doesn't use contiguous memory,
26
+ # but rather uses a deuqe of numpy arrays.
27
+ class SequenceStack: ...
28
+
29
+
25
30
  class SequenceBuffer:
26
31
  """A lock-protected circular buffer backed by a single numpy byte array.
27
32
 
@@ -55,7 +60,7 @@ class SequenceBuffer:
55
60
  self._overflow_occurred: bool = False
56
61
 
57
62
  self._lock = threading.Lock() # not re-entrant, but slightly faster than RLock
58
- self._pending_slot: tuple[NDArray, int] | None = None # only 1 outstanding slot
63
+ self._pending_slot: deque[tuple[NDArray, int]] = deque()
59
64
 
60
65
  # ---------------------------------------------------------------------
61
66
  # Producer API - acquire a slot, fill it, finalize it
@@ -82,10 +87,6 @@ class SequenceBuffer:
82
87
  # --- reserve space -------------------------------------------------
83
88
 
84
89
  with self._lock:
85
- if self._pending_slot is not None:
86
- msg = "Cannot acquire a new slot before finalizing the pending one."
87
- raise RuntimeError(msg)
88
-
89
90
  # Calculate padding needed to align start address to dtype boundary
90
91
  align_pad = (-self._head) % dtype_.itemsize
91
92
  needed = nbytes_data + align_pad
@@ -112,7 +113,7 @@ class SequenceBuffer:
112
113
  arr: NDArray[Any] = np.ndarray(
113
114
  shape, dtype_, buffer=self._pool, offset=start
114
115
  )
115
- self._pending_slot = (arr, needed)
116
+ self._pending_slot.append((arr, needed))
116
117
  return arr
117
118
 
118
119
  def finalize_slot(self, metadata: Mapping[str, Any] | None = None) -> None:
@@ -123,12 +124,11 @@ class SequenceBuffer:
123
124
  slot's metadata dictionary.
124
125
  """
125
126
  with self._lock:
126
- if (slot := self._pending_slot) is None:
127
+ if not self._pending_slot:
127
128
  msg = "No pending slot to finalize"
128
129
  raise RuntimeError(msg)
129
130
 
130
- self._pending_slot = None
131
- arr, nbytes_total = slot
131
+ arr, nbytes_total = self._pending_slot.popleft()
132
132
  self._slots.append(BufferSlot(arr, metadata, nbytes_total))
133
133
 
134
134
  # Convenience: copy-in one-shot insert ------------------------------
@@ -153,7 +153,7 @@ class SequenceBuffer:
153
153
  # ------------------------------------------------------------------
154
154
 
155
155
  def pop_next(
156
- self, *, copy: bool = False
156
+ self, *, out: np.ndarray | None = None
157
157
  ) -> tuple[NDArray[Any], Mapping[str, Any]] | None:
158
158
  """Remove and return the oldest frame.
159
159
 
@@ -165,27 +165,25 @@ class SequenceBuffer:
165
165
  if not self._slots:
166
166
  return None
167
167
  slot = self._slots.popleft()
168
- self._evict_slot(slot)
169
168
 
170
- if copy:
171
- arr = slot.array.copy()
169
+ if out is not None:
170
+ out[:] = slot.array
171
+ arr = out
172
172
  else:
173
- # even though we're popping, we still return a read-only view since
174
- # the caller should be able to modify our _pool directly.
175
- arr = slot.array.view()
176
- arr.flags.writeable = False
173
+ arr = slot.array.copy()
174
+ self._evict_slot(slot)
177
175
 
178
176
  # return actual metadata, we're done with it.
179
177
  return arr, (slot.metadata or {})
180
178
 
181
179
  def peek_last(
182
- self, *, copy: bool = False
180
+ self, *, out: np.ndarray | None = None
183
181
  ) -> tuple[NDArray[Any], Mapping[str, Any]] | None:
184
182
  """Return the newest frame without removing it."""
185
- return self.peek_nth_from_last(0, copy=copy)
183
+ return self.peek_nth_from_last(0, out=out)
186
184
 
187
185
  def peek_nth_from_last(
188
- self, n: int, *, copy: bool = False
186
+ self, n: int, *, out: np.ndarray | None = None
189
187
  ) -> tuple[NDArray[Any], dict[str, Any]] | None:
190
188
  """Return the n-th most recent frame without removing it.
191
189
 
@@ -195,13 +193,11 @@ class SequenceBuffer:
195
193
  if n < 0 or n >= len(self._slots):
196
194
  return None
197
195
  slot = self._slots[-(n + 1)]
198
-
199
- if copy:
200
- arr = slot.array.copy()
196
+ if out is not None:
197
+ out[:] = slot.array
198
+ arr = out
201
199
  else:
202
- # Return a read-only view to prevent modification of data in the buffer
203
- arr = slot.array.view()
204
- arr.flags.writeable = False
200
+ arr = slot.array.copy()
205
201
 
206
202
  # Return a copy of the metadata to avoid external modification
207
203
  return arr, (dict(slot.metadata) if slot.metadata else {})
@@ -213,7 +209,7 @@ class SequenceBuffer:
213
209
  def clear(self) -> None:
214
210
  with self._lock:
215
211
  self._slots.clear()
216
- self._pending_slot = None
212
+ self._pending_slot.clear()
217
213
  self._head = self._tail = self._bytes_in_use = 0
218
214
  self._overflow_occurred = False
219
215
 
@@ -25,8 +25,9 @@ from pymmcore_plus.core import Keyword as KW
25
25
  from pymmcore_plus.core._constants import PixelType
26
26
  from pymmcore_plus.experimental.unicore._device_manager import PyDeviceManager
27
27
  from pymmcore_plus.experimental.unicore._proxy import create_core_proxy
28
- from pymmcore_plus.experimental.unicore.devices._camera import Camera
29
- from pymmcore_plus.experimental.unicore.devices._device import Device
28
+ from pymmcore_plus.experimental.unicore.devices._camera import CameraDevice
29
+ from pymmcore_plus.experimental.unicore.devices._device_base import Device
30
+ from pymmcore_plus.experimental.unicore.devices._shutter import ShutterDevice
30
31
  from pymmcore_plus.experimental.unicore.devices._slm import SLMDevice
31
32
  from pymmcore_plus.experimental.unicore.devices._stage import XYStageDevice, _BaseStage
32
33
  from pymmcore_plus.experimental.unicore.devices._state import StateDevice
@@ -678,11 +679,11 @@ class UniMMCore(CMMCorePlus):
678
679
 
679
680
  # --------------------------------------------------------------------- utils
680
681
 
681
- def _py_camera(self, cameraLabel: str | None = None) -> Camera | None:
682
+ def _py_camera(self, cameraLabel: str | None = None) -> CameraDevice | None:
682
683
  """Return the *Python* Camera for ``label`` (or current), else ``None``."""
683
684
  label = cameraLabel or self.getCameraDevice()
684
685
  if label in self._pydevices:
685
- return self._pydevices.get_device_of_type(label, Camera)
686
+ return self._pydevices.get_device_of_type(label, CameraDevice)
686
687
  return None
687
688
 
688
689
  def setCameraDevice(self, cameraLabel: DeviceLabel | str) -> None:
@@ -751,7 +752,7 @@ class UniMMCore(CMMCorePlus):
751
752
  # ---------------------------------------------------------------- sequence common
752
753
 
753
754
  def _start_sequence(
754
- self, cam: Camera, n_images: int | None, stop_on_overflow: bool
755
+ self, cam: CameraDevice, n_images: int | None, stop_on_overflow: bool
755
756
  ) -> None:
756
757
  """Initialise _seq state and call cam.start_sequence."""
757
758
  shape, dtype = cam.shape(), np.dtype(cam.dtype())
@@ -786,13 +787,15 @@ class UniMMCore(CMMCorePlus):
786
787
  img_number = next(counter)
787
788
  elapsed_ms = (perf_counter_ns() - start_time) / 1e6
788
789
  received = datetime.now().isoformat(sep=" ")
789
- meta_update = {
790
- **cam_meta,
791
- KW.Metadata_TimeInCore: received,
792
- KW.Metadata_ImageNumber: str(img_number),
793
- KW.Elapsed_Time_ms: f"{elapsed_ms:.2f}",
794
- }
795
- self._seq_buffer.finalize_slot({**base_meta, **meta_update})
790
+ self._seq_buffer.finalize_slot(
791
+ {
792
+ **base_meta,
793
+ **cam_meta,
794
+ KW.Metadata_TimeInCore: received,
795
+ KW.Metadata_ImageNumber: str(img_number),
796
+ KW.Elapsed_Time_ms: f"{elapsed_ms:.2f}",
797
+ }
798
+ )
796
799
 
797
800
  # Auto-stop when we've acquired the requested number of images
798
801
  if n_images is not None and (img_number + 1) >= n_images:
@@ -808,8 +811,7 @@ class UniMMCore(CMMCorePlus):
808
811
 
809
812
  self._acquisition_thread = AcquisitionThread(
810
813
  image_generator=cam.start_sequence(
811
- n_images or 2**63 - 1,
812
- get_buffer_with_overflow_handling,
814
+ n_images, get_buffer_with_overflow_handling
813
815
  ),
814
816
  finalize=finalize_with_metadata,
815
817
  label=camera_label,
@@ -823,6 +825,7 @@ class UniMMCore(CMMCorePlus):
823
825
 
824
826
  # ------------------------------------------------- startSequenceAcquisition
825
827
 
828
+ # startSequenceAcquisition
826
829
  def _do_start_sequence_acquisition(
827
830
  self, cameraLabel: str, numImages: int, intervalMs: float, stopOnOverflow: bool
828
831
  ) -> None:
@@ -835,6 +838,7 @@ class UniMMCore(CMMCorePlus):
835
838
 
836
839
  # ------------------------------------------------- continuous acquisition
837
840
 
841
+ # startContinuousSequenceAcquisition
838
842
  def _do_start_continuous_sequence_acquisition(self, intervalMs: float = 0) -> None:
839
843
  if (cam := self._py_camera()) is None: # pragma: no cover
840
844
  return pymmcore.CMMCore.startContinuousSequenceAcquisition(self, intervalMs)
@@ -879,27 +883,41 @@ class UniMMCore(CMMCorePlus):
879
883
 
880
884
  # ---------------------------------------------------- getImages
881
885
 
882
- def getLastImage(self) -> np.ndarray:
886
+ def getLastImage(self, *, out: np.ndarray | None = None) -> np.ndarray:
883
887
  if self._py_camera() is None:
884
888
  return super().getLastImage()
885
- if not (self._seq_buffer) or (result := self._seq_buffer.peek_last()) is None:
889
+ if (
890
+ not (self._seq_buffer)
891
+ or (result := self._seq_buffer.peek_last(out=out)) is None
892
+ ):
886
893
  raise IndexError("Circular buffer is empty.")
887
894
  return result[0]
888
895
 
889
896
  @overload
890
897
  def getLastImageMD(
891
- self, channel: int, slice: int, md: pymmcore.Metadata, /
898
+ self,
899
+ channel: int,
900
+ slice: int,
901
+ md: pymmcore.Metadata,
902
+ /,
903
+ *,
904
+ out: np.ndarray | None = None,
892
905
  ) -> np.ndarray: ...
893
906
  @overload
894
- def getLastImageMD(self, md: pymmcore.Metadata, /) -> np.ndarray: ...
895
- def getLastImageMD(self, *args: Any) -> np.ndarray:
907
+ def getLastImageMD(
908
+ self, md: pymmcore.Metadata, /, *, out: np.ndarray | None = None
909
+ ) -> np.ndarray: ...
910
+ def getLastImageMD(self, *args: Any, out: np.ndarray | None = None) -> np.ndarray:
896
911
  if self._py_camera() is None:
897
912
  return super().getLastImageMD(*args)
898
913
  md_object = args[0] if len(args) == 1 else args[-1]
899
914
  if not isinstance(md_object, pymmcore.Metadata): # pragma: no cover
900
915
  raise TypeError("Expected a Metadata object for the last argument.")
901
916
 
902
- if not (self._seq_buffer) or (result := self._seq_buffer.peek_last()) is None:
917
+ if (
918
+ not (self._seq_buffer)
919
+ or (result := self._seq_buffer.peek_last(out=out)) is None
920
+ ):
903
921
  raise IndexError("Circular buffer is empty.")
904
922
 
905
923
  img, md = result
@@ -910,13 +928,20 @@ class UniMMCore(CMMCorePlus):
910
928
 
911
929
  return img
912
930
 
913
- def getNBeforeLastImageMD(self, n: int, md: pymmcore.Metadata, /) -> np.ndarray:
931
+ def getNBeforeLastImageMD(
932
+ self,
933
+ n: int,
934
+ md: pymmcore.Metadata,
935
+ /,
936
+ *,
937
+ out: np.ndarray | None = None,
938
+ ) -> np.ndarray:
914
939
  if self._py_camera() is None:
915
940
  return super().getNBeforeLastImageMD(n, md)
916
941
 
917
942
  if (
918
943
  not (self._seq_buffer)
919
- or (result := self._seq_buffer.peek_nth_from_last(n)) is None
944
+ or (result := self._seq_buffer.peek_nth_from_last(n, out=out)) is None
920
945
  ):
921
946
  raise IndexError("Circular buffer is empty or n is out of range.")
922
947
 
@@ -1007,7 +1032,7 @@ class UniMMCore(CMMCorePlus):
1007
1032
 
1008
1033
  return self._seq_buffer.size_bytes // bytes_per_frame
1009
1034
 
1010
- def _predicted_bytes_per_frame(self, cam: Camera) -> int:
1035
+ def _predicted_bytes_per_frame(self, cam: CameraDevice) -> int:
1011
1036
  # Estimate capacity based on camera settings and circular buffer size
1012
1037
  shape, dtype = cam.shape(), np.dtype(cam.dtype())
1013
1038
  return int(np.prod(shape) * dtype.itemsize)
@@ -1593,6 +1618,48 @@ class UniMMCore(CMMCorePlus):
1593
1618
  except KeyError as e:
1594
1619
  raise RuntimeError(str(e)) from e # convert to RuntimeError
1595
1620
 
1621
+ # ########################################################################
1622
+ # ------------------------ Shutter Device Methods ------------------------
1623
+ # ########################################################################
1624
+
1625
+ def _py_shutter(self, shutterLabel: str | None = None) -> ShutterDevice | None:
1626
+ """Return the *Python* Shutter device for ``label``, else ``None``."""
1627
+ label = shutterLabel or self.getShutterDevice()
1628
+ if label in self._pydevices:
1629
+ return self._pydevices.get_device_of_type(label, ShutterDevice)
1630
+ return None
1631
+
1632
+ def setShutterDevice(self, shutterLabel: DeviceLabel | str) -> None:
1633
+ label = self._set_current_if_pydevice(KW.CoreShutter, shutterLabel)
1634
+ super().setShutterDevice(label)
1635
+
1636
+ def getShutterDevice(self) -> DeviceLabel | Literal[""]:
1637
+ """Returns the label of the currently selected Shutter device.
1638
+
1639
+ Returns empty string if no Shutter device is selected.
1640
+ """
1641
+ return self._pycore.current(KW.CoreShutter) or super().getShutterDevice()
1642
+
1643
+ @overload
1644
+ def getShutterOpen(self) -> bool: ...
1645
+ @overload
1646
+ def getShutterOpen(self, shutterLabel: DeviceLabel | str) -> bool: ...
1647
+ def getShutterOpen(self, shutterLabel: DeviceLabel | str | None = None) -> bool:
1648
+ shutterLabel = shutterLabel or self.getShutterDevice()
1649
+ if (shutter := self._py_shutter(shutterLabel)) is None:
1650
+ return super().getShutterOpen(shutterLabel)
1651
+
1652
+ with shutter:
1653
+ return shutter.get_open()
1654
+
1655
+ def _do_shutter_open(self, shutterLabel: str, state: bool, /) -> None:
1656
+ """Open or close the shutter."""
1657
+ if (shutter := self._py_shutter(shutterLabel)) is None: # pragma: no cover
1658
+ return pymmcore.CMMCore.setShutterOpen(self, shutterLabel, state)
1659
+
1660
+ with shutter:
1661
+ shutter.set_open(state)
1662
+
1596
1663
 
1597
1664
  # -------------------------------------------------------------------------------
1598
1665
 
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Literal
6
6
 
7
7
  from pymmcore_plus.core._constants import DeviceType, Keyword, PixelFormat
8
8
 
9
- from ._device import Device
9
+ from ._device_base import Device
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Iterator, Mapping, Sequence
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from numpy.typing import DTypeLike
16
16
 
17
17
 
18
- class Camera(Device):
18
+ class CameraDevice(Device):
19
19
  # mandatory methods for Camera device adapters
20
20
 
21
21
  _TYPE: ClassVar[Literal[DeviceType.Camera]] = DeviceType.Camera
@@ -46,7 +46,7 @@ class Camera(Device):
46
46
  @abstractmethod
47
47
  def start_sequence(
48
48
  self,
49
- n: int,
49
+ n: int | None,
50
50
  get_buffer: Callable[[Sequence[int], DTypeLike], np.ndarray],
51
51
  ) -> Iterator[Mapping]:
52
52
  """Start a sequence acquisition.
@@ -60,8 +60,9 @@ class Camera(Device):
60
60
 
61
61
  Parameters
62
62
  ----------
63
- n : int
64
- The number of images to acquire.
63
+ n : int | None
64
+ If an integer, this is the number of images to acquire.
65
+ If None, the camera should acquire images indefinitely until stopped.
65
66
  get_buffer : Callable[[Sequence[int], DTypeLike], np.ndarray]
66
67
  A callable that returns a buffer for the camera to fill with image data.
67
68
  You should call this with the shape of the image and the dtype
@@ -76,6 +77,10 @@ class Camera(Device):
76
77
  """
77
78
  # EXAMPLE USAGE:
78
79
  # shape, dtype = self.shape(), self.dtype()
80
+ # if n is None: # acquire indefinitely until stopped
81
+ # while True:
82
+ # yield ...
83
+ # return
79
84
  # for _ in range(n):
80
85
  # image = get_buffer(shape, dtype)
81
86
  # get the image from the camera, and fill the buffer in place
@@ -0,0 +1,12 @@
1
+ from pymmcore_plus.core._constants import DeviceType
2
+
3
+ from ._device_base import Device
4
+
5
+
6
+ class GenericDevice(Device):
7
+ """Generic device API, e.g. for devices that don't fit into other categories.
8
+
9
+ Generic Devices generally only use the device property interface.
10
+ """
11
+
12
+ _TYPE = DeviceType.GenericDevice
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
21
21
 
22
22
  from typing_extensions import Self, TypeAlias
23
23
 
24
- from ._device import Device
24
+ from ._device_base import Device
25
25
 
26
26
  PropArg: TypeAlias = (
27
27
  PropertyType | type | Literal["float", "integer", "string", "boolean"] | None
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+
5
+ from pymmcore_plus.core._constants import DeviceType
6
+
7
+ from ._device_base import Device
8
+
9
+
10
+ class ShutterDevice(Device):
11
+ """Shutter device API, e.g. for physical shutters or electronic shutter control.
12
+
13
+ Or any 2-state device that can be either open or closed.
14
+ """
15
+
16
+ _TYPE = DeviceType.ShutterDevice
17
+
18
+ @abstractmethod
19
+ def get_open(self) -> bool:
20
+ """Return True if the shutter is open, False if it is closed."""
21
+
22
+ @abstractmethod
23
+ def set_open(self, open: bool) -> None:
24
+ """Set the shutter to open or closed.
25
+
26
+ Parameters
27
+ ----------
28
+ open : bool
29
+ True to open the shutter, False to close it.
30
+ """
@@ -7,7 +7,7 @@ from numpy.typing import DTypeLike
7
7
 
8
8
  from pymmcore_plus.core._constants import DeviceType
9
9
 
10
- from ._device import SequenceableDevice
10
+ from ._device_base import SequenceableDevice
11
11
 
12
12
 
13
13
  class SLMDevice(SequenceableDevice[np.ndarray]):
@@ -4,7 +4,7 @@ from typing import ClassVar, Literal
4
4
  from pymmcore_plus.core import DeviceType
5
5
  from pymmcore_plus.core._constants import Keyword
6
6
 
7
- from ._device import SeqT, SequenceableDevice
7
+ from ._device_base import SeqT, SequenceableDevice
8
8
 
9
9
  __all__ = ["_BaseStage"]
10
10
 
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, cast
5
5
 
6
6
  from pymmcore_plus.core._constants import DeviceType, Keyword
7
7
 
8
- from ._device import Device
8
+ from ._device_base import Device
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Iterable, Mapping
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.15.0
3
+ Version: 0.15.3
4
4
  Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
5
5
  Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
6
6
  Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
@@ -30,7 +30,7 @@ Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
30
30
  Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
31
31
  Requires-Dist: platformdirs>=3.0.0
32
32
  Requires-Dist: psygnal>=0.10
33
- Requires-Dist: pymmcore>=11.2.1.71.0
33
+ Requires-Dist: pymmcore>=11.9.0.73.0
34
34
  Requires-Dist: rich>=10.2.0
35
35
  Requires-Dist: tensorstore!=0.1.72,>=0.1.67
36
36
  Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'