pymmcore-plus 0.13.7__py3-none-any.whl → 0.15.0__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 (27) hide show
  1. pymmcore_plus/__init__.py +2 -0
  2. pymmcore_plus/_accumulator.py +258 -0
  3. pymmcore_plus/_pymmcore.py +4 -2
  4. pymmcore_plus/core/__init__.py +34 -1
  5. pymmcore_plus/core/_constants.py +21 -3
  6. pymmcore_plus/core/_device.py +739 -19
  7. pymmcore_plus/core/_mmcore_plus.py +260 -47
  8. pymmcore_plus/core/events/_protocol.py +49 -34
  9. pymmcore_plus/core/events/_psygnal.py +2 -2
  10. pymmcore_plus/experimental/unicore/__init__.py +7 -1
  11. pymmcore_plus/experimental/unicore/_proxy.py +20 -3
  12. pymmcore_plus/experimental/unicore/core/__init__.py +0 -0
  13. pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +318 -0
  14. pymmcore_plus/experimental/unicore/core/_unicore.py +1702 -0
  15. pymmcore_plus/experimental/unicore/devices/_camera.py +196 -0
  16. pymmcore_plus/experimental/unicore/devices/_device.py +54 -28
  17. pymmcore_plus/experimental/unicore/devices/_properties.py +8 -1
  18. pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
  19. pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
  20. pymmcore_plus/mda/events/_protocol.py +8 -8
  21. pymmcore_plus/mda/handlers/_tensorstore_handler.py +3 -1
  22. {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/METADATA +14 -37
  23. {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/RECORD +26 -20
  24. pymmcore_plus/experimental/unicore/_unicore.py +0 -703
  25. {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/WHEEL +0 -0
  26. {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/entry_points.txt +0 -0
  27. {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,9 +13,18 @@ from pathlib import Path
13
13
  from re import Pattern
14
14
  from textwrap import dedent
15
15
  from threading import Thread
16
- from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast, overload
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ NamedTuple,
21
+ TypeVar,
22
+ cast,
23
+ overload,
24
+ )
17
25
 
18
26
  from psygnal import SignalInstance
27
+ from typing_extensions import deprecated
19
28
 
20
29
  import pymmcore_plus._pymmcore as pymmcore
21
30
  from pymmcore_plus._logger import current_logfile, logger
@@ -23,6 +32,7 @@ from pymmcore_plus._util import find_micromanager, print_tabular_data
23
32
  from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
24
33
  from pymmcore_plus.metadata.functions import summary_metadata
25
34
 
35
+ from . import _device
26
36
  from ._adapter import DeviceAdapter
27
37
  from ._config import Configuration
28
38
  from ._config_group import ConfigGroup
@@ -31,26 +41,38 @@ from ._constants import (
31
41
  DeviceInitializationState,
32
42
  DeviceType,
33
43
  FocusDirection,
44
+ Keyword,
34
45
  PixelType,
35
46
  PropertyType,
36
47
  )
37
- from ._device import Device
38
48
  from ._metadata import Metadata
39
49
  from ._property import DeviceProperty
40
50
  from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_class
41
51
 
42
52
  if TYPE_CHECKING:
43
53
  from collections.abc import Iterable, Iterator, Sequence
44
- from typing import Literal, TypedDict, Unpack
54
+ from typing import Literal, Never, TypeAlias, TypedDict, Union, Unpack
45
55
 
46
56
  import numpy as np
57
+ from pymmcore import DeviceLabel
47
58
  from useq import MDAEvent
48
59
 
49
60
  from pymmcore_plus.mda._runner import SingleOutput
50
61
  from pymmcore_plus.metadata.schema import SummaryMetaV1
51
62
 
52
63
  _T = TypeVar("_T")
64
+ _DT = TypeVar("_DT", bound=_device.Device)
53
65
  ListOrTuple = list[_T] | tuple[_T, ...]
66
+ DeviceTypesWithCurrent: TypeAlias = Union[
67
+ Literal[DeviceType.CameraDevice]
68
+ | Literal[DeviceType.ShutterDevice]
69
+ | Literal[DeviceType.StageDevice]
70
+ | Literal[DeviceType.XYStageDevice]
71
+ | Literal[DeviceType.AutoFocusDevice]
72
+ | Literal[DeviceType.SLMDevice]
73
+ | Literal[DeviceType.GalvoDevice]
74
+ | Literal[DeviceType.ImageProcessorDevice]
75
+ ]
54
76
 
55
77
  class PropertySchema(TypedDict, total=False):
56
78
  """JSON schema `dict` describing a device property."""
@@ -261,7 +283,7 @@ class CMMCorePlus(pymmcore.CMMCore):
261
283
 
262
284
  self._events = _get_auto_core_callback_class()()
263
285
  self._callback_relay = MMCallbackRelay(self.events)
264
- self.registerCallback(self._callback_relay)
286
+ super().registerCallback(self._callback_relay)
265
287
 
266
288
  self._mda_runner = MDARunner()
267
289
  self._mda_runner.set_engine(MDAEngine(self))
@@ -274,6 +296,24 @@ class CMMCorePlus(pymmcore.CMMCore):
274
296
  self._weak_clean = weakref.WeakMethod(self.unloadAllDevices)
275
297
  atexit.register(self._weak_clean)
276
298
 
299
+ @deprecated(
300
+ "registerCallback is disallowed in pymmcore-plus. Use .events instead."
301
+ )
302
+ def registerCallback(self, *_: Never) -> Never: # type: ignore[override]
303
+ """*registerCallback is disallowed in pymmcore-plus!*
304
+
305
+ If you want to connect callbacks to events, use the
306
+ [`CMMCorePlus.events`][pymmcore_plus.CMMCorePlus.events] property instead.
307
+ """ # noqa
308
+ raise RuntimeError(
309
+ dedent("""
310
+ This method is disallowed in pymmcore-plus.
311
+
312
+ If you want to connect callbacks to events, use the
313
+ `CMMCorePlus.events` property instead.
314
+ """)
315
+ )
316
+
277
317
  @property
278
318
  def events(self) -> PCoreSignaler:
279
319
  """Signaler for core events.
@@ -287,14 +327,20 @@ class CMMCorePlus(pymmcore.CMMCore):
287
327
  return self._events
288
328
 
289
329
  def __repr__(self) -> str:
290
- return f"<{type(self).__name__} at {hex(id(self))}>"
330
+ """Return a string representation of the core object."""
331
+ ndevices = len(self.getLoadedDevices()) - 1
332
+ return f"<{type(self).__name__} at {hex(id(self))} with {ndevices} devices>"
291
333
 
292
334
  def __del__(self) -> None:
293
335
  if hasattr(self, "_weak_clean"):
294
336
  atexit.unregister(self._weak_clean)
295
- self.unloadAllDevices()
296
- # clean up logging
297
- self.setPrimaryLogFile("")
337
+ try:
338
+ super().registerCallback(None) # type: ignore
339
+ self.reset()
340
+ # clean up logging
341
+ self.setPrimaryLogFile("")
342
+ except Exception as e:
343
+ logger.exception("Error during CMMCorePlus.__del__(): %s", e)
298
344
 
299
345
  # Re-implemented methods from the CMMCore API
300
346
 
@@ -334,7 +380,12 @@ class CMMCorePlus(pymmcore.CMMCore):
334
380
  and the property Value has actually changed.
335
381
  """
336
382
  with self._property_change_emission_ensured(stateDeviceLabel, STATE_PROPS):
337
- super().setStateLabel(stateDeviceLabel, stateLabel)
383
+ try:
384
+ super().setStateLabel(stateDeviceLabel, stateLabel)
385
+ except RuntimeError as e: # pragma: no cover
386
+ state_labels = self.getStateLabels(stateDeviceLabel)
387
+ msg = f"{e}. Available Labels: {state_labels}"
388
+ raise RuntimeError(msg) from None
338
389
 
339
390
  def setDeviceAdapterSearchPaths(self, paths: Sequence[str]) -> None:
340
391
  """Set the device adapter search paths.
@@ -377,6 +428,9 @@ class CMMCorePlus(pymmcore.CMMCore):
377
428
  recognized by the specific plugin library. See
378
429
  [`pymmcore.CMMCore.getAvailableDevices`][] for a list of valid device names.
379
430
  """
431
+ if str(label).lower() == Keyword.CoreDevice.value.lower(): # pragma: no cover
432
+ raise ValueError(f"Label {label!r} is reserved.")
433
+
380
434
  try:
381
435
  super().loadDevice(label, moduleName, deviceName)
382
436
  except (RuntimeError, ValueError) as e:
@@ -670,9 +724,9 @@ class CMMCorePlus(pymmcore.CMMCore):
670
724
  """
671
725
  md = Metadata()
672
726
  if channel is not None and slice is not None:
673
- img = super().getLastImageMD(channel, slice, md)
727
+ img = self.getLastImageMD(channel, slice, md)
674
728
  else:
675
- img = super().getLastImageMD(md)
729
+ img = self.getLastImageMD(md)
676
730
  return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
677
731
 
678
732
  @overload
@@ -714,7 +768,7 @@ class CMMCorePlus(pymmcore.CMMCore):
714
768
  Image and metadata
715
769
  """
716
770
  md = Metadata()
717
- img = super().popNextImageMD(channel, slice, md)
771
+ img = self.popNextImageMD(channel, slice, md)
718
772
  return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
719
773
 
720
774
  def popNextImage(self, *, fix: bool = True) -> np.ndarray:
@@ -758,7 +812,7 @@ class CMMCorePlus(pymmcore.CMMCore):
758
812
  will be reshaped to (w, h, n_components) using `fixImage`.
759
813
  """
760
814
  md = Metadata()
761
- img = super().getNBeforeLastImageMD(n, md)
815
+ img = self.getNBeforeLastImageMD(n, md)
762
816
  return self.fixImage(img) if fix and not pymmcore.NANO else img, md
763
817
 
764
818
  def setConfig(self, groupName: str, configName: str) -> None:
@@ -850,7 +904,7 @@ class CMMCorePlus(pymmcore.CMMCore):
850
904
  device_adapter: str | re.Pattern | None = ...,
851
905
  *,
852
906
  as_object: Literal[True] = ...,
853
- ) -> Iterator[Device]: ...
907
+ ) -> Iterator[_device.Device]: ...
854
908
 
855
909
  def iterDevices(
856
910
  self,
@@ -859,7 +913,7 @@ class CMMCorePlus(pymmcore.CMMCore):
859
913
  device_adapter: str | re.Pattern | None = None,
860
914
  *,
861
915
  as_object: bool = True,
862
- ) -> Iterator[Device] | Iterator[str]:
916
+ ) -> Iterator[_device.Device] | Iterator[str]:
863
917
  """Iterate over currently loaded devices.
864
918
 
865
919
  :sparkles: *This method is new in `CMMCorePlus`.*
@@ -912,7 +966,7 @@ class CMMCorePlus(pymmcore.CMMCore):
912
966
  devices = [d for d in devices if ptrn.search(self.getDeviceLibrary(d))]
913
967
 
914
968
  for dev in devices:
915
- yield Device(dev, mmcore=self) if as_object else dev
969
+ yield _device.Device.create(dev, mmcore=self) if as_object else dev
916
970
 
917
971
  @overload
918
972
  def iterProperties(
@@ -1093,7 +1147,77 @@ class CMMCorePlus(pymmcore.CMMCore):
1093
1147
  """
1094
1148
  return DeviceAdapter(library_name, mmcore=self)
1095
1149
 
1096
- def getDeviceObject(self, device_label: str) -> Device:
1150
+ @overload
1151
+ def getDeviceObject(
1152
+ self, device_label: str, device_type: Literal[DeviceType.Camera]
1153
+ ) -> _device.CameraDevice: ...
1154
+ @overload
1155
+ def getDeviceObject(
1156
+ self, device_label: str, device_type: Literal[DeviceType.Stage]
1157
+ ) -> _device.StageDevice: ...
1158
+ @overload
1159
+ def getDeviceObject(
1160
+ self, device_label: str, device_type: Literal[DeviceType.State]
1161
+ ) -> _device.StateDevice: ...
1162
+ @overload
1163
+ def getDeviceObject(
1164
+ self, device_label: str, device_type: Literal[DeviceType.Shutter]
1165
+ ) -> _device.ShutterDevice: ...
1166
+ @overload
1167
+ def getDeviceObject(
1168
+ self, device_label: str, device_type: Literal[DeviceType.XYStage]
1169
+ ) -> _device.XYStageDevice: ...
1170
+ @overload
1171
+ def getDeviceObject(
1172
+ self, device_label: str, device_type: Literal[DeviceType.Serial]
1173
+ ) -> _device.SerialDevice: ...
1174
+ @overload
1175
+ def getDeviceObject(
1176
+ self, device_label: str, device_type: Literal[DeviceType.Generic]
1177
+ ) -> _device.GenericDevice: ...
1178
+ @overload
1179
+ def getDeviceObject(
1180
+ self, device_label: str, device_type: Literal[DeviceType.AutoFocus]
1181
+ ) -> _device.AutoFocusDevice: ...
1182
+ @overload
1183
+ def getDeviceObject(
1184
+ self, device_label: str, device_type: Literal[DeviceType.ImageProcessor]
1185
+ ) -> _device.ImageProcessorDevice: ...
1186
+ @overload
1187
+ def getDeviceObject(
1188
+ self, device_label: str, device_type: Literal[DeviceType.SignalIO]
1189
+ ) -> _device.SignalIODevice: ...
1190
+ @overload
1191
+ def getDeviceObject(
1192
+ self, device_label: str, device_type: Literal[DeviceType.Magnifier]
1193
+ ) -> _device.MagnifierDevice: ...
1194
+ @overload
1195
+ def getDeviceObject(
1196
+ self, device_label: str, device_type: Literal[DeviceType.SLM]
1197
+ ) -> _device.SLMDevice: ...
1198
+ @overload
1199
+ def getDeviceObject(
1200
+ self, device_label: str, device_type: Literal[DeviceType.Hub]
1201
+ ) -> _device.HubDevice: ...
1202
+ @overload
1203
+ def getDeviceObject(
1204
+ self, device_label: str, device_type: Literal[DeviceType.Galvo]
1205
+ ) -> _device.GalvoDevice: ...
1206
+ @overload
1207
+ def getDeviceObject(
1208
+ self,
1209
+ device_label: Literal[Keyword.CoreDevice],
1210
+ device_type: Literal[DeviceType.Core],
1211
+ ) -> _device.CoreDevice: ...
1212
+ @overload
1213
+ def getDeviceObject(
1214
+ self, device_label: str, device_type: DeviceType = ...
1215
+ ) -> _device.Device: ...
1216
+ @overload
1217
+ def getDeviceObject(self, device_label: str, device_type: type[_DT]) -> _DT: ...
1218
+ def getDeviceObject(
1219
+ self, device_label: str, device_type: type[_DT] | DeviceType = DeviceType.Any
1220
+ ) -> _DT | _device.Device:
1097
1221
  """Return a `Device` object bound to device_label on this core.
1098
1222
 
1099
1223
  :sparkles: *This method is new in `CMMCorePlus`.*
@@ -1136,7 +1260,18 @@ class CMMCorePlus(pymmcore.CMMCore):
1136
1260
  }
1137
1261
  }
1138
1262
  """
1139
- return Device(device_label, mmcore=self)
1263
+ dev = _device.Device.create(device_label, mmcore=self)
1264
+ if (isinstance(device_type, type) and not isinstance(dev, device_type)) or (
1265
+ isinstance(device_type, DeviceType)
1266
+ and device_type not in {DeviceType.Any, DeviceType.Unknown}
1267
+ and dev.type() != device_type
1268
+ ):
1269
+ raise TypeError(
1270
+ f"{device_type!r} requested but device with label "
1271
+ f"{device_label!r} is a {dev.type()}."
1272
+ )
1273
+
1274
+ return dev
1140
1275
 
1141
1276
  def getConfigGroupObject(
1142
1277
  self, group_name: str, allow_missing: bool = False
@@ -1189,6 +1324,61 @@ class CMMCorePlus(pymmcore.CMMCore):
1189
1324
  for group in self.getAvailableConfigGroups():
1190
1325
  yield ConfigGroup(group, mmcore=self)
1191
1326
 
1327
+ def getCurrentDeviceOfType(
1328
+ self, device_type: DeviceTypesWithCurrent
1329
+ ) -> DeviceLabel | Literal[""]:
1330
+ """Return the current device of type `device_type`.
1331
+
1332
+ Only the following device types have a "current" device:
1333
+ - CameraDevice
1334
+ - ShutterDevice
1335
+ - StageDevice
1336
+ - XYStageDevice
1337
+ - AutoFocusDevice
1338
+ - SLMDevice
1339
+ - GalvoDevice
1340
+ - ImageProcessorDevice
1341
+
1342
+ Calling this method with any other device type will raise a `ValueError`.
1343
+
1344
+ :sparkles: *This method is new in `CMMCorePlus`.*
1345
+
1346
+ Parameters
1347
+ ----------
1348
+ device_type : DeviceType
1349
+ The type of device to get the current device for.
1350
+ See [`DeviceType`][pymmcore_plus.DeviceType] for a list of device types.
1351
+
1352
+ Returns
1353
+ -------
1354
+ str
1355
+ The label of the current device of type `device_type`.
1356
+ If no device of that type is currently set, an empty string is returned.
1357
+
1358
+ Raises
1359
+ ------
1360
+ ValueError
1361
+ If the core does not have the concept of a "current" device of the provided
1362
+ `device_type`.
1363
+ """
1364
+ if device_type == DeviceType.CameraDevice:
1365
+ return self.getCameraDevice()
1366
+ if device_type == DeviceType.ShutterDevice:
1367
+ return self.getShutterDevice()
1368
+ if device_type == DeviceType.StageDevice:
1369
+ return self.getFocusDevice()
1370
+ if device_type == DeviceType.XYStageDevice:
1371
+ return self.getXYStageDevice()
1372
+ if device_type == DeviceType.AutoFocusDevice:
1373
+ return self.getAutoFocusDevice()
1374
+ if device_type == DeviceType.SLMDevice:
1375
+ return self.getSLMDevice()
1376
+ if device_type == DeviceType.GalvoDevice:
1377
+ return self.getGalvoDevice()
1378
+ if device_type == DeviceType.ImageProcessorDevice:
1379
+ return self.getImageProcessorDevice()
1380
+ raise ValueError(f"'Current' {device_type.name} is undefined. ")
1381
+
1192
1382
  def getDeviceSchema(self, device_label: str) -> DeviceSchema:
1193
1383
  """Return JSON-schema describing device `device_label` and its properties.
1194
1384
 
@@ -1377,7 +1567,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1377
1567
  def setXYPosition(self, x: float, y: float, /) -> None: ...
1378
1568
  @overload
1379
1569
  def setXYPosition(self, xyStageLabel: str, x: float, y: float, /) -> None: ...
1380
- def setXYPosition(self, *args: str | float) -> None:
1570
+ def setXYPosition(self, *args: Any) -> None:
1381
1571
  """Sets the position of the XY stage in microns.
1382
1572
 
1383
1573
  **Why Override?** to store the last commanded stage position internally.
@@ -1386,10 +1576,10 @@ class CMMCorePlus(pymmcore.CMMCore):
1386
1576
  label: str | None = None
1387
1577
  x, y = cast("tuple[float, float]", args)
1388
1578
  elif len(args) == 3:
1389
- label, x, y = args # type: ignore
1579
+ label, x, y = args
1390
1580
  else:
1391
1581
  raise ValueError("Invalid number of arguments. Expected 2 or 3.")
1392
- super().setXYPosition(*args) # type: ignore
1582
+ super().setXYPosition(*args)
1393
1583
  self._last_xy_position[label] = (x, y)
1394
1584
 
1395
1585
  def getZPosition(self) -> float:
@@ -1435,7 +1625,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1435
1625
  if autoshutter := self.getAutoShutter():
1436
1626
  self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
1437
1627
  try:
1438
- super().snapImage()
1628
+ self._do_snap_image()
1439
1629
  self.events.imageSnapped.emit()
1440
1630
  finally:
1441
1631
  if autoshutter:
@@ -1720,15 +1910,12 @@ class CMMCorePlus(pymmcore.CMMCore):
1720
1910
  **Why Override?** To emit a `startContinuousSequenceAcquisition` event.
1721
1911
  """
1722
1912
  self.events.continuousSequenceAcquisitionStarting.emit()
1723
- super().startContinuousSequenceAcquisition(intervalMs)
1913
+ self._do_start_continuous_sequence_acquisition(intervalMs)
1724
1914
  self.events.continuousSequenceAcquisitionStarted.emit()
1725
1915
 
1726
1916
  @overload
1727
1917
  def startSequenceAcquisition(
1728
- self,
1729
- numImages: int,
1730
- intervalMs: float,
1731
- stopOnOverflow: bool,
1918
+ self, numImages: int, intervalMs: float, stopOnOverflow: bool, /
1732
1919
  ) -> None: ...
1733
1920
 
1734
1921
  @overload
@@ -1738,9 +1925,10 @@ class CMMCorePlus(pymmcore.CMMCore):
1738
1925
  numImages: int,
1739
1926
  intervalMs: float,
1740
1927
  stopOnOverflow: bool,
1928
+ /,
1741
1929
  ) -> None: ...
1742
1930
 
1743
- def startSequenceAcquisition(self, *args: Any, **kwargs: Any) -> None:
1931
+ def startSequenceAcquisition(self, *args: Any) -> None:
1744
1932
  """Starts streaming camera sequence acquisition.
1745
1933
 
1746
1934
  This command does not block the calling thread for the duration of the
@@ -1749,18 +1937,16 @@ class CMMCorePlus(pymmcore.CMMCore):
1749
1937
  **Why Override?** To emit a `startSequenceAcquisition` event.
1750
1938
  """
1751
1939
  if len(args) == 3:
1752
- numImages, intervalMs, stopOnOverflow = args
1753
- cameraLabel = super().getCameraDevice()
1754
- else:
1755
- cameraLabel, numImages, intervalMs, stopOnOverflow = args
1940
+ args = (self.getCameraDevice(), *args)
1941
+ elif len(args) != 4:
1942
+ raise ValueError(
1943
+ "startSequenceAcquisition requires either 3 or 4 arguments, "
1944
+ f"got {len(args)}."
1945
+ )
1756
1946
 
1757
- self.events.sequenceAcquisitionStarting.emit(
1758
- cameraLabel, numImages, intervalMs, stopOnOverflow
1759
- )
1760
- super().startSequenceAcquisition(*args, **kwargs)
1761
- self.events.sequenceAcquisitionStarted.emit(
1762
- cameraLabel, numImages, intervalMs, stopOnOverflow
1763
- )
1947
+ self.events.sequenceAcquisitionStarting.emit(*args)
1948
+ self._do_start_sequence_acquisition(*args)
1949
+ self.events.sequenceAcquisitionStarted.emit(*args)
1764
1950
 
1765
1951
  def stopSequenceAcquisition(self, cameraLabel: str | None = None) -> None:
1766
1952
  """Stops streaming camera sequence acquisition.
@@ -1769,13 +1955,32 @@ class CMMCorePlus(pymmcore.CMMCore):
1769
1955
 
1770
1956
  **Why Override?** To emit a `stopSequenceAcquisition` event.
1771
1957
  """
1772
- if cameraLabel is None:
1773
- super().stopSequenceAcquisition()
1774
- else:
1775
- super().stopSequenceAcquisition(cameraLabel)
1776
1958
  cameraLabel = cameraLabel or super().getCameraDevice()
1959
+ self._do_stop_sequence_acquisition(cameraLabel)
1777
1960
  self.events.sequenceAcquisitionStopped.emit(cameraLabel)
1778
1961
 
1962
+ # here for ease of overriding in Unicore ---------------------
1963
+
1964
+ def _do_snap_image(self) -> None:
1965
+ super().snapImage()
1966
+
1967
+ def _do_start_sequence_acquisition(
1968
+ self, cameraLabel: str, numImages: int, intervalMs: float, stopOnOverflow: bool
1969
+ ) -> None:
1970
+ super().startSequenceAcquisition(
1971
+ cameraLabel, numImages, intervalMs, stopOnOverflow
1972
+ )
1973
+
1974
+ def _do_start_continuous_sequence_acquisition(self, intervalMs: float) -> None:
1975
+ """Starts the actual continuous sequence acquisition process."""
1976
+ super().startContinuousSequenceAcquisition(intervalMs)
1977
+
1978
+ def _do_stop_sequence_acquisition(self, cameraLabel: str) -> None:
1979
+ """Stops the actual sequence acquisition process."""
1980
+ super().stopSequenceAcquisition(cameraLabel)
1981
+
1982
+ # end of Unicore helpers ---------------------
1983
+
1779
1984
  def setAutoFocusOffset(self, offset: float) -> None:
1780
1985
  """Applies offset the one-shot focusing device.
1781
1986
 
@@ -1990,21 +2195,29 @@ class CMMCorePlus(pymmcore.CMMCore):
1990
2195
  return list(xs), list(ys), list(ws), list(hs)
1991
2196
 
1992
2197
  @overload
1993
- def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
2198
+ def setROI(self, x: int, y: int, width: int, height: int, /) -> None: ...
1994
2199
 
1995
2200
  @overload
1996
- def setROI(self, label: str, x: int, y: int, width: int, height: int) -> None: ...
2201
+ def setROI(
2202
+ self, label: str, x: int, y: int, width: int, height: int, /
2203
+ ) -> None: ...
1997
2204
 
1998
- def setROI(self, *args: Any, **kwargs: Any) -> None:
2205
+ def setROI(self, *args: Any) -> None:
1999
2206
  """Set the camera Region of Interest (ROI).
2000
2207
 
2001
2208
  **Why Override?** To emit a `roiSet` event.
2002
2209
  """
2003
- super().setROI(*args, **kwargs)
2004
2210
  if len(args) == 4:
2005
2211
  args = (super().getCameraDevice(), *args)
2212
+ self._do_set_roi(*args)
2006
2213
  self.events.roiSet.emit(*args)
2007
2214
 
2215
+ # here for ease of overriding in Unicore
2216
+
2217
+ def _do_set_roi(self, label: str, x: int, y: int, width: int, height: int) -> None:
2218
+ """Internal method to set the ROI for a specific camera device."""
2219
+ super().setROI(label, x, y, width, height)
2220
+
2008
2221
  def setChannelGroup(self, channelGroup: str) -> None:
2009
2222
  """Specifies the group determining the channel selection.
2010
2223