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.
Files changed (40) hide show
  1. {imswitchclient-0.1.8/imswitchclient.egg-info → imswitchclient-0.2.0}/PKG-INFO +58 -2
  2. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/README.md +57 -1
  3. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/ImSwitchClient.py +6 -0
  4. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/__init__.py +1 -1
  5. imswitchclient-0.2.0/imswitchclient/autofocusManager.py +51 -0
  6. imswitchclient-0.2.0/imswitchclient/lasersManager.py +76 -0
  7. imswitchclient-0.2.0/imswitchclient/liveViewManager.py +38 -0
  8. imswitchclient-0.2.0/imswitchclient/positionersManager.py +153 -0
  9. imswitchclient-0.2.0/imswitchclient/recordingManager.py +259 -0
  10. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/settingsManager.py +54 -0
  11. imswitchclient-0.2.0/imswitchclient/smart.py +362 -0
  12. imswitchclient-0.2.0/imswitchclient/uc2ConfigManager.py +50 -0
  13. {imswitchclient-0.1.8 → imswitchclient-0.2.0/imswitchclient.egg-info}/PKG-INFO +58 -2
  14. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/SOURCES.txt +6 -1
  15. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/setup.cfg +2 -2
  16. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/setup.py +1 -1
  17. imswitchclient-0.2.0/tests/test_imswitchclient.py +262 -0
  18. imswitchclient-0.2.0/tests/test_smart.py +213 -0
  19. imswitchclient-0.1.8/imswitchclient/lasersManager.py +0 -30
  20. imswitchclient-0.1.8/imswitchclient/positionersManager.py +0 -91
  21. imswitchclient-0.1.8/imswitchclient/recordingManager.py +0 -142
  22. imswitchclient-0.1.8/tests/test_imswitchclient.py +0 -21
  23. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/LICENSE +0 -0
  24. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/MANIFEST.in +0 -0
  25. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/client.py +0 -0
  26. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/communicationManager.py +0 -0
  27. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/experimentController.py +0 -0
  28. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/histoscanManager.py +0 -0
  29. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/ledMatrixManager.py +0 -0
  30. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/mdaController.py +0 -0
  31. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/objectiveController.py +0 -0
  32. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/readnoiseManager.py +0 -0
  33. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/socketClient.py +0 -0
  34. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient/viewManager.py +0 -0
  35. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/dependency_links.txt +0 -0
  36. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/not-zip-safe +0 -0
  37. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/requires.txt +0 -0
  38. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/imswitchclient.egg-info/top_level.txt +0 -0
  39. {imswitchclient-0.1.8 → imswitchclient-0.2.0}/tests/__init__.py +0 -0
  40. {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.1.8
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:
@@ -3,4 +3,4 @@ from imswitchclient import *
3
3
 
4
4
  __author__ = """Benedict Diederich"""
5
5
  __email__ = 'benedictdied@gmail.com'
6
- __version__ = "v0.1.8"#'0.1.7'
6
+ __version__ = "v0.2.0"#'0.1.7'
@@ -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)