imswitchclient 0.1.8__tar.gz → 0.2.0__tar.gz
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.
- {imswitchclient-0.1.8/imswitchclient.egg-info → imswitchclient-0.2.0}/PKG-INFO +58 -2
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/README.md +57 -1
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/ImSwitchClient.py +6 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/__init__.py +1 -1
- imswitchclient-0.2.0/imswitchclient/autofocusManager.py +51 -0
- imswitchclient-0.2.0/imswitchclient/lasersManager.py +76 -0
- imswitchclient-0.2.0/imswitchclient/liveViewManager.py +38 -0
- imswitchclient-0.2.0/imswitchclient/positionersManager.py +153 -0
- imswitchclient-0.2.0/imswitchclient/recordingManager.py +259 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/settingsManager.py +54 -0
- imswitchclient-0.2.0/imswitchclient/smart.py +362 -0
- imswitchclient-0.2.0/imswitchclient/uc2ConfigManager.py +50 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0/imswitchclient.egg-info}/PKG-INFO +58 -2
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/SOURCES.txt +6 -1
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/setup.cfg +2 -2
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/setup.py +1 -1
- imswitchclient-0.2.0/tests/test_imswitchclient.py +262 -0
- imswitchclient-0.2.0/tests/test_smart.py +213 -0
- imswitchclient-0.1.8/imswitchclient/lasersManager.py +0 -30
- imswitchclient-0.1.8/imswitchclient/positionersManager.py +0 -91
- imswitchclient-0.1.8/imswitchclient/recordingManager.py +0 -142
- imswitchclient-0.1.8/tests/test_imswitchclient.py +0 -21
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/LICENSE +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/MANIFEST.in +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/client.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/communicationManager.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/experimentController.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/histoscanManager.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/ledMatrixManager.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/mdaController.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/objectiveController.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/readnoiseManager.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/socketClient.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/viewManager.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/dependency_links.txt +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/not-zip-safe +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/requires.txt +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/top_level.txt +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/tests/__init__.py +0 -0
- {imswitchclient-0.1.8 → imswitchclient-0.2.0}/tests/test_baseurl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imswitchclient
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: This is a package that connects ImSwitch's REST API to the rest of the world (e.g. jupyter lab)
|
|
5
5
|
Home-page: https://github.com/openuc2/imswitchclient
|
|
6
6
|
Author: Benedict Diederich
|
|
@@ -175,18 +175,30 @@ The ImSwitch API provides access to various components:
|
|
|
175
175
|
- `getAllDeviceNames()` - Get all available positioners.
|
|
176
176
|
- `getPositionerPositions()` - Get current position.
|
|
177
177
|
- `movePositioner(name, axis, value, is_absolute, is_blocking)` - Move the stage.
|
|
178
|
+
- `moveRelativeXY(dx, dy, positioner_name, is_blocking, speed)` - Relative X/Y move in µm.
|
|
179
|
+
- `stop(name, axis)` / `stopAllAxes(name)` - Stop one axis / every axis.
|
|
178
180
|
- `homeAxis(name, axis, is_blocking)` - Home the positioner.
|
|
179
181
|
|
|
180
182
|
### Lasers Manager
|
|
181
183
|
- `getLaserNames()` - Get available lasers.
|
|
182
184
|
- `setLaserActive(name, status)` - Turn laser on/off.
|
|
183
185
|
- `setLaserValue(name, value)` - Set laser intensity.
|
|
186
|
+
- `getLaserValueRanges(name)` / `getLaserValue(name)` / `getLaserActive(name)` - Read back.
|
|
187
|
+
- `setLaserOn(name, value)` / `setLaserOff(name)` - Value and on/off in one call.
|
|
188
|
+
- `runLaserFor(name, value, duration, on_tick, stop_event)` - Run a PWM channel (pump, valve)
|
|
189
|
+
for a fixed time; always switched off afterwards, also on errors or interruption.
|
|
184
190
|
|
|
185
191
|
### Recording Manager
|
|
186
192
|
- `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
|
|
187
193
|
- `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
|
|
188
194
|
- `stopRecording()` - Stop recording.
|
|
189
|
-
- `snapImageToPath(file_name)` - Snap image and save to specified path.
|
|
195
|
+
- `snapImageToPath(file_name, save_format, return_preview)` - Snap image and save to specified path.
|
|
196
|
+
- `snapAndWait(file_name, save_format, poll, timeout, on_progress, stop_event)` - Snap in the
|
|
197
|
+
background on the server and wait for it. Use for long exposures (minutes to hours); an
|
|
198
|
+
interrupt, `stop_event` or timeout cancels the exposure on the server.
|
|
199
|
+
- `startSnap(...)` / `getSnapStatus(job_id)` / `cancelSnap(job_id)` - The same, step by step.
|
|
200
|
+
- `previewToNumpy(result)` - 8-bit preview of a snap result as numpy array.
|
|
201
|
+
- `downloadImage(relative_file_path)` / `downloadFile(...)` - Fetch the saved file (full bit depth).
|
|
190
202
|
- `startVideoStream()` - Start MJPEG video stream.
|
|
191
203
|
- `stopVideoStream()` - Stop MJPEG video stream.
|
|
192
204
|
- `getVideoFrame()` - Get current frame from video stream.
|
|
@@ -199,6 +211,7 @@ The recording manager supports multiple save formats:
|
|
|
199
211
|
- `SaveFormat.MP4` - MP4 video format
|
|
200
212
|
- `SaveFormat.PNG` - PNG format
|
|
201
213
|
- `SaveFormat.JPG` - JPEG format
|
|
214
|
+
- `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
|
|
202
215
|
|
|
203
216
|
### Settings Manager
|
|
204
217
|
- `getDetectorNames()` - Get available detector names.
|
|
@@ -207,6 +220,25 @@ The recording manager supports multiple save formats:
|
|
|
207
220
|
- `setDetectorGain(detector_name, gain)` - Set detector gain.
|
|
208
221
|
- `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
|
|
209
222
|
- `setDetectorROI(detector_name, x, y, w, h)` - Set detector Region of Interest.
|
|
223
|
+
- `getDetectorParameter(parameter_name, detector_name)` - Read one parameter from the camera.
|
|
224
|
+
- `setDetectorMode(detector_name, is_auto)` / `setDetectorExposureOnce(detector_name)` - Auto exposure.
|
|
225
|
+
- `getCameraTemperature(detector_name)` / `setCameraTargetTemperature(temperature_c)` - Cooled cameras.
|
|
226
|
+
- `getDetectorParameterTree(detector_name)` / `setDetectorParameterValue(name, value)` - Typed access
|
|
227
|
+
to every camera parameter.
|
|
228
|
+
|
|
229
|
+
### Live View Manager
|
|
230
|
+
- `startLiveView(detector_name, protocol, params, force)` / `stopLiveView(detector_name)`
|
|
231
|
+
- `getLiveViewActive()`, `getActiveStreams()`, `getLongExposureInfo(detector_name)`
|
|
232
|
+
|
|
233
|
+
### UC2 Config Manager
|
|
234
|
+
- `getBusStatus()` - CAN-bus power and emergency-stop state.
|
|
235
|
+
- `setBusPower(enable)` / `setBusPowerAndWait(enable, settle)` - Switch the power of all CAN
|
|
236
|
+
slave boards (motors, LED/laser boards). Raises if the board refuses.
|
|
237
|
+
- `isBoardConnected()`, `getBoardTemperature()`
|
|
238
|
+
|
|
239
|
+
### Autofocus Manager
|
|
240
|
+
- `autoFocusAndWait(rangez, resolutionz, ...)` - Server-side Z scan around the current focus,
|
|
241
|
+
waits for the result. `autoFocus`, `getAutofocusStatus`, `stopAutofocus` for manual control.
|
|
210
242
|
|
|
211
243
|
### View Manager
|
|
212
244
|
- `setLiveViewActive(active)` - Enable/disable live view.
|
|
@@ -221,6 +253,30 @@ The recording manager supports multiple save formats:
|
|
|
221
253
|
- `setLED(led_id, state)` - Set specific LED with ID and state.
|
|
222
254
|
- `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
|
|
223
255
|
|
|
256
|
+
### Smart microscopy helpers (`imswitchclient.smart`)
|
|
257
|
+
Building blocks for closed-loop experiments; they work on numpy frames and plain callables and are
|
|
258
|
+
tested without a microscope (`tests/test_smart.py`).
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from imswitchclient import smart
|
|
262
|
+
snap = smart.make_snap(client) # -> float32 frame
|
|
263
|
+
move = smart.make_relative_mover(client) # move(dx_um, dy_um), blocking
|
|
264
|
+
|
|
265
|
+
cal = smart.StageCameraCalibration.measure(snap, move, step_um=50)
|
|
266
|
+
obj = smart.find_objects(snap())[0] # largest blob
|
|
267
|
+
move(*cal.move_to_center(*obj["offset"])) # centre it - rotation and flips included
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- `StageCameraCalibration` - pixel offsets <-> stage moves, measured on the live images.
|
|
271
|
+
- `phase_correlate(a, b)` - sub-pixel image shift.
|
|
272
|
+
- `find_objects(frame)` / `foreground_mask(frame)` - blob detection.
|
|
273
|
+
- `EmptyFieldReference(empty_frame).coverage(frame)` - how much of a field is sample (0..1).
|
|
274
|
+
- `SafetyBox` - limits closed-loop moves to a square around the start.
|
|
275
|
+
- `Mosaic(frames, positions, cal)` - quick overview of frames placed by stage position.
|
|
276
|
+
|
|
277
|
+
The ImSwitch notebooks `ImSwitch_Smart_ObjectTracking`, `ImSwitch_Smart_PathFollowing` and
|
|
278
|
+
`ImSwitch_Smart_AdaptiveImaging` show them in use.
|
|
279
|
+
|
|
224
280
|
### Communication Manager
|
|
225
281
|
- `acquireImage()` - Acquire an image through communication channel.
|
|
226
282
|
- `getImage()` - Get an image from communication channel.
|
|
@@ -136,18 +136,30 @@ The ImSwitch API provides access to various components:
|
|
|
136
136
|
- `getAllDeviceNames()` - Get all available positioners.
|
|
137
137
|
- `getPositionerPositions()` - Get current position.
|
|
138
138
|
- `movePositioner(name, axis, value, is_absolute, is_blocking)` - Move the stage.
|
|
139
|
+
- `moveRelativeXY(dx, dy, positioner_name, is_blocking, speed)` - Relative X/Y move in µm.
|
|
140
|
+
- `stop(name, axis)` / `stopAllAxes(name)` - Stop one axis / every axis.
|
|
139
141
|
- `homeAxis(name, axis, is_blocking)` - Home the positioner.
|
|
140
142
|
|
|
141
143
|
### Lasers Manager
|
|
142
144
|
- `getLaserNames()` - Get available lasers.
|
|
143
145
|
- `setLaserActive(name, status)` - Turn laser on/off.
|
|
144
146
|
- `setLaserValue(name, value)` - Set laser intensity.
|
|
147
|
+
- `getLaserValueRanges(name)` / `getLaserValue(name)` / `getLaserActive(name)` - Read back.
|
|
148
|
+
- `setLaserOn(name, value)` / `setLaserOff(name)` - Value and on/off in one call.
|
|
149
|
+
- `runLaserFor(name, value, duration, on_tick, stop_event)` - Run a PWM channel (pump, valve)
|
|
150
|
+
for a fixed time; always switched off afterwards, also on errors or interruption.
|
|
145
151
|
|
|
146
152
|
### Recording Manager
|
|
147
153
|
- `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
|
|
148
154
|
- `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
|
|
149
155
|
- `stopRecording()` - Stop recording.
|
|
150
|
-
- `snapImageToPath(file_name)` - Snap image and save to specified path.
|
|
156
|
+
- `snapImageToPath(file_name, save_format, return_preview)` - Snap image and save to specified path.
|
|
157
|
+
- `snapAndWait(file_name, save_format, poll, timeout, on_progress, stop_event)` - Snap in the
|
|
158
|
+
background on the server and wait for it. Use for long exposures (minutes to hours); an
|
|
159
|
+
interrupt, `stop_event` or timeout cancels the exposure on the server.
|
|
160
|
+
- `startSnap(...)` / `getSnapStatus(job_id)` / `cancelSnap(job_id)` - The same, step by step.
|
|
161
|
+
- `previewToNumpy(result)` - 8-bit preview of a snap result as numpy array.
|
|
162
|
+
- `downloadImage(relative_file_path)` / `downloadFile(...)` - Fetch the saved file (full bit depth).
|
|
151
163
|
- `startVideoStream()` - Start MJPEG video stream.
|
|
152
164
|
- `stopVideoStream()` - Stop MJPEG video stream.
|
|
153
165
|
- `getVideoFrame()` - Get current frame from video stream.
|
|
@@ -160,6 +172,7 @@ The recording manager supports multiple save formats:
|
|
|
160
172
|
- `SaveFormat.MP4` - MP4 video format
|
|
161
173
|
- `SaveFormat.PNG` - PNG format
|
|
162
174
|
- `SaveFormat.JPG` - JPEG format
|
|
175
|
+
- `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
|
|
163
176
|
|
|
164
177
|
### Settings Manager
|
|
165
178
|
- `getDetectorNames()` - Get available detector names.
|
|
@@ -168,6 +181,25 @@ The recording manager supports multiple save formats:
|
|
|
168
181
|
- `setDetectorGain(detector_name, gain)` - Set detector gain.
|
|
169
182
|
- `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
|
|
170
183
|
- `setDetectorROI(detector_name, x, y, w, h)` - Set detector Region of Interest.
|
|
184
|
+
- `getDetectorParameter(parameter_name, detector_name)` - Read one parameter from the camera.
|
|
185
|
+
- `setDetectorMode(detector_name, is_auto)` / `setDetectorExposureOnce(detector_name)` - Auto exposure.
|
|
186
|
+
- `getCameraTemperature(detector_name)` / `setCameraTargetTemperature(temperature_c)` - Cooled cameras.
|
|
187
|
+
- `getDetectorParameterTree(detector_name)` / `setDetectorParameterValue(name, value)` - Typed access
|
|
188
|
+
to every camera parameter.
|
|
189
|
+
|
|
190
|
+
### Live View Manager
|
|
191
|
+
- `startLiveView(detector_name, protocol, params, force)` / `stopLiveView(detector_name)`
|
|
192
|
+
- `getLiveViewActive()`, `getActiveStreams()`, `getLongExposureInfo(detector_name)`
|
|
193
|
+
|
|
194
|
+
### UC2 Config Manager
|
|
195
|
+
- `getBusStatus()` - CAN-bus power and emergency-stop state.
|
|
196
|
+
- `setBusPower(enable)` / `setBusPowerAndWait(enable, settle)` - Switch the power of all CAN
|
|
197
|
+
slave boards (motors, LED/laser boards). Raises if the board refuses.
|
|
198
|
+
- `isBoardConnected()`, `getBoardTemperature()`
|
|
199
|
+
|
|
200
|
+
### Autofocus Manager
|
|
201
|
+
- `autoFocusAndWait(rangez, resolutionz, ...)` - Server-side Z scan around the current focus,
|
|
202
|
+
waits for the result. `autoFocus`, `getAutofocusStatus`, `stopAutofocus` for manual control.
|
|
171
203
|
|
|
172
204
|
### View Manager
|
|
173
205
|
- `setLiveViewActive(active)` - Enable/disable live view.
|
|
@@ -182,6 +214,30 @@ The recording manager supports multiple save formats:
|
|
|
182
214
|
- `setLED(led_id, state)` - Set specific LED with ID and state.
|
|
183
215
|
- `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
|
|
184
216
|
|
|
217
|
+
### Smart microscopy helpers (`imswitchclient.smart`)
|
|
218
|
+
Building blocks for closed-loop experiments; they work on numpy frames and plain callables and are
|
|
219
|
+
tested without a microscope (`tests/test_smart.py`).
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from imswitchclient import smart
|
|
223
|
+
snap = smart.make_snap(client) # -> float32 frame
|
|
224
|
+
move = smart.make_relative_mover(client) # move(dx_um, dy_um), blocking
|
|
225
|
+
|
|
226
|
+
cal = smart.StageCameraCalibration.measure(snap, move, step_um=50)
|
|
227
|
+
obj = smart.find_objects(snap())[0] # largest blob
|
|
228
|
+
move(*cal.move_to_center(*obj["offset"])) # centre it - rotation and flips included
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `StageCameraCalibration` - pixel offsets <-> stage moves, measured on the live images.
|
|
232
|
+
- `phase_correlate(a, b)` - sub-pixel image shift.
|
|
233
|
+
- `find_objects(frame)` / `foreground_mask(frame)` - blob detection.
|
|
234
|
+
- `EmptyFieldReference(empty_frame).coverage(frame)` - how much of a field is sample (0..1).
|
|
235
|
+
- `SafetyBox` - limits closed-loop moves to a square around the start.
|
|
236
|
+
- `Mosaic(frames, positions, cal)` - quick overview of frames placed by stage position.
|
|
237
|
+
|
|
238
|
+
The ImSwitch notebooks `ImSwitch_Smart_ObjectTracking`, `ImSwitch_Smart_PathFollowing` and
|
|
239
|
+
`ImSwitch_Smart_AdaptiveImaging` show them in use.
|
|
240
|
+
|
|
185
241
|
### Communication Manager
|
|
186
242
|
- `acquireImage()` - Acquire an image through communication channel.
|
|
187
243
|
- `getImage()` - Get an image from communication channel.
|
|
@@ -14,6 +14,9 @@ from .settingsManager import settingsManager
|
|
|
14
14
|
from .readnoiseManager import readnoiseManager
|
|
15
15
|
from .viewManager import viewManager
|
|
16
16
|
from .communicationManager import communicationManager
|
|
17
|
+
from .uc2ConfigManager import uc2ConfigManager
|
|
18
|
+
from .liveViewManager import liveViewManager
|
|
19
|
+
from .autofocusManager import autofocusManager
|
|
17
20
|
from .socketClient import socketClient
|
|
18
21
|
|
|
19
22
|
DEFAULT_ROUTE = "/imswitch/api"
|
|
@@ -86,6 +89,9 @@ class ImSwitchClient(object):
|
|
|
86
89
|
self.readnoiseManager = readnoiseManager(self)
|
|
87
90
|
self.viewManager = viewManager(self)
|
|
88
91
|
self.communicationManager = communicationManager(self)
|
|
92
|
+
self.uc2ConfigManager = uc2ConfigManager(self)
|
|
93
|
+
self.liveViewManager = liveViewManager(self)
|
|
94
|
+
self.autofocusManager = autofocusManager(self)
|
|
89
95
|
|
|
90
96
|
# initialize Socket.IO client
|
|
91
97
|
try:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class autofocusManager(object):
|
|
5
|
+
"""Server-side autofocus (AutofocusController): a Z step scan around the
|
|
6
|
+
current position with a Gaussian fit of the focus metric. Runs on the
|
|
7
|
+
server; `autoFocusAndWait` polls it to completion."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, parent):
|
|
10
|
+
self.parent = parent
|
|
11
|
+
|
|
12
|
+
def _url(self, func):
|
|
13
|
+
return f"{self.parent.base_uri}/AutofocusController/{func}"
|
|
14
|
+
|
|
15
|
+
def autoFocus(self, rangez=100, resolutionz=10, tSettle=0.1, focusAlgorithm="LAPE",
|
|
16
|
+
nCropsize=2048, nGauss=0, twoStage=False):
|
|
17
|
+
"""Start a scan of ±`rangez` µm in `resolutionz` µm steps; returns at once"""
|
|
18
|
+
payload = {'rangez': rangez, 'resolutionz': resolutionz, 'tSettle': tSettle,
|
|
19
|
+
'focusAlgorithm': focusAlgorithm, 'nCropsize': nCropsize, 'nGauss': nGauss,
|
|
20
|
+
'twoStage': twoStage}
|
|
21
|
+
return self.parent.get_json(self._url("autoFocus"), payload=payload)
|
|
22
|
+
|
|
23
|
+
def getAutofocusStatus(self):
|
|
24
|
+
"""{'state': idle|starting|scanning|fitting|moving_to_focus|finished|aborted|error, ...}"""
|
|
25
|
+
return self.parent.get_json(self._url("getAutofocusStatus"))
|
|
26
|
+
|
|
27
|
+
def stopAutofocus(self):
|
|
28
|
+
return self.parent.get_json(self._url("stopAutofocus"))
|
|
29
|
+
|
|
30
|
+
def autoFocusAndWait(self, rangez=100, resolutionz=10, poll=0.5, timeout=300, **kwargs):
|
|
31
|
+
"""`autoFocus` and wait for it; returns the final status dict.
|
|
32
|
+
|
|
33
|
+
Raises if the server refuses to start, the scan ends in error/aborted,
|
|
34
|
+
or `timeout` s pass (the scan is stopped then).
|
|
35
|
+
"""
|
|
36
|
+
started = self.autoFocus(rangez=rangez, resolutionz=resolutionz, **kwargs)
|
|
37
|
+
if isinstance(started, dict) and started.get("status") == "error":
|
|
38
|
+
raise RuntimeError(f"autofocus refused: {started.get('message')}")
|
|
39
|
+
t0 = time.time()
|
|
40
|
+
time.sleep(poll)
|
|
41
|
+
while True:
|
|
42
|
+
status = self.getAutofocusStatus()
|
|
43
|
+
state = status.get("state")
|
|
44
|
+
if state in ("finished", "idle") and not status.get("isRunning"):
|
|
45
|
+
return status
|
|
46
|
+
if state in ("error", "aborted"):
|
|
47
|
+
raise RuntimeError(f"autofocus ended with state '{state}'")
|
|
48
|
+
if time.time() - t0 > timeout:
|
|
49
|
+
self.stopAutofocus()
|
|
50
|
+
raise TimeoutError(f"autofocus exceeded {timeout} s")
|
|
51
|
+
time.sleep(poll)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class lasersManager(object):
|
|
5
|
+
|
|
6
|
+
def __init__(self, parent):
|
|
7
|
+
self.parent = parent
|
|
8
|
+
|
|
9
|
+
def getLaserNames(self):
|
|
10
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserNames"
|
|
11
|
+
headers = {'accept': 'application/json'}
|
|
12
|
+
|
|
13
|
+
response = self.parent.get_json(url, headers=headers)
|
|
14
|
+
return response
|
|
15
|
+
|
|
16
|
+
def getLaserValueRanges(self, laser_name):
|
|
17
|
+
"""(min, max) of the value `setLaserValue` accepts, e.g. (0, 1023) for a PWM channel"""
|
|
18
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserValueRanges"
|
|
19
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
20
|
+
|
|
21
|
+
def getLaserValue(self, laser_name):
|
|
22
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserValue"
|
|
23
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
24
|
+
|
|
25
|
+
def getLaserActive(self, laser_name):
|
|
26
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserActive"
|
|
27
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
28
|
+
|
|
29
|
+
def setLaserValue(self, laser_name, value=0):
|
|
30
|
+
url = f"{self.parent.base_uri}/LaserController/setLaserValue"
|
|
31
|
+
payload = {
|
|
32
|
+
'laserName': laser_name,
|
|
33
|
+
'value': value
|
|
34
|
+
}
|
|
35
|
+
response = self.parent.get_json(url, payload=payload)
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
def setLaserActive(self, laser_name, active=True):
|
|
39
|
+
url = f"{self.parent.base_uri}/LaserController/setLaserActive"
|
|
40
|
+
payload = {
|
|
41
|
+
"laserName": laser_name,
|
|
42
|
+
"active": active
|
|
43
|
+
}
|
|
44
|
+
response = self.parent.get_json(url, payload=payload)
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
def setLaserOn(self, laser_name, value):
|
|
48
|
+
"""Set the value and switch the channel on"""
|
|
49
|
+
self.setLaserValue(laser_name, value)
|
|
50
|
+
return self.setLaserActive(laser_name, True)
|
|
51
|
+
|
|
52
|
+
def setLaserOff(self, laser_name):
|
|
53
|
+
"""Set the value to 0 and switch the channel off"""
|
|
54
|
+
self.setLaserValue(laser_name, 0)
|
|
55
|
+
return self.setLaserActive(laser_name, False)
|
|
56
|
+
|
|
57
|
+
def runLaserFor(self, laser_name, value, duration, on_tick=None, tick=1.0, stop_event=None):
|
|
58
|
+
"""Drive a channel at `value` for `duration` seconds, then switch it off.
|
|
59
|
+
|
|
60
|
+
Meant for actuators wired to a laser/PWM output (pumps, valves). The
|
|
61
|
+
channel is switched off in a `finally`, so an interrupted notebook
|
|
62
|
+
cell (KeyboardInterrupt) or a failed request does not leave a pump
|
|
63
|
+
running. `on_tick(elapsed_s)` is called about every `tick` seconds.
|
|
64
|
+
Setting the `threading.Event` `stop_event` ends the run early.
|
|
65
|
+
"""
|
|
66
|
+
self.setLaserOn(laser_name, value)
|
|
67
|
+
try:
|
|
68
|
+
t0 = time.time()
|
|
69
|
+
while (elapsed := time.time() - t0) < duration:
|
|
70
|
+
if stop_event is not None and stop_event.is_set():
|
|
71
|
+
break
|
|
72
|
+
if on_tick:
|
|
73
|
+
on_tick(elapsed)
|
|
74
|
+
time.sleep(min(tick, duration - elapsed))
|
|
75
|
+
finally:
|
|
76
|
+
self.setLaserOff(laser_name)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class liveViewManager(object):
|
|
2
|
+
"""Server-side live stream (LiveViewController).
|
|
3
|
+
|
|
4
|
+
Long exposures stop the stream on the server; snaps arm the camera on
|
|
5
|
+
demand, so the stream only needs to run for things that converge on
|
|
6
|
+
streamed frames, such as auto exposure.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, parent):
|
|
10
|
+
self.parent = parent
|
|
11
|
+
|
|
12
|
+
def _url(self, func):
|
|
13
|
+
return f"{self.parent.base_uri}/LiveViewController/{func}"
|
|
14
|
+
|
|
15
|
+
def startLiveView(self, detector_name=None, protocol="jpeg", params=None, force=False):
|
|
16
|
+
"""Start the stream. Refused with status 'long_exposure' above the
|
|
17
|
+
server's long-exposure threshold unless `force` is set."""
|
|
18
|
+
query = {'protocol': protocol, 'force': force}
|
|
19
|
+
if detector_name is not None:
|
|
20
|
+
query['detectorName'] = detector_name
|
|
21
|
+
return self.parent.post_json(self._url("startLiveView"), payload=params, params=query)
|
|
22
|
+
|
|
23
|
+
def stopLiveView(self, detector_name=None, stop_camera=True):
|
|
24
|
+
payload = {'stopCamera': stop_camera}
|
|
25
|
+
if detector_name is not None:
|
|
26
|
+
payload['detectorName'] = detector_name
|
|
27
|
+
return self.parent.get_json(self._url("stopLiveView"), payload=payload)
|
|
28
|
+
|
|
29
|
+
def getLiveViewActive(self):
|
|
30
|
+
return self.parent.get_json(self._url("getLiveViewActive"))
|
|
31
|
+
|
|
32
|
+
def getActiveStreams(self):
|
|
33
|
+
return self.parent.get_json(self._url("getActiveStreams"))
|
|
34
|
+
|
|
35
|
+
def getLongExposureInfo(self, detector_name=None):
|
|
36
|
+
"""{'exposureMs', 'thresholdMs', 'isLongExposure'}"""
|
|
37
|
+
payload = {} if detector_name is None else {'detectorName': detector_name}
|
|
38
|
+
return self.parent.get_json(self._url("getLongExposureInfo"), payload=payload)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
# Blocking moves keep the HTTP request open for the whole travel, which can be
|
|
6
|
+
# minutes on a long stage. The server itself gives up after ~100 s.
|
|
7
|
+
BLOCKING_MOVE_TIMEOUT = 300
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class positionersManager(object):
|
|
11
|
+
|
|
12
|
+
def __init__(self, parent):
|
|
13
|
+
self.parent = parent
|
|
14
|
+
self.allStageNames = self.getAllDeviceNames()
|
|
15
|
+
|
|
16
|
+
def getAllDeviceNames(self):
|
|
17
|
+
url = f"{self.parent.base_uri}/PositionerController/getPositionerNames"
|
|
18
|
+
headers = {'accept': 'application/json'}
|
|
19
|
+
|
|
20
|
+
response = self.parent.get_json(url, headers=headers)
|
|
21
|
+
return response
|
|
22
|
+
|
|
23
|
+
def stop(self, positioner_name, axis):
|
|
24
|
+
if positioner_name is None:
|
|
25
|
+
positioner_name = self.allStageNames[0]
|
|
26
|
+
|
|
27
|
+
url = f"{self.parent.base_uri}/PositionerController/stopAxis"
|
|
28
|
+
payload = {
|
|
29
|
+
'positionerName': positioner_name,
|
|
30
|
+
'axis': axis
|
|
31
|
+
}
|
|
32
|
+
response = self.parent.get_json(url, payload=payload)
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
def movePositionerForever(self, positioner_name=None, axis="X", speed=0, is_stop=False):
|
|
36
|
+
if positioner_name is None:
|
|
37
|
+
positioner_name = self.allStageNames[0]
|
|
38
|
+
|
|
39
|
+
url = f"{self.parent.base_uri}/PositionerController/movePositionerForever"
|
|
40
|
+
payload = {
|
|
41
|
+
'positionerName': positioner_name,
|
|
42
|
+
'axis': axis,
|
|
43
|
+
'speed': speed,
|
|
44
|
+
'is_stop': is_stop
|
|
45
|
+
}
|
|
46
|
+
response = self.parent.get_json(url, payload=payload)
|
|
47
|
+
return response
|
|
48
|
+
|
|
49
|
+
def setPositionerSpeed(self, positioner_name, axis, speed):
|
|
50
|
+
if positioner_name is None:
|
|
51
|
+
positioner_name = self.allStageNames[0]
|
|
52
|
+
|
|
53
|
+
url = f"{self.parent.base_uri}/PositionerController/setPositionerSpeed"
|
|
54
|
+
payload = {
|
|
55
|
+
'positionerName': positioner_name,
|
|
56
|
+
'axis': axis,
|
|
57
|
+
'speed': speed
|
|
58
|
+
}
|
|
59
|
+
response = self.parent.get_json(url, payload=payload)
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
def movePositioner(self, positioner_name, axis, dist, is_absolute=True, is_blocking=True, speed=10000,
|
|
63
|
+
timeout=None):
|
|
64
|
+
"""Move one axis by/to `dist` µm. `timeout` (s) bounds the HTTP request;
|
|
65
|
+
the default allows long blocking moves (`BLOCKING_MOVE_TIMEOUT`)."""
|
|
66
|
+
if timeout is None:
|
|
67
|
+
timeout = BLOCKING_MOVE_TIMEOUT if is_blocking else 30
|
|
68
|
+
if positioner_name is None:
|
|
69
|
+
positioner_name = self.allStageNames[0]
|
|
70
|
+
|
|
71
|
+
url = f"{self.parent.base_uri}/PositionerController/movePositioner"
|
|
72
|
+
payload = {
|
|
73
|
+
'positionerName': positioner_name,
|
|
74
|
+
'axis': axis,
|
|
75
|
+
'dist': dist,
|
|
76
|
+
'isAbsolute': is_absolute,
|
|
77
|
+
'isBlocking': is_blocking,
|
|
78
|
+
'speed': speed
|
|
79
|
+
}
|
|
80
|
+
#headers = {'accept': 'application/json'}
|
|
81
|
+
|
|
82
|
+
response = self.parent.get_json(url, payload=payload, timeout=timeout)
|
|
83
|
+
return response
|
|
84
|
+
|
|
85
|
+
def getPositionerPositions(self, id=None):
|
|
86
|
+
url = f"{self.parent.base_uri}/PositionerController/getPositionerPositions"
|
|
87
|
+
payload = {
|
|
88
|
+
}
|
|
89
|
+
response = self.parent.get_json(url, payload=payload)
|
|
90
|
+
if type(id) == int:
|
|
91
|
+
return response[id]
|
|
92
|
+
return response
|
|
93
|
+
|
|
94
|
+
def homeAxis(self, positioner_name, axis, is_blocking=False):
|
|
95
|
+
url = f"{self.parent.base_uri}/PositionerController/homeAxis"
|
|
96
|
+
if positioner_name is None:
|
|
97
|
+
positioner_name = self.allStageNames[0]
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
'positionerName': positioner_name,
|
|
101
|
+
'axis': axis,
|
|
102
|
+
'isBlocking': is_blocking
|
|
103
|
+
}
|
|
104
|
+
response = self.parent.get_json(url, payload=payload)
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
def stopAllAxes(self, positioner_name=None):
|
|
108
|
+
"""Immediately stop every axis of the positioner"""
|
|
109
|
+
if positioner_name is None:
|
|
110
|
+
positioner_name = self.allStageNames[0]
|
|
111
|
+
url = f"{self.parent.base_uri}/PositionerController/stopAllAxes"
|
|
112
|
+
return self.parent.get_json(url, payload={'positionerName': positioner_name})
|
|
113
|
+
|
|
114
|
+
def moveRelativeXY(self, dx, dy, positioner_name=None, is_blocking=True, speed=10000):
|
|
115
|
+
"""Relative X then Y move in µm; zero components are skipped"""
|
|
116
|
+
if positioner_name is None:
|
|
117
|
+
positioner_name = self.allStageNames[0]
|
|
118
|
+
for axis, dist in (("X", dx), ("Y", dy)):
|
|
119
|
+
if dist:
|
|
120
|
+
self.movePositioner(positioner_name, axis, dist, is_absolute=False,
|
|
121
|
+
is_blocking=is_blocking, speed=speed)
|
|
122
|
+
|
|
123
|
+
def moveAbsoluteXY(self, x, y, positioner_name=None, speed=10000, tolerance_um=10.0,
|
|
124
|
+
timeout=None, poll=0.5):
|
|
125
|
+
"""Absolute X/Y move (µm, user frame) that returns once the stage *reports*
|
|
126
|
+
the target, within `tolerance_um`; returns the final positions.
|
|
127
|
+
|
|
128
|
+
`timeout` (s) defaults to twice the travel time at `speed` plus 30 s, so
|
|
129
|
+
a long travel is not cut off. Should an HTTP request time out anyway,
|
|
130
|
+
the move goes on on the server and this keeps polling the position
|
|
131
|
+
instead of failing. Raises TimeoutError if the target is not reached.
|
|
132
|
+
"""
|
|
133
|
+
if positioner_name is None:
|
|
134
|
+
positioner_name = self.allStageNames[0]
|
|
135
|
+
pos = self.getPositionerPositions()[positioner_name]
|
|
136
|
+
travel = max(abs(x - pos["X"]), abs(y - pos["Y"]))
|
|
137
|
+
if timeout is None:
|
|
138
|
+
timeout = 2 * travel / max(float(speed), 1.0) + 30
|
|
139
|
+
t_end = time.time() + timeout
|
|
140
|
+
for axis, target in (("X", x), ("Y", y)):
|
|
141
|
+
try:
|
|
142
|
+
self.movePositioner(positioner_name, axis, target, is_absolute=True, is_blocking=True,
|
|
143
|
+
speed=speed, timeout=max(5.0, t_end - time.time()))
|
|
144
|
+
except requests.exceptions.ReadTimeout:
|
|
145
|
+
pass # still travelling; the position poll below decides
|
|
146
|
+
while True:
|
|
147
|
+
pos = self.getPositionerPositions()[positioner_name]
|
|
148
|
+
if abs(pos["X"] - x) <= tolerance_um and abs(pos["Y"] - y) <= tolerance_um:
|
|
149
|
+
return pos
|
|
150
|
+
if time.time() > t_end:
|
|
151
|
+
raise TimeoutError(f"stage reports X={pos['X']:.1f} Y={pos['Y']:.1f} µm "
|
|
152
|
+
f"{timeout:.0f} s after moving to X={x:.1f} Y={y:.1f} µm")
|
|
153
|
+
time.sleep(poll)
|