pymmcore-plus 0.13.6__py3-none-any.whl → 0.14.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.
@@ -1,12 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any, Literal, overload
4
4
 
5
- from ._constants import DeviceType
5
+ from ._constants import DeviceType, FocusDirection, Keyword
6
6
  from ._property import DeviceProperty
7
7
  from .events._device_signal_view import _DevicePropValueSignal
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+
12
+ from pymmcore import StateLabel
13
+ from typing_extensions import Self
14
+
15
+ from pymmcore_plus._accumulator import (
16
+ PositionChangeAccumulator,
17
+ XYPositionChangeAccumulator,
18
+ )
10
19
  from pymmcore_plus.core.events._protocol import PSignalInstance
11
20
 
12
21
  from ._constants import DeviceDetectionStatus
@@ -14,7 +23,7 @@ if TYPE_CHECKING:
14
23
 
15
24
 
16
25
  class Device:
17
- """Convenience view onto a device.
26
+ """Convenience object-oriented device API.
18
27
 
19
28
  This is the type of object that is returned by
20
29
  [`pymmcore_plus.CMMCorePlus.getDeviceObject`][]
@@ -22,9 +31,9 @@ class Device:
22
31
  Parameters
23
32
  ----------
24
33
  device_label : str
25
- Device this property belongs to
34
+ Device label assigned to this device.
26
35
  mmcore : CMMCorePlus
27
- CMMCorePlus instance
36
+ CMMCorePlus instance that owns this device.
28
37
 
29
38
  Examples
30
39
  --------
@@ -46,6 +55,21 @@ class Device:
46
55
  UNASSIGNED = "__UNASSIGNED__"
47
56
  propertyChanged: PSignalInstance
48
57
 
58
+ @classmethod
59
+ def create(cls, device_label: str, mmcore: CMMCorePlus) -> Self:
60
+ sub_cls = cls.get_subclass(device_label, mmcore)
61
+ # make sure it's an error to call this class method on a subclass with
62
+ # a non-matching type
63
+ if issubclass(sub_cls, cls):
64
+ return sub_cls(device_label, mmcore)
65
+ dev_type = mmcore.getDeviceType(device_label).name
66
+ raise TypeError(f"Cannot cast {dev_type} {device_label!r} to {cls}")
67
+
68
+ @classmethod
69
+ def get_subclass(cls, device_label: str, mmcore: CMMCorePlus) -> type[Device]:
70
+ dev_type = mmcore.getDeviceType(device_label)
71
+ return _TYPE_MAP[dev_type]
72
+
49
73
  def __init__(
50
74
  self,
51
75
  device_label: str = UNASSIGNED,
@@ -63,6 +87,18 @@ class Device:
63
87
  self._mmc = mmcore
64
88
 
65
89
  self._label = device_label
90
+ self._type = None
91
+ if self.isLoaded():
92
+ adapter_name = self._mmc.getDeviceLibrary(device_label)
93
+ device_name = self._mmc.getDeviceName(device_label)
94
+ description = self._mmc.getDeviceDescription(device_label)
95
+ type = self._mmc.getDeviceType(device_label) # noqa: A001
96
+ if self.type() != type:
97
+ raise TypeError(
98
+ f"Cannot create loaded device with label {device_label!r} and type "
99
+ f"{type.name!r} as an instance of {self.__class__.__name__!r}"
100
+ )
101
+
66
102
  self._adapter_name = adapter_name
67
103
  self._device_name = device_name
68
104
  self._type = type
@@ -77,7 +113,9 @@ class Device:
77
113
  @label.setter
78
114
  def label(self, value: str) -> None:
79
115
  if self.isLoaded():
80
- raise RuntimeError("Cannot change label of loaded device")
116
+ raise RuntimeError(f"Cannot change label of loaded device {self.label!r}.")
117
+ if value in self._mmc.getLoadedDevices(): # pragma: no cover
118
+ raise RuntimeError(f"Label {value!r} is already in use.")
81
119
  self._label = value
82
120
 
83
121
  @property
@@ -120,24 +158,54 @@ class Device:
120
158
  @property
121
159
  def properties(self) -> tuple[DeviceProperty, ...]:
122
160
  """Get all properties supported by device as DeviceProperty objects."""
123
- return tuple(
124
- DeviceProperty(self.label, name, self._mmc) for name in self.propertyNames()
125
- )
161
+ return tuple(self.getPropertyObject(name) for name in self.propertyNames())
126
162
 
127
163
  def getPropertyObject(self, property_name: str) -> DeviceProperty:
128
164
  """Return a `DeviceProperty` object bound to this device on this core."""
165
+ if not self._mmc.hasProperty(self.label, property_name):
166
+ raise ValueError(f"Device {self.label!r} has no property {property_name!r}")
129
167
  return DeviceProperty(self.label, property_name, self._mmc)
130
168
 
169
+ def setProperty(self, property_name: str, value: bool | float | int | str) -> None:
170
+ """Set a device property value.
171
+
172
+ See also,
173
+ [`Device.getPropertyObject`][pymmcore_plus.core.Device.getPropertyObject].
174
+
175
+ Examples
176
+ --------
177
+ >>> camera = Device("Camera")
178
+ >>> camera.setProperty("Exposure", 100)
179
+ >>> print(camera.getProperty("Exposure"))
180
+ # or
181
+ >>> exposure = camera.getPropertyObject("Exposure")
182
+ >>> exposure.value = 100
183
+ >>> print(exposure.value)
184
+ """
185
+ return self._mmc.setProperty(self.label, property_name, value)
186
+
187
+ def getProperty(self, property_name: str) -> str:
188
+ """Get a device property value."""
189
+ return self._mmc.getProperty(self.label, property_name)
190
+
131
191
  def initialize(self) -> None:
132
192
  """Initialize device."""
133
193
  return self._mmc.initializeDevice(self.label)
134
194
 
195
+ def unload(self) -> None:
196
+ """Unload device from the core and adjust all configuration data."""
197
+ return self._mmc.unloadDevice(self.label)
198
+
199
+ def isLoaded(self) -> bool:
200
+ """Return `True` if device is loaded."""
201
+ return self.label in self._mmc.getLoadedDevices()
202
+
135
203
  def load(
136
204
  self,
137
205
  adapter_name: str = "",
138
206
  device_name: str = "",
139
207
  device_label: str = "",
140
- ) -> None:
208
+ ) -> Device:
141
209
  """Load device from the plugin library.
142
210
 
143
211
  Parameters
@@ -156,6 +224,9 @@ class Device:
156
224
  assigned a default name: `adapter_name-device_name`, unless this Device
157
225
  instance was initialized with a label.
158
226
  """
