pymmcore-plus 0.16.0__py3-none-any.whl → 0.17.1__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.
- pymmcore_plus/_ipy_completion.py +1 -1
- pymmcore_plus/_logger.py +2 -2
- pymmcore_plus/core/_device.py +37 -6
- pymmcore_plus/core/_mmcore_plus.py +4 -15
- pymmcore_plus/core/_property.py +1 -1
- pymmcore_plus/core/_sequencing.py +2 -0
- pymmcore_plus/experimental/simulate/__init__.py +88 -0
- pymmcore_plus/experimental/simulate/_objects.py +670 -0
- pymmcore_plus/experimental/simulate/_render.py +510 -0
- pymmcore_plus/experimental/simulate/_sample.py +156 -0
- pymmcore_plus/experimental/unicore/__init__.py +2 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +26 -0
- pymmcore_plus/experimental/unicore/core/_config.py +706 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +832 -20
- pymmcore_plus/experimental/unicore/devices/_device_base.py +13 -0
- pymmcore_plus/experimental/unicore/devices/_hub.py +50 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +46 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +6 -0
- pymmcore_plus/mda/handlers/_5d_writer_base.py +16 -5
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +7 -1
- pymmcore_plus/metadata/_ome.py +75 -21
- pymmcore_plus/metadata/functions.py +2 -1
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.1.dist-info}/METADATA +5 -3
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.1.dist-info}/RECORD +27 -21
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.1.dist-info}/WHEEL +1 -1
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.1.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ from collections.abc import Iterator, MutableMapping, Sequence
|
|
|
6
6
|
from contextlib import suppress
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from itertools import count
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from time import perf_counter_ns
|
|
10
11
|
from typing import (
|
|
11
12
|
TYPE_CHECKING,
|
|
@@ -20,39 +21,59 @@ from typing import (
|
|
|
20
21
|
import numpy as np
|
|
21
22
|
|
|
22
23
|
import pymmcore_plus._pymmcore as pymmcore
|
|
23
|
-
from pymmcore_plus.core import CMMCorePlus, DeviceType, Keyword
|
|
24
|
+
from pymmcore_plus.core import CMMCorePlus, DeviceType, FocusDirection, Keyword
|
|
24
25
|
from pymmcore_plus.core import Keyword as KW
|
|
26
|
+
from pymmcore_plus.core._config import Configuration
|
|
25
27
|
from pymmcore_plus.core._constants import PixelType
|
|
26
28
|
from pymmcore_plus.experimental.unicore._device_manager import PyDeviceManager
|
|
27
29
|
from pymmcore_plus.experimental.unicore._proxy import create_core_proxy
|
|
28
30
|
from pymmcore_plus.experimental.unicore.devices._camera import CameraDevice
|
|
29
31
|
from pymmcore_plus.experimental.unicore.devices._device_base import Device
|
|
32
|
+
from pymmcore_plus.experimental.unicore.devices._hub import HubDevice
|
|
30
33
|
from pymmcore_plus.experimental.unicore.devices._shutter import ShutterDevice
|
|
31
34
|
from pymmcore_plus.experimental.unicore.devices._slm import SLMDevice
|
|
32
|
-
from pymmcore_plus.experimental.unicore.devices._stage import
|
|
35
|
+
from pymmcore_plus.experimental.unicore.devices._stage import (
|
|
36
|
+
StageDevice,
|
|
37
|
+
XYStageDevice,
|
|
38
|
+
_BaseStage,
|
|
39
|
+
)
|
|
33
40
|
from pymmcore_plus.experimental.unicore.devices._state import StateDevice
|
|
34
41
|
|
|
42
|
+
from ._config import load_system_configuration, save_system_configuration
|
|
35
43
|
from ._sequence_buffer import SequenceBuffer
|
|
36
44
|
|
|
37
45
|
if TYPE_CHECKING:
|
|
38
|
-
from collections.abc import Mapping, Sequence
|
|
46
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
39
47
|
from typing import Literal, NewType
|
|
40
48
|
|
|
41
49
|
from numpy.typing import DTypeLike
|
|
42
50
|
from pymmcore import (
|
|
43
51
|
AdapterName,
|
|
44
52
|
AffineTuple,
|
|
53
|
+
ConfigPresetName,
|
|
45
54
|
DeviceLabel,
|
|
46
55
|
DeviceName,
|
|
47
56
|
PropertyName,
|
|
48
57
|
StateLabel,
|
|
49
58
|
)
|
|
59
|
+
from typing_extensions import TypeAlias
|
|
50
60
|
|
|
51
61
|
from pymmcore_plus.core._constants import DeviceInitializationState, PropertyType
|
|
52
62
|
|
|
53
63
|
PyDeviceLabel = NewType("PyDeviceLabel", DeviceLabel)
|
|
54
64
|
_T = TypeVar("_T")
|
|
55
65
|
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Config Group Type Aliases
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
DevPropTuple = tuple[str, str]
|
|
71
|
+
ConfigDict: TypeAlias = "MutableMapping[DevPropTuple, Any]"
|
|
72
|
+
ConfigGroup: TypeAlias = "MutableMapping[ConfigPresetName, ConfigDict]"
|
|
73
|
+
# technically the keys are ConfigGroupName (a NewType from pymmcore)
|
|
74
|
+
# but to avoid all the casting, we use str here
|
|
75
|
+
ConfigGroups: TypeAlias = MutableMapping[str, ConfigGroup]
|
|
76
|
+
|
|
56
77
|
|
|
57
78
|
class BufferOverflowStop(Exception):
|
|
58
79
|
"""Exception raised to signal graceful stop on buffer overflow."""
|
|
@@ -78,7 +99,7 @@ class _CoreDevice:
|
|
|
78
99
|
determine which real device to use.
|
|
79
100
|
"""
|
|
80
101
|
|
|
81
|
-
def __init__(self, state_cache:
|
|
102
|
+
def __init__(self, state_cache: ThreadSafeConfig) -> None:
|
|
82
103
|
self._state_cache = state_cache
|
|
83
104
|
self._pycurrent: dict[Keyword, PyDeviceLabel | None] = {}
|
|
84
105
|
self.reset_current()
|
|
@@ -102,11 +123,15 @@ class UniMMCore(CMMCorePlus):
|
|
|
102
123
|
|
|
103
124
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
104
125
|
self._pydevices = PyDeviceManager() # manager for python devices
|
|
105
|
-
self._state_cache =
|
|
126
|
+
self._state_cache = ThreadSafeConfig() # threadsafe cache for property states
|
|
106
127
|
self._pycore = _CoreDevice(self._state_cache) # virtual core for python
|
|
107
128
|
self._stop_event: threading.Event = threading.Event()
|
|
108
129
|
self._acquisition_thread: AcquisitionThread | None = None # TODO: implement
|
|
109
130
|
self._seq_buffer = SequenceBuffer(size_mb=_DEFAULT_BUFFER_SIZE_MB)
|
|
131
|
+
# Storage for Python device settings in config groups.
|
|
132
|
+
# Groups and presets are ALWAYS also created in C++ (via super() calls).
|
|
133
|
+
# This dict only stores (device, property) -> value for Python devices.
|
|
134
|
+
self._py_config_groups: ConfigGroups = {}
|
|
110
135
|
|
|
111
136
|
super().__init__(*args, **kwargs)
|
|
112
137
|
|
|
@@ -136,7 +161,78 @@ class UniMMCore(CMMCorePlus):
|
|
|
136
161
|
super().waitForDeviceType(DeviceType.AnyType)
|
|
137
162
|
self.unloadAllDevices()
|
|
138
163
|
self._pycore.reset_current()
|
|
139
|
-
|
|
164
|
+
self._py_config_groups.clear() # Clear Python device config settings
|
|
165
|
+
super().reset() # Clears C++ config groups and channel group
|
|
166
|
+
|
|
167
|
+
def loadSystemConfiguration(
|
|
168
|
+
self, fileName: str | Path = "MMConfig_demo.cfg"
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Load a system config file conforming to the MM `.cfg` format.
|
|
171
|
+
|
|
172
|
+
This is a Python implementation that supports both C++ and Python devices.
|
|
173
|
+
Lines prefixed with `#py ` are processed as Python device commands but
|
|
174
|
+
are ignored by upstream C++/pymmcore implementations.
|
|
175
|
+
|
|
176
|
+
Format example::
|
|
177
|
+
|
|
178
|
+
# C++ devices
|
|
179
|
+
Device, Camera, DemoCamera, DCam
|
|
180
|
+
Property, Core, Initialize, 1
|
|
181
|
+
|
|
182
|
+
# Python devices (hidden from upstream via comment prefix)
|
|
183
|
+
# py pyDevice,PyCamera,mypackage.cameras,MyCameraClass
|
|
184
|
+
# py Property,PyCamera,Exposure,50.0
|
|
185
|
+
|
|
186
|
+
https://micro-manager.org/Micro-Manager_Configuration_Guide#configuration-file-syntax
|
|
187
|
+
|
|
188
|
+
For relative paths, the current working directory is first checked, then
|
|
189
|
+
the device adapter path is checked.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
fileName : str | Path
|
|
194
|
+
Path to the configuration file. Defaults to "MMConfig_demo.cfg".
|
|
195
|
+
"""
|
|
196
|
+
fpath = Path(fileName).expanduser()
|
|
197
|
+
if not fpath.exists() and not fpath.is_absolute() and self._mm_path:
|
|
198
|
+
fpath = Path(self._mm_path) / fileName
|
|
199
|
+
if not fpath.exists():
|
|
200
|
+
raise FileNotFoundError(f"Path does not exist: {fpath}")
|
|
201
|
+
|
|
202
|
+
cfg_path = str(fpath.resolve())
|
|
203
|
+
try:
|
|
204
|
+
load_system_configuration(self, cfg_path)
|
|
205
|
+
except Exception:
|
|
206
|
+
# On failure, unload all devices to avoid leaving loaded but
|
|
207
|
+
# uninitialized devices that could cause crashes
|
|
208
|
+
with suppress(Exception):
|
|
209
|
+
self.unloadAllDevices()
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
self._last_sys_config = cfg_path
|
|
213
|
+
# Emit system configuration loaded event
|
|
214
|
+
self.events.systemConfigurationLoaded.emit()
|
|
215
|
+
|
|
216
|
+
def saveSystemConfiguration(
|
|
217
|
+
self, filename: str | Path, *, prefix_py_devices: bool = True
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Save the current system configuration to a text file.
|
|
220
|
+
|
|
221
|
+
This saves both C++ and Python devices. Python device lines are prefixed
|
|
222
|
+
with `#py ` by default so they are ignored by upstream C++/pymmcore.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
filename : str | Path
|
|
227
|
+
Path to save the configuration file.
|
|
228
|
+
prefix_py_devices : bool, optional
|
|
229
|
+
If True (default), Python device lines are prefixed with `#py ` so
|
|
230
|
+
they are ignored by upstream C++/pymmcore implementations, allowing
|
|
231
|
+
config files to work with regular pymmcore. If False, Python device
|
|
232
|
+
lines are saved without the prefix (config will only be loadable by
|
|
233
|
+
UniMMCore).
|
|
234
|
+
"""
|
|
235
|
+
save_system_configuration(self, filename, prefix_py_devices=prefix_py_devices)
|
|
140
236
|
|
|
141
237
|
# ------------------------------------------------------------------------
|
|
142
238
|
# ----------------- Functionality for All Devices ------------------------
|
|
@@ -163,11 +259,11 @@ class UniMMCore(CMMCorePlus):
|
|
|
163
259
|
try:
|
|
164
260
|
CMMCorePlus.loadDevice(self, label, moduleName, deviceName)
|
|
165
261
|
except RuntimeError as e:
|
|
166
|
-
# it was a C++ device, should have worked ... raise the error
|
|
167
262
|
if moduleName not in super().getDeviceAdapterNames():
|
|
168
263
|
pydev = self._get_py_device_instance(moduleName, deviceName)
|
|
169
264
|
self.loadPyDevice(label, pydev)
|
|
170
265
|
return
|
|
266
|
+
# it was a C++ device, should have worked ... raise the error
|
|
171
267
|
if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
|
|
172
268
|
raise exc from e
|
|
173
269
|
|
|
@@ -212,6 +308,10 @@ class UniMMCore(CMMCorePlus):
|
|
|
212
308
|
|
|
213
309
|
load_py_device = loadPyDevice
|
|
214
310
|
|
|
311
|
+
def isPyDevice(self, label: DeviceLabel | str) -> bool:
|
|
312
|
+
"""Returns True if the specified device label corresponds to a Python device."""
|
|
313
|
+
return label in self._pydevices
|
|
314
|
+
|
|
215
315
|
def unloadDevice(self, label: DeviceLabel | str) -> None:
|
|
216
316
|
if label not in self._pydevices: # pragma: no cover
|
|
217
317
|
return super().unloadDevice(label)
|
|
@@ -240,7 +340,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
240
340
|
|
|
241
341
|
def getLoadedDevicesOfType(self, devType: int) -> tuple[DeviceLabel, ...]:
|
|
242
342
|
pydevs = self._pydevices.get_labels_of_type(devType)
|
|
243
|
-
return pydevs + super().getLoadedDevicesOfType(devType)
|
|
343
|
+
return pydevs + tuple(super().getLoadedDevicesOfType(devType))
|
|
244
344
|
|
|
245
345
|
def getDeviceType(self, label: str) -> DeviceType:
|
|
246
346
|
if label not in self._pydevices: # pragma: no cover
|
|
@@ -262,6 +362,67 @@ class UniMMCore(CMMCorePlus):
|
|
|
262
362
|
return super().getDeviceDescription(label)
|
|
263
363
|
return self._pydevices[label].description()
|
|
264
364
|
|
|
365
|
+
# ---------------------------- Parent/Hub Relationships ---------------------------
|
|
366
|
+
|
|
367
|
+
def getParentLabel(
|
|
368
|
+
self, peripheralLabel: DeviceLabel | str
|
|
369
|
+
) -> DeviceLabel | Literal[""]:
|
|
370
|
+
if peripheralLabel not in self._pydevices: # pragma: no cover
|
|
371
|
+
return super().getParentLabel(peripheralLabel)
|
|
372
|
+
return self._pydevices[peripheralLabel].get_parent_label() # type: ignore[return-value]
|
|
373
|
+
|
|
374
|
+
def setParentLabel(
|
|
375
|
+
self, deviceLabel: DeviceLabel | str, parentHubLabel: DeviceLabel | str
|
|
376
|
+
) -> None:
|
|
377
|
+
if deviceLabel == KW.CoreDevice:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Reject cross-language hub/peripheral relationships
|
|
381
|
+
device_is_py = deviceLabel in self._pydevices
|
|
382
|
+
parent_is_py = parentHubLabel in self._pydevices
|
|
383
|
+
if parentHubLabel and device_is_py != parent_is_py:
|
|
384
|
+
raise RuntimeError( # pragma: no cover
|
|
385
|
+
"Cannot set cross-language parent/child relationship between C++ and "
|
|
386
|
+
"Python devices"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if device_is_py:
|
|
390
|
+
self._pydevices[deviceLabel].set_parent_label(parentHubLabel)
|
|
391
|
+
else:
|
|
392
|
+
super().setParentLabel(deviceLabel, parentHubLabel)
|
|
393
|
+
|
|
394
|
+
def getInstalledDevices(
|
|
395
|
+
self, hubLabel: DeviceLabel | str
|
|
396
|
+
) -> tuple[DeviceName, ...]:
|
|
397
|
+
if hubLabel not in self._pydevices: # pragma: no cover
|
|
398
|
+
return tuple(super().getInstalledDevices(hubLabel))
|
|
399
|
+
|
|
400
|
+
with self._pydevices.get_device_of_type(hubLabel, HubDevice) as hub:
|
|
401
|
+
peripherals = hub.get_installed_peripherals()
|
|
402
|
+
return tuple(p[0] for p in peripherals if p[0]) # type: ignore[misc]
|
|
403
|
+
|
|
404
|
+
def getLoadedPeripheralDevices(
|
|
405
|
+
self, hubLabel: DeviceLabel | str
|
|
406
|
+
) -> tuple[DeviceLabel, ...]:
|
|
407
|
+
cpp_peripherals = super().getLoadedPeripheralDevices(hubLabel)
|
|
408
|
+
py_peripherals = self._pydevices.get_loaded_peripherals(hubLabel)
|
|
409
|
+
return tuple(cpp_peripherals) + py_peripherals
|
|
410
|
+
|
|
411
|
+
def getInstalledDeviceDescription(
|
|
412
|
+
self, hubLabel: DeviceLabel | str, peripheralLabel: DeviceName | str
|
|
413
|
+
) -> str:
|
|
414
|
+
if hubLabel not in self._pydevices:
|
|
415
|
+
return super().getInstalledDeviceDescription(hubLabel, peripheralLabel)
|
|
416
|
+
|
|
417
|
+
with self._pydevices.get_device_of_type(hubLabel, HubDevice) as hub:
|
|
418
|
+
for p in hub.get_installed_peripherals():
|
|
419
|
+
if p[0] == peripheralLabel:
|
|
420
|
+
return p[1] or "N/A"
|
|
421
|
+
raise RuntimeError( # pragma: no cover
|
|
422
|
+
f"No peripheral with name {peripheralLabel!r} installed in hub "
|
|
423
|
+
f"{hubLabel!r}"
|
|
424
|
+
)
|
|
425
|
+
|
|
265
426
|
# ---------------------------- Properties ---------------------------
|
|
266
427
|
|
|
267
428
|
def getDevicePropertyNames(
|
|
@@ -299,6 +460,12 @@ class UniMMCore(CMMCorePlus):
|
|
|
299
460
|
def setProperty(
|
|
300
461
|
self, label: str, propName: str, propValue: bool | float | int | str
|
|
301
462
|
) -> None:
|
|
463
|
+
# FIXME:
|
|
464
|
+
# this single case is probably just the tip of the iceberg when label is "Core"
|
|
465
|
+
if label == KW.CoreDevice and propName == KW.CoreChannelGroup:
|
|
466
|
+
self.setChannelGroup(str(propValue))
|
|
467
|
+
return
|
|
468
|
+
|
|
302
469
|
if label not in self._pydevices: # pragma: no cover
|
|
303
470
|
return super().setProperty(label, propName, propValue)
|
|
304
471
|
with self._pydevices[label] as dev:
|
|
@@ -418,7 +585,21 @@ class UniMMCore(CMMCorePlus):
|
|
|
418
585
|
return super().waitForDevice(label)
|
|
419
586
|
self._pydevices.wait_for(label, self.getTimeoutMs())
|
|
420
587
|
|
|
421
|
-
|
|
588
|
+
def waitForConfig(self, group: str, configName: str) -> None:
|
|
589
|
+
# Get config data (merged from C++ and Python)
|
|
590
|
+
cfg = self.getConfigData(group, configName, native=True)
|
|
591
|
+
|
|
592
|
+
# Wait for each unique device in the config
|
|
593
|
+
devs_to_await: set[str] = set()
|
|
594
|
+
for i in range(cfg.size()):
|
|
595
|
+
devs_to_await.add(cfg.getSetting(i).getDeviceLabel())
|
|
596
|
+
|
|
597
|
+
for device in devs_to_await:
|
|
598
|
+
try:
|
|
599
|
+
self.waitForDevice(device)
|
|
600
|
+
except Exception:
|
|
601
|
+
# Like C++, trap exceptions and keep quiet
|
|
602
|
+
pass
|
|
422
603
|
|
|
423
604
|
# probably only needed because C++ method is not virtual
|
|
424
605
|
def systemBusy(self) -> bool:
|
|
@@ -656,6 +837,192 @@ class UniMMCore(CMMCorePlus):
|
|
|
656
837
|
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
657
838
|
dev.stop_sequence()
|
|
658
839
|
|
|
840
|
+
# ########################################################################
|
|
841
|
+
# ----------------------------- StageDevice ------------------------------
|
|
842
|
+
# ########################################################################
|
|
843
|
+
|
|
844
|
+
def getFocusDevice(self) -> DeviceLabel | Literal[""]:
|
|
845
|
+
"""Return the current Focus Device."""
|
|
846
|
+
return self._pycore.current(KW.CoreFocus) or super().getFocusDevice()
|
|
847
|
+
|
|
848
|
+
def setFocusDevice(self, focusLabel: str) -> None:
|
|
849
|
+
"""Set new current Focus Device."""
|
|
850
|
+
try:
|
|
851
|
+
super().setFocusDevice(focusLabel)
|
|
852
|
+
except Exception:
|
|
853
|
+
# python device
|
|
854
|
+
if focusLabel in self._pydevices:
|
|
855
|
+
if self.getDeviceType(focusLabel) == DeviceType.StageDevice:
|
|
856
|
+
# assign focus device
|
|
857
|
+
label = self._set_current_if_pydevice(KW.CoreFocus, focusLabel)
|
|
858
|
+
super().setFocusDevice(label)
|
|
859
|
+
# otherwise do nothing
|
|
860
|
+
|
|
861
|
+
@overload
|
|
862
|
+
def getPosition(self) -> float: ...
|
|
863
|
+
@overload
|
|
864
|
+
def getPosition(self, stageLabel: str) -> float: ...
|
|
865
|
+
def getPosition(self, stageLabel: str | None = None) -> float:
|
|
866
|
+
label = stageLabel or self.getFocusDevice()
|
|
867
|
+
if label not in self._pydevices:
|
|
868
|
+
return super().getPosition(label)
|
|
869
|
+
with self._pydevices.get_device_of_type(label, StageDevice) as device:
|
|
870
|
+
return device.get_position_um()
|
|
871
|
+
|
|
872
|
+
@overload
|
|
873
|
+
def setPosition(self, position: float, /) -> None: ...
|
|
874
|
+
@overload
|
|
875
|
+
def setPosition(
|
|
876
|
+
self, stageLabel: DeviceLabel | str, position: float, /
|
|
877
|
+
) -> None: ...
|
|
878
|
+
def setPosition(self, *args: Any) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
879
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getFocusDevice)
|
|
880
|
+
if label not in self._pydevices: # pragma: no cover
|
|
881
|
+
return super().setPosition(label, *args)
|
|
882
|
+
with self._pydevices.get_device_of_type(label, StageDevice) as dev:
|
|
883
|
+
dev.set_position_um(*args)
|
|
884
|
+
|
|
885
|
+
def setFocusDirection(self, stageLabel: DeviceLabel | str, sign: int) -> None:
|
|
886
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
887
|
+
return super().setFocusDirection(stageLabel, sign)
|
|
888
|
+
with self._pydevices.get_device_of_type(stageLabel, StageDevice) as device:
|
|
889
|
+
device.set_focus_direction(sign)
|
|
890
|
+
|
|
891
|
+
def getFocusDirection(self, stageLabel: DeviceLabel | str) -> FocusDirection:
|
|
892
|
+
"""Get the current focus direction of the Z stage."""
|
|
893
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
894
|
+
return super().getFocusDirection(stageLabel)
|
|
895
|
+
with self._pydevices.get_device_of_type(stageLabel, StageDevice) as device:
|
|
896
|
+
return device.get_focus_direction()
|
|
897
|
+
|
|
898
|
+
@overload
|
|
899
|
+
def setOrigin(self) -> None: ...
|
|
900
|
+
@overload
|
|
901
|
+
def setOrigin(self, stageLabel: DeviceLabel | str) -> None: ...
|
|
902
|
+
def setOrigin(self, stageLabel: DeviceLabel | str | None = None) -> None:
|
|
903
|
+
"""Zero the current focus/Z stage's coordinates at the current position."""
|
|
904
|
+
label = stageLabel or self.getFocusDevice()
|
|
905
|
+
if label not in self._pydevices: # pragma: no cover
|
|
906
|
+
return super().setOrigin(label)
|
|
907
|
+
with self._pydevices.get_device_of_type(label, StageDevice) as device:
|
|
908
|
+
device.set_origin()
|
|
909
|
+
|
|
910
|
+
@overload
|
|
911
|
+
def setRelativePosition(self, d: float, /) -> None: ...
|
|
912
|
+
@overload
|
|
913
|
+
def setRelativePosition(
|
|
914
|
+
self, stageLabel: DeviceLabel | str, d: float, /
|
|
915
|
+
) -> None: ...
|
|
916
|
+
def setRelativePosition(self, *args: Any) -> None:
|
|
917
|
+
"""Sets the relative position of the stage in microns."""
|
|
918
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getFocusDevice)
|
|
919
|
+
if label not in self._pydevices: # pragma: no cover
|
|
920
|
+
return super().setRelativePosition(label, *args)
|
|
921
|
+
with self._pydevices.get_device_of_type(label, StageDevice) as dev:
|
|
922
|
+
dev.set_relative_position_um(*args)
|
|
923
|
+
|
|
924
|
+
@overload
|
|
925
|
+
def setAdapterOrigin(self, newZUm: float, /) -> None: ...
|
|
926
|
+
@overload
|
|
927
|
+
def setAdapterOrigin(
|
|
928
|
+
self, stageLabel: DeviceLabel | str, newZUm: float, /
|
|
929
|
+
) -> None: ...
|
|
930
|
+
def setAdapterOrigin(self, *args: Any) -> None:
|
|
931
|
+
"""Enable software translation of coordinates for the current focus/Z stage.
|
|
932
|
+
|
|
933
|
+
The current position of the stage becomes Z = newZUm. Only some stages
|
|
934
|
+
support this functionality; it is recommended that setOrigin() be used
|
|
935
|
+
instead where available.
|
|
936
|
+
"""
|
|
937
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getFocusDevice)
|
|
938
|
+
if label not in self._pydevices: # pragma: no cover
|
|
939
|
+
return super().setAdapterOrigin(label, *args)
|
|
940
|
+
with self._pydevices.get_device_of_type(label, StageDevice) as dev:
|
|
941
|
+
dev.set_adapter_origin_um(*args)
|
|
942
|
+
|
|
943
|
+
def isStageSequenceable(self, stageLabel: DeviceLabel | str) -> bool:
|
|
944
|
+
"""Queries stage if it can be used in a sequence."""
|
|
945
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
946
|
+
return super().isStageSequenceable(stageLabel)
|
|
947
|
+
dev = self._pydevices.get_device_of_type(stageLabel, StageDevice)
|
|
948
|
+
return dev.is_sequenceable()
|
|
949
|
+
|
|
950
|
+
def isStageLinearSequenceable(self, stageLabel: DeviceLabel | str) -> bool:
|
|
951
|
+
"""Queries if the stage can be used in a linear sequence.
|
|
952
|
+
|
|
953
|
+
A linear sequence is defined by a step size and number of slices.
|
|
954
|
+
"""
|
|
955
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
956
|
+
return super().isStageLinearSequenceable(stageLabel)
|
|
957
|
+
dev = self._pydevices.get_device_of_type(stageLabel, StageDevice)
|
|
958
|
+
return dev.is_linear_sequenceable()
|
|
959
|
+
|
|
960
|
+
def getStageSequenceMaxLength(self, stageLabel: DeviceLabel | str) -> int:
|
|
961
|
+
"""Gets the maximum length of a stage's position sequence."""
|
|
962
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
963
|
+
return super().getStageSequenceMaxLength(stageLabel)
|
|
964
|
+
dev = self._pydevices.get_device_of_type(stageLabel, StageDevice)
|
|
965
|
+
return dev.get_sequence_max_length()
|
|
966
|
+
|
|
967
|
+
def loadStageSequence(
|
|
968
|
+
self,
|
|
969
|
+
stageLabel: DeviceLabel | str,
|
|
970
|
+
positionSequence: Sequence[float],
|
|
971
|
+
) -> None:
|
|
972
|
+
"""Transfer a sequence of stage positions to the stage.
|
|
973
|
+
|
|
974
|
+
This should only be called for stages that are sequenceable.
|
|
975
|
+
"""
|
|
976
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
977
|
+
return super().loadStageSequence(stageLabel, positionSequence)
|
|
978
|
+
dev = self._pydevices.get_device_of_type(stageLabel, StageDevice)
|
|
979
|
+
if len(positionSequence) > dev.get_sequence_max_length():
|
|
980
|
+
raise ValueError(
|
|
981
|
+
f"Sequence is too long. Max length is {dev.get_sequence_max_length()}"
|
|
982
|
+
)
|
|
983
|
+
dev.send_sequence(tuple(positionSequence))
|
|
984
|
+
|
|
985
|
+
def startStageSequence(self, stageLabel: DeviceLabel | str) -> None:
|
|
986
|
+
"""Starts an ongoing sequence of triggered events in a stage.
|
|
987
|
+
|
|
988
|
+
This should only be called for stages that are sequenceable.
|
|
989
|
+
"""
|
|
990
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
991
|
+
return super().startStageSequence(stageLabel)
|
|
992
|
+
with self._pydevices.get_device_of_type(stageLabel, StageDevice) as dev:
|
|
993
|
+
dev.start_sequence()
|
|
994
|
+
|
|
995
|
+
def stopStageSequence(self, stageLabel: DeviceLabel | str) -> None:
|
|
996
|
+
"""Stops an ongoing sequence of triggered events in a stage.
|
|
997
|
+
|
|
998
|
+
This should only be called for stages that are sequenceable.
|
|
999
|
+
"""
|
|
1000
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
1001
|
+
return super().stopStageSequence(stageLabel)
|
|
1002
|
+
with self._pydevices.get_device_of_type(stageLabel, StageDevice) as dev:
|
|
1003
|
+
dev.stop_sequence()
|
|
1004
|
+
|
|
1005
|
+
def setStageLinearSequence(
|
|
1006
|
+
self, stageLabel: DeviceLabel | str, dZ_um: float, nSlices: int
|
|
1007
|
+
) -> None:
|
|
1008
|
+
"""Loads a linear sequence (defined by step size and nr. of steps)."""
|
|
1009
|
+
if nSlices < 0:
|
|
1010
|
+
raise ValueError("Linear sequence cannot have negative length")
|
|
1011
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
1012
|
+
return super().setStageLinearSequence(stageLabel, dZ_um, nSlices)
|
|
1013
|
+
with self._pydevices.get_device_of_type(stageLabel, StageDevice) as dev:
|
|
1014
|
+
dev.set_linear_sequence(dZ_um, nSlices)
|
|
1015
|
+
|
|
1016
|
+
def isContinuousFocusDrive(self, stageLabel: DeviceLabel | str) -> bool:
|
|
1017
|
+
"""Check if a stage has continuous focusing capability.
|
|
1018
|
+
|
|
1019
|
+
Returns True if positions can be set while continuous focus runs.
|
|
1020
|
+
"""
|
|
1021
|
+
if stageLabel not in self._pydevices: # pragma: no cover
|
|
1022
|
+
return super().isContinuousFocusDrive(stageLabel)
|
|
1023
|
+
dev = self._pydevices.get_device_of_type(stageLabel, StageDevice)
|
|
1024
|
+
return dev.is_continuous_focus_drive()
|
|
1025
|
+
|
|
659
1026
|
# -----------------------------------------------------------------------
|
|
660
1027
|
# ---------------------------- Any Stage --------------------------------
|
|
661
1028
|
# -----------------------------------------------------------------------
|
|
@@ -1100,9 +1467,8 @@ class UniMMCore(CMMCorePlus):
|
|
|
1100
1467
|
def getNumberOfCameraChannels(self) -> int:
|
|
1101
1468
|
if self._py_camera() is None: # pragma: no cover
|
|
1102
1469
|
return super().getNumberOfCameraChannels()
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
)
|
|
1470
|
+
|
|
1471
|
+
return 1
|
|
1106
1472
|
|
|
1107
1473
|
def getCameraChannelName(self, channelNr: int) -> str:
|
|
1108
1474
|
"""Get the name of the camera channel."""
|
|
@@ -1663,6 +2029,437 @@ class UniMMCore(CMMCorePlus):
|
|
|
1663
2029
|
with shutter:
|
|
1664
2030
|
shutter.set_open(state)
|
|
1665
2031
|
|
|
2032
|
+
# ########################################################################
|
|
2033
|
+
# -------------------- Configuration Group Methods -----------------------
|
|
2034
|
+
# ########################################################################
|
|
2035
|
+
#
|
|
2036
|
+
# HYBRID PATTERN: Config groups exist in both C++ and Python.
|
|
2037
|
+
# - Groups and presets are ALWAYS created in C++ (via super())
|
|
2038
|
+
# - _config_groups only stores settings for PYTHON devices
|
|
2039
|
+
# - Methods merge results from both systems where appropriate
|
|
2040
|
+
#
|
|
2041
|
+
# This ensures:
|
|
2042
|
+
# - C++ CoreCallback can find configs and emit proper events
|
|
2043
|
+
# - defineStateLabel in C++ can update configs referencing C++ devices
|
|
2044
|
+
# - loadSystemConfiguration works correctly
|
|
2045
|
+
# - Python device settings are properly tracked and applied
|
|
2046
|
+
|
|
2047
|
+
# -------------------------------------------------------------------------
|
|
2048
|
+
# Group-level operations
|
|
2049
|
+
# -------------------------------------------------------------------------
|
|
2050
|
+
|
|
2051
|
+
def defineConfigGroup(self, groupName: str) -> None:
|
|
2052
|
+
# Create group in C++ (handles validation and events)
|
|
2053
|
+
super().defineConfigGroup(groupName)
|
|
2054
|
+
# Also create empty group in Python for potential Python device settings
|
|
2055
|
+
self._py_config_groups[groupName] = {}
|
|
2056
|
+
|
|
2057
|
+
def deleteConfigGroup(self, groupName: str) -> None:
|
|
2058
|
+
# Delete from C++ (handles validation and events)
|
|
2059
|
+
super().deleteConfigGroup(groupName)
|
|
2060
|
+
self._py_config_groups.pop(groupName, None)
|
|
2061
|
+
|
|
2062
|
+
def renameConfigGroup(self, oldGroupName: str, newGroupName: str) -> None:
|
|
2063
|
+
# Rename in C++ (handles validation)
|
|
2064
|
+
super().renameConfigGroup(oldGroupName, newGroupName)
|
|
2065
|
+
if oldGroupName in self._py_config_groups:
|
|
2066
|
+
self._py_config_groups[newGroupName] = self._py_config_groups.pop(
|
|
2067
|
+
oldGroupName
|
|
2068
|
+
)
|
|
2069
|
+
|
|
2070
|
+
# -------------------------------------------------------------------------
|
|
2071
|
+
# Preset-level operations
|
|
2072
|
+
# -------------------------------------------------------------------------
|
|
2073
|
+
|
|
2074
|
+
@overload
|
|
2075
|
+
def defineConfig(self, groupName: str, configName: str) -> None: ...
|
|
2076
|
+
@overload
|
|
2077
|
+
def defineConfig(
|
|
2078
|
+
self,
|
|
2079
|
+
groupName: str,
|
|
2080
|
+
configName: str,
|
|
2081
|
+
deviceLabel: str,
|
|
2082
|
+
propName: str,
|
|
2083
|
+
value: Any,
|
|
2084
|
+
) -> None: ...
|
|
2085
|
+
def defineConfig(
|
|
2086
|
+
self,
|
|
2087
|
+
groupName: str,
|
|
2088
|
+
configName: str,
|
|
2089
|
+
deviceLabel: str | None = None,
|
|
2090
|
+
propName: str | None = None,
|
|
2091
|
+
value: Any | None = None,
|
|
2092
|
+
) -> None:
|
|
2093
|
+
# Route to appropriate storage based on device type
|
|
2094
|
+
if deviceLabel is None or propName is None or value is None:
|
|
2095
|
+
# No device specified: just create empty group/preset in C++
|
|
2096
|
+
super().defineConfig(groupName, configName)
|
|
2097
|
+
# Also ensure Python storage has the structure
|
|
2098
|
+
group = self._py_config_groups.setdefault(groupName, {})
|
|
2099
|
+
group.setdefault(cast("ConfigPresetName", configName), {})
|
|
2100
|
+
|
|
2101
|
+
elif deviceLabel in self._pydevices:
|
|
2102
|
+
# Python device: store in our _config_groups
|
|
2103
|
+
# But first ensure the group/preset exists in C++ too
|
|
2104
|
+
if not super().isGroupDefined(groupName):
|
|
2105
|
+
super().defineConfigGroup(groupName)
|
|
2106
|
+
self._py_config_groups[groupName] = {}
|
|
2107
|
+
if not super().isConfigDefined(groupName, configName):
|
|
2108
|
+
super().defineConfig(groupName, configName)
|
|
2109
|
+
|
|
2110
|
+
# Store Python device setting locally
|
|
2111
|
+
group = self._py_config_groups.setdefault(groupName, {})
|
|
2112
|
+
preset = group.setdefault(cast("ConfigPresetName", configName), {})
|
|
2113
|
+
preset[(deviceLabel, propName)] = value
|
|
2114
|
+
# Emit event (C++ won't emit for Python device settings)
|
|
2115
|
+
self.events.configDefined.emit(
|
|
2116
|
+
groupName, configName, deviceLabel, propName, str(value)
|
|
2117
|
+
)
|
|
2118
|
+
else:
|
|
2119
|
+
# C++ device: let C++ handle it entirely
|
|
2120
|
+
# C++ expects string values, so convert
|
|
2121
|
+
super().defineConfig(
|
|
2122
|
+
groupName, configName, deviceLabel, propName, str(value)
|
|
2123
|
+
)
|
|
2124
|
+
# Ensure our Python storage has the group/preset structure
|
|
2125
|
+
group = self._py_config_groups.setdefault(groupName, {})
|
|
2126
|
+
group.setdefault(cast("ConfigPresetName", configName), {})
|
|
2127
|
+
|
|
2128
|
+
@overload
|
|
2129
|
+
def deleteConfig(self, groupName: str, configName: str) -> None: ...
|
|
2130
|
+
@overload
|
|
2131
|
+
def deleteConfig(
|
|
2132
|
+
self, groupName: str, configName: str, deviceLabel: str, propName: str
|
|
2133
|
+
) -> None: ...
|
|
2134
|
+
def deleteConfig(
|
|
2135
|
+
self,
|
|
2136
|
+
groupName: str,
|
|
2137
|
+
configName: str,
|
|
2138
|
+
deviceLabel: str | None = None,
|
|
2139
|
+
propName: str | None = None,
|
|
2140
|
+
) -> None:
|
|
2141
|
+
if deviceLabel is None or propName is None:
|
|
2142
|
+
# Deleting entire preset: delete from both C++ and Python storage
|
|
2143
|
+
py_group = self._py_config_groups.get(groupName, {})
|
|
2144
|
+
py_group.pop(configName, None) # type: ignore[call-overload]
|
|
2145
|
+
super().deleteConfig(groupName, configName)
|
|
2146
|
+
|
|
2147
|
+
# Deleting a specific property from a preset
|
|
2148
|
+
elif deviceLabel in self._pydevices:
|
|
2149
|
+
# Python device: remove from our storage
|
|
2150
|
+
py_group = self._py_config_groups.get(groupName, {})
|
|
2151
|
+
py_preset = py_group.get(configName, {}) # type: ignore[call-overload]
|
|
2152
|
+
key = (deviceLabel, propName)
|
|
2153
|
+
if key in py_preset:
|
|
2154
|
+
del py_preset[key]
|
|
2155
|
+
self.events.configDeleted.emit(groupName, configName)
|
|
2156
|
+
else:
|
|
2157
|
+
raise RuntimeError(
|
|
2158
|
+
f"Property '{propName}' not found in preset '{configName}'"
|
|
2159
|
+
)
|
|
2160
|
+
else:
|
|
2161
|
+
# C++ device: let C++ handle it
|
|
2162
|
+
super().deleteConfig(groupName, configName, deviceLabel, propName)
|
|
2163
|
+
|
|
2164
|
+
def renameConfig(
|
|
2165
|
+
self, groupName: str, oldConfigName: str, newConfigName: str
|
|
2166
|
+
) -> None:
|
|
2167
|
+
# Rename in C++ (handles validation)
|
|
2168
|
+
super().renameConfig(groupName, oldConfigName, newConfigName)
|
|
2169
|
+
# Also rename in Python storage if present
|
|
2170
|
+
py_group = self._py_config_groups.get(groupName, {})
|
|
2171
|
+
if oldConfigName in py_group:
|
|
2172
|
+
py_group[newConfigName] = py_group.pop(oldConfigName) # type: ignore
|
|
2173
|
+
|
|
2174
|
+
@overload
|
|
2175
|
+
def getConfigData(
|
|
2176
|
+
self, configGroup: str, configName: str, *, native: Literal[True]
|
|
2177
|
+
) -> pymmcore.Configuration: ...
|
|
2178
|
+
@overload
|
|
2179
|
+
def getConfigData(
|
|
2180
|
+
self, configGroup: str, configName: str, *, native: Literal[False] = False
|
|
2181
|
+
) -> Configuration: ...
|
|
2182
|
+
def getConfigData(
|
|
2183
|
+
self, configGroup: str, configName: str, *, native: bool = False
|
|
2184
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2185
|
+
# Get C++ config data (includes all C++ device settings)
|
|
2186
|
+
cpp_cfg: pymmcore.Configuration = super().getConfigData(
|
|
2187
|
+
configGroup, configName, native=True
|
|
2188
|
+
)
|
|
2189
|
+
|
|
2190
|
+
# Add Python device settings from our storage
|
|
2191
|
+
py_group = self._py_config_groups.get(configGroup, {})
|
|
2192
|
+
py_preset = py_group.get(configName, {}) # type: ignore[call-overload]
|
|
2193
|
+
for (dev, prop), value in py_preset.items():
|
|
2194
|
+
cpp_cfg.addSetting(pymmcore.PropertySetting(dev, prop, str(value)))
|
|
2195
|
+
|
|
2196
|
+
if native:
|
|
2197
|
+
return cpp_cfg
|
|
2198
|
+
return Configuration.from_configuration(cpp_cfg)
|
|
2199
|
+
|
|
2200
|
+
# -------------------------------------------------------------------------
|
|
2201
|
+
# Applying configurations
|
|
2202
|
+
# -------------------------------------------------------------------------
|
|
2203
|
+
|
|
2204
|
+
def setConfig(self, groupName: str, configName: str) -> None:
|
|
2205
|
+
# Apply C++ device settings via super() - this handles validation,
|
|
2206
|
+
# error retry logic, and state cache updates for C++ devices
|
|
2207
|
+
super().setConfig(groupName, configName)
|
|
2208
|
+
|
|
2209
|
+
# Now apply Python device settings from our storage
|
|
2210
|
+
py_group = self._py_config_groups.get(groupName, {})
|
|
2211
|
+
py_preset = py_group.get(configName, {}) # type: ignore[call-overload]
|
|
2212
|
+
|
|
2213
|
+
if py_preset:
|
|
2214
|
+
failed: list[tuple[DevPropTuple, Any]] = []
|
|
2215
|
+
for (device, prop), value in py_preset.items():
|
|
2216
|
+
try:
|
|
2217
|
+
self.setProperty(device, prop, value)
|
|
2218
|
+
except Exception:
|
|
2219
|
+
failed.append(((device, prop), value))
|
|
2220
|
+
|
|
2221
|
+
# Retry failed properties (handles dependency chains)
|
|
2222
|
+
if failed:
|
|
2223
|
+
errors: list[str] = []
|
|
2224
|
+
for (device, prop), value in failed:
|
|
2225
|
+
try:
|
|
2226
|
+
self.setProperty(device, prop, value)
|
|
2227
|
+
except Exception as e:
|
|
2228
|
+
errors.append(f"{device}.{prop}={value}: {e}")
|
|
2229
|
+
if errors:
|
|
2230
|
+
raise RuntimeError("Failed to apply: " + "; ".join(errors))
|
|
2231
|
+
|
|
2232
|
+
# -------------------------------------------------------------------------
|
|
2233
|
+
# Current config detection
|
|
2234
|
+
# -------------------------------------------------------------------------
|
|
2235
|
+
|
|
2236
|
+
def getCurrentConfig(self, groupName: str) -> ConfigPresetName | Literal[""]:
|
|
2237
|
+
return self._getCurrentConfig(groupName, from_cache=False)
|
|
2238
|
+
|
|
2239
|
+
def getCurrentConfigFromCache(
|
|
2240
|
+
self, groupName: str
|
|
2241
|
+
) -> ConfigPresetName | Literal[""]:
|
|
2242
|
+
return self._getCurrentConfig(groupName, from_cache=True)
|
|
2243
|
+
|
|
2244
|
+
def _getCurrentConfig(
|
|
2245
|
+
self, groupName: str, from_cache: bool
|
|
2246
|
+
) -> ConfigPresetName | Literal[""]:
|
|
2247
|
+
"""Find the first preset whose settings all match current device state.
|
|
2248
|
+
|
|
2249
|
+
This checks both C++ device settings (via super()) and Python device settings.
|
|
2250
|
+
"""
|
|
2251
|
+
# Get C++ result first
|
|
2252
|
+
if from_cache:
|
|
2253
|
+
cpp_result = super().getCurrentConfigFromCache(groupName)
|
|
2254
|
+
else:
|
|
2255
|
+
cpp_result = super().getCurrentConfig(groupName)
|
|
2256
|
+
|
|
2257
|
+
# If no Python device settings exist for this group, C++ result is sufficient
|
|
2258
|
+
py_group = self._py_config_groups.get(groupName, {})
|
|
2259
|
+
has_py_settings = any(py_group.values())
|
|
2260
|
+
if not has_py_settings:
|
|
2261
|
+
return cpp_result
|
|
2262
|
+
|
|
2263
|
+
# We have Python device settings - need to verify they match too
|
|
2264
|
+
# Get current state of all Python device properties in this group
|
|
2265
|
+
getter = self.getPropertyFromCache if from_cache else self.getProperty
|
|
2266
|
+
current_py_state: ConfigDict = {}
|
|
2267
|
+
seen_keys: set[DevPropTuple] = set()
|
|
2268
|
+
for preset in py_group.values():
|
|
2269
|
+
for key in preset:
|
|
2270
|
+
if key not in seen_keys:
|
|
2271
|
+
seen_keys.add(key)
|
|
2272
|
+
with suppress(Exception):
|
|
2273
|
+
current_py_state[key] = getter(*key)
|
|
2274
|
+
|
|
2275
|
+
# Check each preset to see if Python device settings match
|
|
2276
|
+
for preset_name in self.getAvailableConfigs(groupName):
|
|
2277
|
+
py_preset = py_group.get(preset_name, {})
|
|
2278
|
+
if all(
|
|
2279
|
+
_values_match(current_py_state.get(k), v) for k, v in py_preset.items()
|
|
2280
|
+
):
|
|
2281
|
+
# Python settings match - but only return if C++ also matches
|
|
2282
|
+
# (or if there are no C++ settings for this preset)
|
|
2283
|
+
cpp_cfg = super().getConfigData(groupName, preset_name, native=True)
|
|
2284
|
+
if cpp_cfg.size() == 0:
|
|
2285
|
+
# No C++ settings, Python match is sufficient
|
|
2286
|
+
return preset_name
|
|
2287
|
+
# Check each C++ setting with numeric-aware comparison
|
|
2288
|
+
# (C++ getCurrentConfig uses strict string comparison which fails
|
|
2289
|
+
# for values like "50.0000" vs "50")
|
|
2290
|
+
all_cpp_match = True
|
|
2291
|
+
for i in range(cpp_cfg.size()):
|
|
2292
|
+
setting = cpp_cfg.getSetting(i)
|
|
2293
|
+
current_val = getter(
|
|
2294
|
+
setting.getDeviceLabel(), setting.getPropertyName()
|
|
2295
|
+
)
|
|
2296
|
+
if not _values_match(current_val, setting.getPropertyValue()):
|
|
2297
|
+
all_cpp_match = False
|
|
2298
|
+
break
|
|
2299
|
+
if all_cpp_match:
|
|
2300
|
+
return preset_name
|
|
2301
|
+
|
|
2302
|
+
return ""
|
|
2303
|
+
|
|
2304
|
+
# -------------------------------------------------------------------------
|
|
2305
|
+
# State queries
|
|
2306
|
+
# -------------------------------------------------------------------------
|
|
2307
|
+
|
|
2308
|
+
def getConfigState(
|
|
2309
|
+
self, group: str, config: str, *, native: bool = False
|
|
2310
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2311
|
+
# Get C++ config state (current values for C++ device properties)
|
|
2312
|
+
cpp_state: pymmcore.Configuration = super().getConfigState(
|
|
2313
|
+
group, config, native=True
|
|
2314
|
+
)
|
|
2315
|
+
|
|
2316
|
+
# Add current values for Python device properties
|
|
2317
|
+
py_group = self._py_config_groups.get(group, {})
|
|
2318
|
+
py_preset = py_group.get(config, {}) # type: ignore[call-overload]
|
|
2319
|
+
for dev, prop in py_preset:
|
|
2320
|
+
current_value = self.getProperty(dev, prop)
|
|
2321
|
+
cpp_state.addSetting(
|
|
2322
|
+
pymmcore.PropertySetting(dev, prop, str(current_value))
|
|
2323
|
+
)
|
|
2324
|
+
|
|
2325
|
+
if native:
|
|
2326
|
+
return cpp_state
|
|
2327
|
+
return Configuration.from_configuration(cpp_state)
|
|
2328
|
+
|
|
2329
|
+
@overload
|
|
2330
|
+
def getConfigGroupState(
|
|
2331
|
+
self, group: str, *, native: Literal[True]
|
|
2332
|
+
) -> pymmcore.Configuration: ...
|
|
2333
|
+
@overload
|
|
2334
|
+
def getConfigGroupState(
|
|
2335
|
+
self, group: str, *, native: Literal[False] = False
|
|
2336
|
+
) -> Configuration: ...
|
|
2337
|
+
def getConfigGroupState(
|
|
2338
|
+
self, group: str, *, native: bool = False
|
|
2339
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2340
|
+
return self._getConfigGroupState(group, from_cache=False, native=native)
|
|
2341
|
+
|
|
2342
|
+
@overload
|
|
2343
|
+
def getConfigGroupStateFromCache(
|
|
2344
|
+
self, group: str, *, native: Literal[True]
|
|
2345
|
+
) -> pymmcore.Configuration: ...
|
|
2346
|
+
@overload
|
|
2347
|
+
def getConfigGroupStateFromCache(
|
|
2348
|
+
self, group: str, *, native: Literal[False] = False
|
|
2349
|
+
) -> Configuration: ...
|
|
2350
|
+
def getConfigGroupStateFromCache(
|
|
2351
|
+
self, group: str, *, native: bool = False
|
|
2352
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2353
|
+
return self._getConfigGroupState(group, from_cache=True, native=native)
|
|
2354
|
+
|
|
2355
|
+
def _getConfigGroupState(
|
|
2356
|
+
self, group: str, from_cache: bool, native: bool = False
|
|
2357
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2358
|
+
"""Get current values for all properties in a group."""
|
|
2359
|
+
# Get C++ group state
|
|
2360
|
+
if from_cache:
|
|
2361
|
+
cpp_state: pymmcore.Configuration = super().getConfigGroupStateFromCache(
|
|
2362
|
+
group, native=True
|
|
2363
|
+
)
|
|
2364
|
+
else:
|
|
2365
|
+
cpp_state = super().getConfigGroupState(group, native=True)
|
|
2366
|
+
|
|
2367
|
+
# Add Python device property values
|
|
2368
|
+
py_group = self._py_config_groups.get(group, {})
|
|
2369
|
+
getter = self.getPropertyFromCache if from_cache else self.getProperty
|
|
2370
|
+
for preset in py_group.values():
|
|
2371
|
+
for device, prop in preset:
|
|
2372
|
+
value = str(getter(device, prop))
|
|
2373
|
+
cpp_state.addSetting(pymmcore.PropertySetting(device, prop, value))
|
|
2374
|
+
|
|
2375
|
+
if native:
|
|
2376
|
+
return cpp_state
|
|
2377
|
+
return Configuration.from_configuration(cpp_state)
|
|
2378
|
+
|
|
2379
|
+
# ########################################################################
|
|
2380
|
+
# ----------------------- System State Methods ---------------------------
|
|
2381
|
+
# ########################################################################
|
|
2382
|
+
|
|
2383
|
+
# currently we still allow C++ to cache the system state for C++ devices,
|
|
2384
|
+
# but we could choose to just own it all ourselves in the future.
|
|
2385
|
+
|
|
2386
|
+
def getSystemState(
|
|
2387
|
+
self, *, native: bool = False
|
|
2388
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2389
|
+
"""Return the entire system state including Python device properties.
|
|
2390
|
+
|
|
2391
|
+
This method iterates through all devices (C++ and Python) and returns
|
|
2392
|
+
all property values. Following the C++ implementation pattern.
|
|
2393
|
+
"""
|
|
2394
|
+
return self._getSystemStateCache(cache=False, native=native)
|
|
2395
|
+
|
|
2396
|
+
@overload
|
|
2397
|
+
def getSystemStateCache(
|
|
2398
|
+
self, *, native: Literal[True]
|
|
2399
|
+
) -> pymmcore.Configuration: ...
|
|
2400
|
+
@overload
|
|
2401
|
+
def getSystemStateCache(
|
|
2402
|
+
self, *, native: Literal[False] = False
|
|
2403
|
+
) -> Configuration: ...
|
|
2404
|
+
def getSystemStateCache(
|
|
2405
|
+
self, *, native: bool = False
|
|
2406
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2407
|
+
return self._getSystemStateCache(cache=True, native=native)
|
|
2408
|
+
|
|
2409
|
+
def _getSystemStateCache(
|
|
2410
|
+
self, cache: bool, native: bool = False
|
|
2411
|
+
) -> Configuration | pymmcore.Configuration:
|
|
2412
|
+
"""Return the entire system state from cache, including Python devices.
|
|
2413
|
+
|
|
2414
|
+
For Python devices, returns cached values from our state cache.
|
|
2415
|
+
Falls back to live values if not in cache.
|
|
2416
|
+
"""
|
|
2417
|
+
# Get the C++ system state cache first
|
|
2418
|
+
if cache:
|
|
2419
|
+
cpp_cfg: pymmcore.Configuration = super().getSystemStateCache(native=True)
|
|
2420
|
+
else:
|
|
2421
|
+
cpp_cfg = super().getSystemState(native=True)
|
|
2422
|
+
|
|
2423
|
+
# Add Python device properties from our cache
|
|
2424
|
+
for label in self._pydevices:
|
|
2425
|
+
with suppress(Exception): # Skip devices that can't be accessed
|
|
2426
|
+
with self._pydevices[label] as dev:
|
|
2427
|
+
for prop_name in dev.get_property_names():
|
|
2428
|
+
with suppress(Exception): # Skip properties that fail
|
|
2429
|
+
key = (label, prop_name)
|
|
2430
|
+
if cache and key in self._state_cache:
|
|
2431
|
+
value = self._state_cache[key]
|
|
2432
|
+
else:
|
|
2433
|
+
value = dev.get_property_value(prop_name)
|
|
2434
|
+
cpp_cfg.addSetting(
|
|
2435
|
+
pymmcore.PropertySetting(
|
|
2436
|
+
label,
|
|
2437
|
+
prop_name,
|
|
2438
|
+
str(value),
|
|
2439
|
+
dev.is_property_read_only(prop_name),
|
|
2440
|
+
)
|
|
2441
|
+
)
|
|
2442
|
+
|
|
2443
|
+
return cpp_cfg if native else Configuration.from_configuration(cpp_cfg)
|
|
2444
|
+
|
|
2445
|
+
def updateSystemStateCache(self) -> None:
|
|
2446
|
+
"""Update the system state cache for all devices including Python devices.
|
|
2447
|
+
|
|
2448
|
+
This populates our Python-side cache with current values from all
|
|
2449
|
+
Python devices, then calls the C++ updateSystemStateCache.
|
|
2450
|
+
"""
|
|
2451
|
+
# Update Python device properties in our cache
|
|
2452
|
+
for label in self._pydevices:
|
|
2453
|
+
with suppress(Exception): # Skip devices that can't be accessed
|
|
2454
|
+
with self._pydevices[label] as dev:
|
|
2455
|
+
for prop_name in dev.get_property_names():
|
|
2456
|
+
with suppress(Exception): # Skip properties that fail
|
|
2457
|
+
value = dev.get_property_value(prop_name)
|
|
2458
|
+
self._state_cache[(label, prop_name)] = value
|
|
2459
|
+
|
|
2460
|
+
# Call C++ updateSystemStateCache
|
|
2461
|
+
super().updateSystemStateCache()
|
|
2462
|
+
|
|
1666
2463
|
|
|
1667
2464
|
# -------------------------------------------------------------------------------
|
|
1668
2465
|
|
|
@@ -1685,7 +2482,7 @@ def _ensure_label(
|
|
|
1685
2482
|
return cast("str", args[0]), args[1:]
|
|
1686
2483
|
|
|
1687
2484
|
|
|
1688
|
-
class
|
|
2485
|
+
class ThreadSafeConfig(MutableMapping["DevPropTuple", Any]):
|
|
1689
2486
|
"""A thread-safe cache for property states.
|
|
1690
2487
|
|
|
1691
2488
|
Keys are tuples of (device_label, property_name), and values are the last known
|
|
@@ -1693,24 +2490,24 @@ class PropertyStateCache(MutableMapping[tuple[str, str], Any]):
|
|
|
1693
2490
|
"""
|
|
1694
2491
|
|
|
1695
2492
|
def __init__(self) -> None:
|
|
1696
|
-
self._store: dict[
|
|
2493
|
+
self._store: dict[DevPropTuple, Any] = {}
|
|
1697
2494
|
self._lock = threading.Lock()
|
|
1698
2495
|
|
|
1699
|
-
def __getitem__(self, key:
|
|
2496
|
+
def __getitem__(self, key: DevPropTuple) -> Any:
|
|
1700
2497
|
with self._lock:
|
|
1701
2498
|
try:
|
|
1702
2499
|
return self._store[key]
|
|
1703
2500
|
except KeyError: # pragma: no cover
|
|
1704
|
-
|
|
2501
|
+
dev, prop = key
|
|
1705
2502
|
raise KeyError(
|
|
1706
2503
|
f"Property {prop!r} of device {dev!r} not found in cache"
|
|
1707
2504
|
) from None
|
|
1708
2505
|
|
|
1709
|
-
def __setitem__(self, key:
|
|
2506
|
+
def __setitem__(self, key: DevPropTuple, value: Any) -> None:
|
|
1710
2507
|
with self._lock:
|
|
1711
2508
|
self._store[key] = value
|
|
1712
2509
|
|
|
1713
|
-
def __delitem__(self, key:
|
|
2510
|
+
def __delitem__(self, key: DevPropTuple) -> None:
|
|
1714
2511
|
with self._lock:
|
|
1715
2512
|
del self._store[key]
|
|
1716
2513
|
|
|
@@ -1718,7 +2515,7 @@ class PropertyStateCache(MutableMapping[tuple[str, str], Any]):
|
|
|
1718
2515
|
with self._lock:
|
|
1719
2516
|
return key in self._store
|
|
1720
2517
|
|
|
1721
|
-
def __iter__(self) -> Iterator[
|
|
2518
|
+
def __iter__(self) -> Iterator[DevPropTuple]:
|
|
1722
2519
|
with self._lock:
|
|
1723
2520
|
return iter(self._store.copy()) # Prevent modifications during iteration
|
|
1724
2521
|
|
|
@@ -1769,4 +2566,19 @@ class AcquisitionThread(threading.Thread):
|
|
|
1769
2566
|
) from e
|
|
1770
2567
|
|
|
1771
2568
|
|
|
1772
|
-
#
|
|
2569
|
+
# --------- helpers -------------------------------------------------------
|
|
2570
|
+
|
|
2571
|
+
|
|
2572
|
+
def _values_match(current: Any, expected: Any) -> bool:
|
|
2573
|
+
"""Compare property values, handling numeric string comparisons.
|
|
2574
|
+
|
|
2575
|
+
Unlike C++ MMCore which does strict string comparison, this performs
|
|
2576
|
+
numeric-aware comparison to handle cases like "50.0000" == 50.
|
|
2577
|
+
"""
|
|
2578
|
+
if current == expected:
|
|
2579
|
+
return True
|
|
2580
|
+
# Try numeric comparison
|
|
2581
|
+
try:
|
|
2582
|
+
return float(current) == float(expected)
|
|
2583
|
+
except (ValueError, TypeError):
|
|
2584
|
+
return str(current) == str(expected)
|