227
+ # if self.isLoaded():
228
+ # raise RuntimeError(f"Device {self.label!r} is already loaded.")
229
+
159
230
  if not (adapter_name := adapter_name or self._adapter_name):
160
231
  raise TypeError("Must specify adapter_name")
161
232
  if not (device_name := device_name or self._device_name):
@@ -165,15 +236,10 @@ class Device:
165
236
  elif self.label == self.UNASSIGNED:
166
237
  self.label = f"{adapter_name}-{device_name}"
167
238
 
239
+ # note: this method takes care of label already being loaded and only
240
+ # warns if the exact label, adapter, and device are in use
168
241
  self._mmc.loadDevice(self.label, adapter_name, device_name)
169
-
170
- def unload(self) -> None:
171
- """Unload device from the core and adjust all configuration data."""
172
- return self._mmc.unloadDevice(self.label)
173
-
174
- def isLoaded(self) -> bool:
175
- """Return `True` if device is loaded."""
176
- return self.label in self._mmc.getLoadedDevices()
242
+ return Device.create(self.label, self._mmc)
177
243
 
178
244
  def detect(self) -> DeviceDetectionStatus:
179
245
  """Tries to communicate to device through a given serial port.
@@ -205,6 +271,14 @@ class Device:
205
271
  """Block the calling thread until device becomes non-busy."""
206
272
  self._mmc.waitForDevice(self.label)
207
273
 
274
+ def getParentLabel(self) -> str:
275
+ """Return the parent device label of this device."""
276
+ return self._mmc.getParentLabel(self.label)
277
+
278
+ def setParentLabel(self, parent_label: str) -> None:
279
+ """Set the parent device label of this device."""
280
+ self._mmc.setParentLabel(self.label, parent_label)
281
+
208
282
  def __repr__(self) -> str:
209
283
  if self.isLoaded():
210
284
  n = len(self.propertyNames())
@@ -214,4 +288,650 @@ class Device:
214
288
  props = "NOT LOADED"
215
289
  lib = ""
216
290
  core = repr(self._mmc).strip("<>")
217
- return f"<Device {self.label!r} {lib}on {core}: {props}>"
291
+ return f"<{self.__class__.__name__} {self.label!r} {lib}on {core}: {props}>"
292
+
293
+
294
+ class CameraDevice(Device):
295
+ def type(self) -> Literal[DeviceType.Camera]:
296
+ return DeviceType.Camera
297
+
298
+ def setROI(self, x: int, y: int, width: int, height: int) -> None:
299
+ """Set region of interest for camera."""
300
+ self._mmc.setROI(self.label, x, y, width, height)
301
+
302
+ def getROI(self) -> list[int]: # always a list of 4 ints ... but not a tuple
303
+ """Return region of interest for camera."""
304
+ return self._mmc.getROI(self.label)
305
+
306
+ # no device label-specific method for these ... would need to implement directly
307
+ # clearROI
308
+ # isMultiROISupported
309
+ # isMultiROIEnabled
310
+ # setMultiROI
311
+ # getMultiROI
312
+ # snapImage
313
+ # getImage
314
+ # getImageWidth
315
+ # getImageHeight
316
+ # getBytesPerPixel
317
+ # getImageBitDepth
318
+ # getNumberOfComponents
319
+ # getNumberOfCameraChannels
320
+
321
+ @property
322
+ def exposure(self) -> float:
323
+ return self.getExposure()
324
+
325
+ @exposure.setter
326
+ def exposure(self, value: float) -> None:
327
+ self.setExposure(value)
328
+
329
+ def setExposure(self, exposure: float) -> None:
330
+ """Set exposure time for camera."""
331
+ self._mmc.setExposure(self.label, exposure)
332
+
333
+ def getExposure(self) -> float:
334
+ """Return exposure time for camera."""
335
+ return self._mmc.getExposure(self.label)
336
+
337
+ def startSequenceAcquisition(
338
+ self, numImages: int, intervalMs: float, stopOnOverflow: bool
339
+ ) -> None:
340
+ """Start sequence acquisition."""
341
+ self._mmc.startSequenceAcquisition(
342
+ self.label, numImages, intervalMs, stopOnOverflow
343
+ )
344
+
345
+ def prepareSequenceAcquisition(self) -> None:
346
+ """Prepare sequence acquisition."""
347
+ self._mmc.prepareSequenceAcquisition(self.label)
348
+
349
+ def stopSequenceAcquisition(self) -> None:
350
+ """Stop sequence acquisition."""
351
+ self._mmc.stopSequenceAcquisition(self.label)
352
+
353
+ def isSequenceRunning(self) -> bool:
354
+ """Return `True` if sequence acquisition is running."""
355
+ return self._mmc.isSequenceRunning(self.label)
356
+
357
+ def isExposureSequenceable(self) -> bool:
358
+ """Return `True` if camera supports exposure sequence."""
359
+ return self._mmc.isExposureSequenceable(self.label)
360
+
361
+ def loadExposureSequence(self, sequence: Sequence[float]) -> None:
362
+ """Load exposure sequence."""
363
+ self._mmc.loadExposureSequence(self.label, sequence)
364
+
365
+ def startExposureSequence(self) -> None:
366
+ """Start exposure sequence."""
367
+ self._mmc.startExposureSequence(self.label)
368
+
369
+ def stopExposureSequence(self) -> None:
370
+ """Stop exposure sequence."""
371
+ self._mmc.stopExposureSequence(self.label)
372
+
373
+ def getExposureSequenceMaxLength(self) -> int:
374
+ """Return the maximum length of a camera's exposure sequence."""
375
+ return self._mmc.getExposureSequenceMaxLength(self.label)
376
+
377
+ isSequenceable = isExposureSequenceable
378
+ loadSequence = loadExposureSequence
379
+ startSequence = startExposureSequence
380
+ stopSequence = stopExposureSequence
381
+ getSequenceMaxLength = getExposureSequenceMaxLength
382
+
383
+
384
+ class ShutterDevice(Device):
385
+ def type(self) -> Literal[DeviceType.Shutter]:
386
+ return DeviceType.Shutter
387
+
388
+ def open(self) -> None:
389
+ """Open shutter."""
390
+ self._mmc.setShutterOpen(self.label, True)
391
+
392
+ def close(self) -> None:
393
+ """Close shutter."""
394
+ self._mmc.setShutterOpen(self.label, False)
395
+
396
+ def isOpen(self) -> bool:
397
+ """Return `True` if shutter is open."""
398
+ return self._mmc.getShutterOpen(self.label)
399
+
400
+
401
+ class StateDevice(Device):
402
+ def type(self) -> Literal[DeviceType.State]:
403
+ return DeviceType.State
404
+
405
+ @property
406
+ def state(self) -> int:
407
+ return self._mmc.getState(self.label)
408
+
409
+ @state.setter
410
+ def state(self, state: int) -> None:
411
+ self._mmc.setState(self.label, state)
412
+
413
+ def setState(self, state: int) -> None:
414
+ """Set state."""
415
+ self._mmc.setState(self.label, state)
416
+
417
+ def getState(self) -> int:
418
+ """Return state."""
419
+ return self._mmc.getState(self.label)
420
+
421
+ def getNumberOfStates(self) -> int:
422
+ """Return number of states."""
423
+ return self._mmc.getNumberOfStates(self.label)
424
+
425
+ def setStateLabel(self, label: str) -> None:
426
+ """Set state by label."""
427
+ self._mmc.setStateLabel(self.label, label)
428
+
429
+ def getStateLabel(self) -> StateLabel:
430
+ """Return state label."""
431
+ return self._mmc.getStateLabel(self.label)
432
+
433
+ def defineStateLabel(self, state: int, label: str) -> None:
434
+ """Define state labels."""
435
+ self._mmc.defineStateLabel(self.label, state, label)
436
+
437
+ def getStateLabels(self) -> tuple[StateLabel, ...]:
438
+ """Return state labels."""
439
+ return self._mmc.getStateLabels(self.label)
440
+
441
+ def getStateFromLabel(self, label: str) -> int:
442
+ """Return state for given label."""
443
+ return self._mmc.getStateFromLabel(self.label, label)
444
+
445
+
446
+ class _StageBase(Device):
447
+ def stop(self) -> None:
448
+ """Stop XY stage movement."""
449
+ self._mmc.stop(self.label)
450
+
451
+ def home(self) -> None:
452
+ """Home XY stage."""
453
+ self._mmc.home(self.label)
454
+
455
+
456
+ class StageDevice(_StageBase):
457
+ def type(self) -> Literal[DeviceType.Stage]:
458
+ return DeviceType.Stage
459
+
460
+ def setPosition(self, position: float) -> None:
461
+ self._mmc.setPosition(self.label, position)
462
+
463
+ def getPosition(self) -> float:
464
+ return self._mmc.getPosition(self.label)
465
+
466
+ @property
467
+ def position(self) -> float:
468
+ return self.getPosition()
469
+
470
+ @position.setter
471
+ def position(self, value: float) -> None:
472
+ self.setPosition(value)
473
+
474
+ def setRelativePosition(self, offset: float) -> None:
475
+ self._mmc.setRelativePosition(self.label, offset)
476
+
477
+ def getPositionAccumulator(self) -> PositionChangeAccumulator:
478
+ from pymmcore_plus._accumulator import PositionChangeAccumulator
479
+
480
+ return PositionChangeAccumulator.get_cached(self.label, self._mmc)
481
+
482
+ def setOrigin(self) -> None:
483
+ self._mmc.setOrigin(self.label)
484
+
485
+ def setAdapterOrigin(self, newZUm: float) -> None:
486
+ self._mmc.setAdapterOrigin(self.label, newZUm)
487
+
488
+ def setFocusDirection(self, sign: int) -> None:
489
+ self._mmc.setFocusDirection(self.label, sign)
490
+
491
+ def getFocusDirection(self) -> FocusDirection:
492
+ return self._mmc.getFocusDirection(self.label)
493
+
494
+ def isContinuousFocusDrive(self) -> bool:
495
+ """Return `True` if device supports continuous focus."""
496
+ return self._mmc.isContinuousFocusDrive(self.label)
497
+
498
+ def isStageSequenceable(self) -> bool:
499
+ """Return `True` if device supports stage sequence."""
500
+ return self._mmc.isStageSequenceable(self.label)
501
+
502
+ def isStageLinearSequenceable(self) -> bool:
503
+ """Return `True` if device supports linear stage sequence."""
504
+ return self._mmc.isStageLinearSequenceable(self.label)
505
+
506
+ def startStageSequence(self) -> None:
507
+ """Start stage sequence."""
508
+ self._mmc.startStageSequence(self.label)
509
+
510
+ def stopStageSequence(self) -> None:
511
+ """Stop stage sequence."""
512
+ self._mmc.stopStageSequence(self.label)
513
+
514
+ def getStageSequenceMaxLength(self) -> int:
515
+ """Return maximum length of stage sequence."""
516
+ return self._mmc.getStageSequenceMaxLength(self.label)
517
+
518
+ def loadStageSequence(self, positions: Sequence[float]) -> None:
519
+ """Load stage sequence."""
520
+ self._mmc.loadStageSequence(self.label, positions)
521
+
522
+ def setStageLinearSequence(self, dZ_um: float, nSlices: int) -> None:
523
+ """Set stage linear sequence."""
524
+ self._mmc.setStageLinearSequence(self.label, dZ_um, nSlices)
525
+
526
+ isSequenceable = isStageSequenceable
527
+ loadSequence = loadStageSequence
528
+ startSequence = startStageSequence
529
+ stopSequence = stopStageSequence
530
+ getSequenceMaxLength = getStageSequenceMaxLength
531
+
532
+
533
+ class XYStageDevice(_StageBase):
534
+ def type(self) -> Literal[DeviceType.XYStage]:
535
+ return DeviceType.XYStage
536
+
537
+ def setXYPosition(self, x: float, y: float) -> None:
538
+ """Set the position of the XY stage in microns."""
539
+ self._mmc.setXYPosition(self.label, x, y)
540
+
541
+ def getXYPosition(self) -> Sequence[float]:
542
+ """Return the position of the XY stage in microns."""
543
+ return self._mmc.getXYPosition(self.label)
544
+
545
+ @property
546
+ def position(self) -> tuple[float, float]:
547
+ """Return the position of the XY stage in microns."""
548
+ return tuple(self._mmc.getXYPosition(self.label)) # type: ignore [return-value]
549
+
550
+ @position.setter
551
+ def position(self, value: tuple[float, float]) -> None:
552
+ """Set the position of the XY stage in microns."""
553
+ self._mmc.setXYPosition(self.label, *value)
554
+
555
+ def setRelativeXYPosition(self, dx: float, dy: float) -> None:
556
+ """Set the relative position of the XY stage in microns."""
557
+ self._mmc.setRelativeXYPosition(self.label, dx, dy)
558
+
559
+ def getPositionAccumulator(self) -> XYPositionChangeAccumulator:
560
+ from pymmcore_plus._accumulator import XYPositionChangeAccumulator
561
+
562
+ return XYPositionChangeAccumulator.get_cached(self.label, self._mmc)
563
+
564
+ def getXPosition(self) -> float:
565
+ """Return the X position of the XY stage in microns."""
566
+ return self._mmc.getXPosition(self.label)
567
+
568
+ def getYPosition(self) -> float:
569
+ """Return the Y position of the XY stage in microns."""
570
+ return self._mmc.getYPosition(self.label)
571
+
572
+ def setOriginXY(self) -> None:
573
+ """Zero the current XY stage's coordinates at the current position."""
574
+ self._mmc.setOriginXY(self.label)
575
+
576
+ setOrigin = setOriginXY
577
+
578
+ def setOriginX(self) -> None:
579
+ """Zero the given XY stage's X coordinate at the current position."""
580
+ self._mmc.setOriginX(self.label)
581
+
582
+ def setOriginY(self) -> None:
583
+ """Zero the given XY stage's Y coordinate at the current position."""
584
+ self._mmc.setOriginY(self.label)
585
+
586
+ def setAdapterOriginXY(self, newXUm: float, newYUm: float) -> None:
587
+ """Enable software translation of coordinates for the current XY stage.
588
+
589
+ The current position of the stage becomes (newXUm, newYUm). It is recommended
590
+ that setOriginXY() be used instead where available.
591
+ """
592
+ self._mmc.setAdapterOriginXY(self.label, newXUm, newYUm)
593
+
594
+ def isXYStageSequenceable(self) -> bool:
595
+ """Return `True` if device supports XY stage sequence."""
596
+ return self._mmc.isXYStageSequenceable(self.label)
597
+
598
+ def startXYStageSequence(self) -> None:
599
+ """Start XY stage sequence."""
600
+ self._mmc.startXYStageSequence(self.label)
601
+
602
+ def stopXYStageSequence(self) -> None:
603
+ """Stop XY stage sequence."""
604
+ self._mmc.stopXYStageSequence(self.label)
605
+
606
+ def getXYStageSequenceMaxLength(self) -> int:
607
+ """Return maximum length of XY stage sequence."""
608
+ return self._mmc.getXYStageSequenceMaxLength(self.label)
609
+
610
+ def loadXYStageSequence(
611
+ self, xSequence: Sequence[float], ySequence: Sequence[float]
612
+ ) -> None:
613
+ """Load XY stage sequence."""
614
+ self._mmc.loadXYStageSequence(self.label, xSequence, ySequence)
615
+
616
+ def loadSequence(self, sequence: Sequence[tuple[float, float]]) -> None:
617
+ """Load XY stage sequence with a sequence of 2-tuples.
618
+
619
+ Provided as a wrapper for loadXYStageSequence, for API parity with other
620
+ sequencaable devices.
621
+ """
622
+ xSequence, ySequence = zip(*sequence)
623
+ self._mmc.loadXYStageSequence(self.label, xSequence, ySequence)
624
+
625
+ isSequenceable = isXYStageSequenceable
626
+ startSequence = startXYStageSequence
627
+ stopSequence = stopXYStageSequence
628
+ getSequenceMaxLength = getXYStageSequenceMaxLength
629
+
630
+
631
+ class SerialDevice(Device):
632
+ def type(self) -> Literal[DeviceType.Serial]:
633
+ return DeviceType.Serial
634
+
635
+ def setCommand(self, command: str, term: str) -> None:
636
+ """Send string to the serial device and return an answer."""
637
+ self._mmc.setSerialPortCommand(self.label, command, term)
638
+
639
+ def getAnswer(self, term: str) -> str:
640
+ """Continuously read from the serial port until the term is encountered."""
641
+ return self._mmc.getSerialPortAnswer(self.label, term)
642
+
643
+ def write(self, data: bytes) -> None:
644
+ """Send string to the serial device."""
645
+ self._mmc.writeToSerialPort(self.label, data)
646
+
647
+ def read(self) -> list[str]:
648
+ """Reads the contents of the Rx buffer."""
649
+ return self._mmc.readFromSerialPort(self.label)
650
+
651
+ def setProperties(
652
+ self,
653
+ answerTimeout: str,
654
+ baudRate: str,
655
+ delayBetweenCharsMs: str,
656
+ handshaking: str,
657
+ parity: str,
658
+ stopBits: str,
659
+ ) -> None:
660
+ """Sets all com port properties in a single call."""
661
+ self._mmc.setSerialProperties(
662
+ self.label,
663
+ answerTimeout,
664
+ baudRate,
665
+ delayBetweenCharsMs,
666
+ handshaking,
667
+ parity,
668
+ stopBits,
669
+ )
670
+
671
+ @property
672
+ def answer_timeout(self) -> str:
673
+ """Return the timeout for serial port commands."""
674
+ return self._mmc.getProperty(self.label, Keyword.AnswerTimeout)
675
+
676
+ @property
677
+ def baud_rate(self) -> str:
678
+ """Return the baud rate for serial port commands."""
679
+ return self._mmc.getProperty(self.label, Keyword.BaudRate)
680
+
681
+ @property
682
+ def data_bits(self) -> str:
683
+ """Return the data bits for serial port commands."""
684
+ return self._mmc.getProperty(self.label, Keyword.DataBits)
685
+
686
+ @property
687
+ def parity(self) -> str:
688
+ """Return the parity for serial port commands."""
689
+ return self._mmc.getProperty(self.label, Keyword.Parity)
690
+
691
+ @property
692
+ def stop_bits(self) -> str:
693
+ """Return the stop bits for serial port commands."""
694
+ return self._mmc.getProperty(self.label, Keyword.StopBits)
695
+
696
+ @property
697
+ def handshaking(self) -> str:
698
+ """Return the handshaking for serial port commands."""
699
+ return self._mmc.getProperty(self.label, Keyword.Handshaking)
700
+
701
+ @property
702
+ def delay_between_chars_ms(self) -> str:
703
+ """Return the delay between characters in milliseconds."""
704
+ return self._mmc.getProperty(self.label, Keyword.DelayBetweenCharsMs)
705
+
706
+
707
+ class GenericDevice(Device):
708
+ def type(self) -> Literal[DeviceType.Generic]:
709
+ return DeviceType.Generic
710
+
711
+
712
+ class AutoFocusDevice(Device):
713
+ def type(self) -> Literal[DeviceType.AutoFocus]:
714
+ return DeviceType.AutoFocus
715
+
716
+ # none of these actually accept a label, and should be called on Core
717
+ # getLastFocusScore
718
+ # getCurrentFocusScore
719
+ # enableContinuousFocus
720
+ # isContinuousFocusEnabled
721
+ # isContinuousFocusLocked
722
+ # isContinuousFocusDrive
723
+ # fullFocus
724
+ # incrementalFocus
725
+ # setAutoFocusOffset
726
+ # getAutoFocusOffset
727
+
728
+
729
+ class SLMDevice(Device):
730
+ def type(self) -> Literal[DeviceType.SLM]:
731
+ return DeviceType.SLM
732
+
733
+ def setImage(self, pixels: Any) -> None:
734
+ """Write an image to the SLM ."""
735
+ self._mmc.setSLMImage(self.label, pixels)
736
+
737
+ @overload
738
+ def setPixelsTo(self, intensity: int, /) -> None: ...
739
+ @overload
740
+ def setPixelsTo(self, red: int, green: int, blue: int, /) -> None: ...
741
+ def setPixelsTo(self, *args: int) -> None:
742
+ """Set all SLM pixels to a single 8-bit intensity or RGB color."""
743
+ self._mmc.setSLMPixelsTo(self.label, *args)
744
+
745
+ def displayImage(self) -> None:
746
+ """Display the image on the SLM."""
747
+ self._mmc.displaySLMImage(self.label)
748
+
749
+ def setExposure(self, exposure_ms: float) -> None:
750
+ """Set exposure time for SLM."""
751
+ self._mmc.setSLMExposure(self.label, exposure_ms)
752
+
753
+ def getExposure(self) -> float:
754
+ """Return exposure time for SLM."""
755
+ return self._mmc.getSLMExposure(self.label)
756
+
757
+ @property
758
+ def exposure(self) -> float:
759
+ return self.getExposure()
760
+
761
+ @exposure.setter
762
+ def exposure(self, value: float) -> None:
763
+ self.setExposure(value)
764
+
765
+ def width(self) -> int:
766
+ """Return the width of the SLM image."""
767
+ return self._mmc.getSLMWidth(self.label)
768
+
769
+ def height(self) -> int:
770
+ """Return the height of the SLM image."""
771
+ return self._mmc.getSLMHeight(self.label)
772
+
773
+ def numberOfComponents(self) -> int:
774
+ """Return the number of components in the SLM image."""
775
+ return self._mmc.getSLMNumberOfComponents(self.label)
776
+
777
+ def bytesPerPixel(self) -> int:
778
+ """Return the number of bytes per pixel in the SLM image."""
779
+ return self._mmc.getSLMBytesPerPixel(self.label)
780
+
781
+ def getSequenceMaxLength(self) -> int:
782
+ """Return the maximum length of a sequence for the SLM."""
783
+ return self._mmc.getSLMSequenceMaxLength(self.label)
784
+
785
+ def isSequenceable(self) -> bool:
786
+ """Return `True` if the SLM supports sequences."""
787
+ # there is no MMCore API for this
788
+ try:
789
+ return self.getSequenceMaxLength() > 0
790
+ except RuntimeError:
791
+ return False
792
+
793
+ def loadSequence(self, imageSequence: list[bytes]) -> None:
794
+ """Load a sequence of images to the SLM."""
795
+ self._mmc.loadSLMSequence(self.label, imageSequence)
796
+
797
+ def startSequence(self) -> None:
798
+ """Start the sequence of images on the SLM."""
799
+ self._mmc.startSLMSequence(self.label)
800
+
801
+ def stopSequence(self) -> None:
802
+ """Stop the sequence of images on the SLM."""
803
+ self._mmc.stopSLMSequence(self.label)
804
+
805
+
806
+ class HubDevice(Device):
807
+ def type(self) -> Literal[DeviceType.Hub]:
808
+ return DeviceType.Hub
809
+
810
+ def getInstalledDevices(self) -> tuple[str, ...]:
811
+ """Return the list of installed devices."""
812
+ return self._mmc.getInstalledDevices(self.label)
813
+
814
+ def getInstalledDeviceDescription(self, device_label: str) -> str:
815
+ """Return the description of the installed device."""
816
+ return self._mmc.getInstalledDeviceDescription(self.label, device_label)
817
+
818
+ def getLoadedPeripheralDevices(self) -> tuple[str, ...]:
819
+ """Return the list of loaded peripheral devices."""
820
+ return self._mmc.getLoadedPeripheralDevices(self.label)
821
+
822
+
823
+ class GalvoDevice(Device):
824
+ def type(self) -> Literal[DeviceType.Galvo]:
825
+ return DeviceType.Galvo
826
+
827
+ def pointAndFire(self, x: float, y: float, pulseTime_us: float) -> None:
828
+ """Set Galvo to (x, y) and fire the laser for a predetermined duration."""
829
+ self._mmc.pointGalvoAndFire(self.label, x, y, pulseTime_us)
830
+
831
+ def setSpotInterval(self, pulseTime_us: float) -> None:
832
+ """Set the SpotInterval for the specified galvo device."""
833
+ self._mmc.setGalvoSpotInterval(self.label, pulseTime_us)
834
+
835
+ def setPosition(self, x: float, y: float) -> None:
836
+ """Set the position of the galvo device."""
837
+ self._mmc.setGalvoPosition(self.label, x, y)
838
+
839
+ def getPosition(self) -> list[float]:
840
+ """Return the position of the galvo device."""
841
+ return self._mmc.getGalvoPosition(self.label)
842
+
843
+ @property
844
+ def position(self) -> list[float]:
845
+ return self._mmc.getGalvoPosition(self.label)
846
+
847
+ @position.setter
848
+ def position(self, value: tuple[float, float]) -> None:
849
+ self._mmc.setGalvoPosition(self.label, *value)
850
+
851
+ def setIlluminationState(self, state: bool) -> None:
852
+ """Set the galvo's illumination state to on or off."""
853
+ self._mmc.setGalvoIlluminationState(self.label, state)
854
+
855
+ def getXRange(self) -> float:
856
+ """Get the Galvo x range."""
857
+ return self._mmc.getGalvoXRange(self.label)
858
+
859
+ def getXMinimum(self) -> float:
860
+ """Get the Galvo x minimum."""
861
+ return self._mmc.getGalvoXMinimum(self.label)
862
+
863
+ def getYRange(self) -> float:
864
+ """Get the Galvo y range."""
865
+ return self._mmc.getGalvoYRange(self.label)
866
+
867
+ def getYMinimum(self) -> float:
868
+ """Get the Galvo y minimum."""
869
+ return self._mmc.getGalvoYMinimum(self.label)
870
+
871
+ def addPolygonVertex(self, polygonIndex: int, x: float, y: float) -> None:
872
+ """Add a vertex to the polygon."""
873
+ self._mmc.addGalvoPolygonVertex(self.label, polygonIndex, x, y)
874
+
875
+ def deletePolygons(self) -> None:
876
+ """Delete all polygons."""
877
+ self._mmc.deleteGalvoPolygons(self.label)
878
+
879
+ def loadPolygons(self) -> None:
880
+ """Load a set of galvo polygons to the device."""
881
+ self._mmc.loadGalvoPolygons(self.label)
882
+
883
+ def runPolygons(self) -> None:
884
+ """Run a loop of galvo polygons."""
885
+ self._mmc.runGalvoPolygons(self.label)
886
+
887
+ def runSequence(self) -> None:
888
+ """Run a sequence of galvo positions."""
889
+ self._mmc.runGalvoSequence(self.label)
890
+
891
+ def setPolygonRepetitions(self, repetitions: int) -> None:
892
+ """Set the number of times the galvo polygon should be repeated."""
893
+ self._mmc.setGalvoPolygonRepetitions(self.label, repetitions)
894
+
895
+ def getChannel(self) -> str:
896
+ """Get the name of the active galvo channel (for a multi-laser galvo device)."""
897
+ return self._mmc.getGalvoChannel(self.label)
898
+
899
+
900
+ class ImageProcessorDevice(Device):
901
+ def type(self) -> Literal[DeviceType.ImageProcessor]:
902
+ return DeviceType.ImageProcessor
903
+
904
+
905
+ class SignalIODevice(Device):
906
+ def type(self) -> Literal[DeviceType.SignalIO]:
907
+ return DeviceType.SignalIO
908
+
909
+
910
+ class MagnifierDevice(Device):
911
+ def type(self) -> Literal[DeviceType.Magnifier]:
912
+ return DeviceType.Magnifier
913
+
914
+
915
+ # Special device...
916
+ class CoreDevice(Device):
917
+ def type(self) -> Literal[DeviceType.Core]:
918
+ return DeviceType.Core
919
+
920
+
921
+ _TYPE_MAP: dict[DeviceType, type[Device]] = {
922
+ DeviceType.Camera: CameraDevice,
923
+ DeviceType.Shutter: ShutterDevice,
924
+ DeviceType.State: StateDevice,
925
+ DeviceType.Stage: StageDevice,
926
+ DeviceType.XYStage: XYStageDevice,
927
+ DeviceType.Serial: SerialDevice,
928
+ DeviceType.Generic: GenericDevice,
929
+ DeviceType.AutoFocus: AutoFocusDevice,
930
+ DeviceType.Core: CoreDevice,
931
+ DeviceType.ImageProcessor: ImageProcessorDevice,
932
+ DeviceType.SignalIO: SignalIODevice,
933
+ DeviceType.Magnifier: MagnifierDevice,
934
+ DeviceType.SLM: SLMDevice,
935
+ DeviceType.Hub: HubDevice,
936
+ DeviceType.Galvo: GalvoDevice,
937
+ }