imswitchclient 0.1.7__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 (45) hide show
  1. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/MANIFEST.in +0 -2
  2. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/PKG-INFO +78 -9
  3. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/README.md +76 -7
  4. imswitchclient-0.2.0/imswitchclient/ImSwitchClient.py +142 -0
  5. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/__init__.py +1 -1
  6. imswitchclient-0.2.0/imswitchclient/autofocusManager.py +51 -0
  7. imswitchclient-0.2.0/imswitchclient/client.py +6 -0
  8. imswitchclient-0.2.0/imswitchclient/lasersManager.py +76 -0
  9. imswitchclient-0.2.0/imswitchclient/liveViewManager.py +38 -0
  10. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/mdaController.py +1 -1
  11. imswitchclient-0.2.0/imswitchclient/positionersManager.py +153 -0
  12. imswitchclient-0.2.0/imswitchclient/readnoiseManager.py +106 -0
  13. imswitchclient-0.2.0/imswitchclient/recordingManager.py +259 -0
  14. imswitchclient-0.2.0/imswitchclient/settingsManager.py +146 -0
  15. imswitchclient-0.2.0/imswitchclient/smart.py +362 -0
  16. imswitchclient-0.2.0/imswitchclient/uc2ConfigManager.py +50 -0
  17. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/PKG-INFO +78 -9
  18. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/SOURCES.txt +8 -3
  19. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/requires.txt +1 -1
  20. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/setup.cfg +2 -2
  21. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/setup.py +3 -4
  22. imswitchclient-0.2.0/tests/test_baseurl.py +65 -0
  23. imswitchclient-0.2.0/tests/test_imswitchclient.py +262 -0
  24. imswitchclient-0.2.0/tests/test_smart.py +213 -0
  25. imswitchclient-0.1.7/CONTRIBUTING.rst +0 -128
  26. imswitchclient-0.1.7/HISTORY.rst +0 -8
  27. imswitchclient-0.1.7/imswitchclient/ImSwitchClient.py +0 -74
  28. imswitchclient-0.1.7/imswitchclient/client.py +0 -81
  29. imswitchclient-0.1.7/imswitchclient/lasersManager.py +0 -30
  30. imswitchclient-0.1.7/imswitchclient/positionersManager.py +0 -91
  31. imswitchclient-0.1.7/imswitchclient/recordingManager.py +0 -142
  32. imswitchclient-0.1.7/imswitchclient/settingsManager.py +0 -70
  33. imswitchclient-0.1.7/tests/test_imswitchclient.py +0 -21
  34. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/LICENSE +0 -0
  35. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/communicationManager.py +0 -0
  36. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/experimentController.py +0 -0
  37. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/histoscanManager.py +0 -0
  38. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/ledMatrixManager.py +0 -0
  39. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/objectiveController.py +0 -0
  40. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/socketClient.py +0 -0
  41. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/viewManager.py +0 -0
  42. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/dependency_links.txt +0 -0
  43. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/not-zip-safe +0 -0
  44. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/top_level.txt +0 -0
  45. {imswitchclient-0.1.7 → imswitchclient-0.2.0}/tests/__init__.py +0 -0
@@ -1,6 +1,4 @@
1
1
  include AUTHORS.rst
2
- include CONTRIBUTING.rst
3
- include HISTORY.rst
4
2
  include LICENSE
5
3
  include README.rst
6
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imswitchclient
3
- Version: 0.1.7
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
@@ -23,7 +23,7 @@ Requires-Dist: Pillow
23
23
  Requires-Dist: numpy
24
24
  Requires-Dist: matplotlib
25
25
  Requires-Dist: opencv-python
26
- Requires-Dist: python-socketio==5.12.1
26
+ Requires-Dist: python-socketio
27
27
  Dynamic: author
28
28
  Dynamic: author-email
29
29
  Dynamic: classifier
@@ -81,10 +81,23 @@ pip install imswitchclient
81
81
  ```python
82
82
  import imswitchclient.ImSwitchClient as imc
83
83
 
84
- # Initialize the client
85
- client = imc.ImSwitchClient(host="0.0.0.0", isHttps=True, port=8001)
84
+ # A full URL is the least ambiguous way to say where ImSwitch is:
85
+ client = imc.ImSwitchClient("http://localhost:8001/imswitch/api") # local instance
86
+ client = imc.ImSwitchClient("http://192.168.178.76/imswitch/api") # Raspberry Pi / Docker behind Caddy (port 80)
87
+ client = imc.ImSwitchClient("https://imswitch.openuc2.com") # https -> port 443
88
+
89
+ # Without arguments it uses $IMSWITCH_API_URL - which ImSwitch sets for the
90
+ # notebooks it serves itself - and otherwise http://localhost:8001/imswitch/api.
91
+ client = imc.ImSwitchClient()
92
+
93
+ # The classic form still works; a bare host name keeps the old defaults (port 8001, http).
94
+ client = imc.ImSwitchClient(host="192.168.178.76", port=80, isHttps=False)
95
+
96
+ print(client.base_uri) # http://192.168.178.76:80/imswitch/api
86
97
  ```
87
98
 
99
+ Pass `socket_port=` when Socket.IO is not served on the API port.
100
+
88
101
  ### Example: Moving a Stage and Acquiring an Image
89
102
 
90
103
  ```python
@@ -162,18 +175,30 @@ The ImSwitch API provides access to various components:
162
175
  - `getAllDeviceNames()` - Get all available positioners.
163
176
  - `getPositionerPositions()` - Get current position.
164
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.
165
180
  - `homeAxis(name, axis, is_blocking)` - Home the positioner.
166
181
 
167
182
  ### Lasers Manager
168
183
  - `getLaserNames()` - Get available lasers.
169
184
  - `setLaserActive(name, status)` - Turn laser on/off.
170
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.
171
190
 
172
191
  ### Recording Manager
173
192
  - `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
174
193
  - `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
175
194
  - `stopRecording()` - Stop recording.
176
- - `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).
177
202
  - `startVideoStream()` - Start MJPEG video stream.
178
203
  - `stopVideoStream()` - Stop MJPEG video stream.
179
204
  - `getVideoFrame()` - Get current frame from video stream.
@@ -186,6 +211,7 @@ The recording manager supports multiple save formats:
186
211
  - `SaveFormat.MP4` - MP4 video format
187
212
  - `SaveFormat.PNG` - PNG format
188
213
  - `SaveFormat.JPG` - JPEG format
214
+ - `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
189
215
 
190
216
  ### Settings Manager
191
217
  - `getDetectorNames()` - Get available detector names.
@@ -194,6 +220,25 @@ The recording manager supports multiple save formats:
194
220
  - `setDetectorGain(detector_name, gain)` - Set detector gain.
195
221
  - `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
196
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.
197
242
 
198
243
  ### View Manager
199
244
  - `setLiveViewActive(active)` - Enable/disable live view.
@@ -208,6 +253,30 @@ The recording manager supports multiple save formats:
208
253
  - `setLED(led_id, state)` - Set specific LED with ID and state.
209
254
  - `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
210
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
+
211
280
  ### Communication Manager
212
281
  - `acquireImage()` - Acquire an image through communication channel.
213
282
  - `getImage()` - Get an image from communication channel.
@@ -246,7 +315,7 @@ import numpy as np
246
315
  import matplotlib.pyplot as plt
247
316
 
248
317
  # Initialize client
249
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
318
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
250
319
 
251
320
  # XY scanning parameters
252
321
  start_x, start_y = 0, 0 # Starting position in µm
@@ -309,7 +378,7 @@ import imswitchclient.ImSwitchClient as imc
309
378
  import numpy as np
310
379
 
311
380
  # Initialize client
312
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
381
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
313
382
 
314
383
  def calculate_focus_score(image):
315
384
  """Calculate focus score using Laplacian variance"""
@@ -362,7 +431,7 @@ from imswitchclient.recordingManager import SaveFormat
362
431
  import time
363
432
 
364
433
  # Initialize client
365
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
434
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
366
435
 
367
436
  # Setup LED illumination
368
437
  client.ledMatrixManager.setAllLEDOff()
@@ -407,7 +476,7 @@ import imswitchclient.ImSwitchClient as imc
407
476
  from imswitchclient.recordingManager import SaveFormat
408
477
 
409
478
  # Initialize client
410
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
479
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
411
480
 
412
481
  # Define multiple positions of interest
413
482
  positions = [
@@ -42,10 +42,23 @@ pip install imswitchclient
42
42
  ```python
43
43
  import imswitchclient.ImSwitchClient as imc
44
44
 
45
- # Initialize the client
46
- client = imc.ImSwitchClient(host="0.0.0.0", isHttps=True, port=8001)
45
+ # A full URL is the least ambiguous way to say where ImSwitch is:
46
+ client = imc.ImSwitchClient("http://localhost:8001/imswitch/api") # local instance
47
+ client = imc.ImSwitchClient("http://192.168.178.76/imswitch/api") # Raspberry Pi / Docker behind Caddy (port 80)
48
+ client = imc.ImSwitchClient("https://imswitch.openuc2.com") # https -> port 443
49
+
50
+ # Without arguments it uses $IMSWITCH_API_URL - which ImSwitch sets for the
51
+ # notebooks it serves itself - and otherwise http://localhost:8001/imswitch/api.
52
+ client = imc.ImSwitchClient()
53
+
54
+ # The classic form still works; a bare host name keeps the old defaults (port 8001, http).
55
+ client = imc.ImSwitchClient(host="192.168.178.76", port=80, isHttps=False)
56
+
57
+ print(client.base_uri) # http://192.168.178.76:80/imswitch/api
47
58
  ```
48
59
 
60
+ Pass `socket_port=` when Socket.IO is not served on the API port.
61
+
49
62
  ### Example: Moving a Stage and Acquiring an Image
50
63
 
51
64
  ```python
@@ -123,18 +136,30 @@ The ImSwitch API provides access to various components:
123
136
  - `getAllDeviceNames()` - Get all available positioners.
124
137
  - `getPositionerPositions()` - Get current position.
125
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.
126
141
  - `homeAxis(name, axis, is_blocking)` - Home the positioner.
127
142
 
128
143
  ### Lasers Manager
129
144
  - `getLaserNames()` - Get available lasers.
130
145
  - `setLaserActive(name, status)` - Turn laser on/off.
131
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.
132
151
 
133
152
  ### Recording Manager
134
153
  - `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
135
154
  - `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
136
155
  - `stopRecording()` - Stop recording.
137
- - `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).
138
163
  - `startVideoStream()` - Start MJPEG video stream.
139
164
  - `stopVideoStream()` - Stop MJPEG video stream.
140
165
  - `getVideoFrame()` - Get current frame from video stream.
@@ -147,6 +172,7 @@ The recording manager supports multiple save formats:
147
172
  - `SaveFormat.MP4` - MP4 video format
148
173
  - `SaveFormat.PNG` - PNG format
149
174
  - `SaveFormat.JPG` - JPEG format
175
+ - `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
150
176
 
151
177
  ### Settings Manager
152
178
  - `getDetectorNames()` - Get available detector names.
@@ -155,6 +181,25 @@ The recording manager supports multiple save formats:
155
181
  - `setDetectorGain(detector_name, gain)` - Set detector gain.
156
182
  - `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
157
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.
158
203
 
159
204
  ### View Manager
160
205
  - `setLiveViewActive(active)` - Enable/disable live view.
@@ -169,6 +214,30 @@ The recording manager supports multiple save formats:
169
214
  - `setLED(led_id, state)` - Set specific LED with ID and state.
170
215
  - `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
171
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
+
172
241
  ### Communication Manager
173
242
  - `acquireImage()` - Acquire an image through communication channel.
174
243
  - `getImage()` - Get an image from communication channel.
@@ -207,7 +276,7 @@ import numpy as np
207
276
  import matplotlib.pyplot as plt
208
277
 
209
278
  # Initialize client
210
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
279
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
211
280
 
212
281
  # XY scanning parameters
213
282
  start_x, start_y = 0, 0 # Starting position in µm
@@ -270,7 +339,7 @@ import imswitchclient.ImSwitchClient as imc
270
339
  import numpy as np
271
340
 
272
341
  # Initialize client
273
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
342
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
274
343
 
275
344
  def calculate_focus_score(image):
276
345
  """Calculate focus score using Laplacian variance"""
@@ -323,7 +392,7 @@ from imswitchclient.recordingManager import SaveFormat
323
392
  import time
324
393
 
325
394
  # Initialize client
326
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
395
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
327
396
 
328
397
  # Setup LED illumination
329
398
  client.ledMatrixManager.setAllLEDOff()
@@ -368,7 +437,7 @@ import imswitchclient.ImSwitchClient as imc
368
437
  from imswitchclient.recordingManager import SaveFormat
369
438
 
370
439
  # Initialize client
371
- client = imc.ImSwitchClient(host="192.168.1.100", port=8001)
440
+ client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
372
441
 
373
442
  # Define multiple positions of interest
374
443
  positions = [
@@ -0,0 +1,142 @@
1
+ import os
2
+ import requests
3
+ import logging
4
+ from urllib.parse import urlsplit
5
+ from .positionersManager import positionersManager
6
+ from .recordingManager import recordingManager
7
+ from .lasersManager import lasersManager
8
+ from .histoscanManager import histoscanManager
9
+ from .experimentController import ExperimentController
10
+ from .mdaController import mdaController
11
+ from .objectiveController import objectiveController
12
+ from .ledMatrixManager import ledMatrixManager
13
+ from .settingsManager import settingsManager
14
+ from .readnoiseManager import readnoiseManager
15
+ from .viewManager import viewManager
16
+ from .communicationManager import communicationManager
17
+ from .uc2ConfigManager import uc2ConfigManager
18
+ from .liveViewManager import liveViewManager
19
+ from .autofocusManager import autofocusManager
20
+ from .socketClient import socketClient
21
+
22
+ DEFAULT_ROUTE = "/imswitch/api"
23
+ DEFAULT_PORT = 8001
24
+
25
+
26
+ def parseBaseUrl(url, defaultRoute=DEFAULT_ROUTE):
27
+ """Split a base URL into (host, port, isHttps, route).
28
+
29
+ Takes whatever you can copy out of the browser's address bar::
30
+
31
+ http://192.168.178.76/imswitch/api/docs -> ('192.168.178.76', 80, False, '/imswitch/api')
32
+ http://100.100.43.118:8001 -> ('100.100.43.118', 8001, False, '/imswitch/api')
33
+ https://imswitch.openuc2.com -> ('imswitch.openuc2.com', 443, True, '/imswitch/api')
34
+ """
35
+ parts = urlsplit(url if "://" in url else f"http://{url}")
36
+ isHttps = parts.scheme == "https"
37
+ route = parts.path.rstrip("/")
38
+ for suffix in ("/docs", "/redoc", "/openapi.json"):
39
+ if route.endswith(suffix):
40
+ route = route[: -len(suffix)]
41
+ port = parts.port or (443 if isHttps else 80)
42
+ return parts.hostname, port, isHttps, route or defaultRoute
43
+
44
+
45
+ class ImSwitchClient(object):
46
+ def __init__(self, host=None, isHttps=None, port=None, route=None, socket_port=None):
47
+ """Connect to an ImSwitch server.
48
+
49
+ `host` may be a full URL, which is the least error-prone way to reach a
50
+ server that is not on the default port::
51
+
52
+ ImSwitchClient("http://192.168.178.76/imswitch/api") # Docker/Caddy on port 80
53
+ ImSwitchClient("http://100.100.43.118:8001") # local instance
54
+ ImSwitchClient("imswitch.openuc2.com") # host only -> port 8001
55
+ ImSwitchClient() # $IMSWITCH_API_URL, else localhost:8001
56
+
57
+ A plain host name keeps the historical defaults (port 8001, http). A URL
58
+ without a port uses the scheme's port (80/443). Explicit `isHttps`,
59
+ `port` and `route` arguments always win over what the URL says.
60
+ `socket_port` defaults to the API port - set it when Socket.IO is not
61
+ proxied on the same port.
62
+ """
63
+ url = host if (host and "://" in host) else None
64
+ if url is None and host is None:
65
+ url = os.environ.get("IMSWITCH_API_URL") or None
66
+ if url:
67
+ urlHost, urlPort, urlHttps, urlRoute = parseBaseUrl(url)
68
+ else:
69
+ urlHost, urlPort, urlHttps, urlRoute = host, None, None, None
70
+
71
+ self.host = urlHost or "localhost"
72
+ self.isHttps = isHttps if isHttps is not None else bool(urlHttps)
73
+ self.port = port if port is not None else (urlPort or (443 if self.isHttps else DEFAULT_PORT))
74
+ self.route = (route or urlRoute or DEFAULT_ROUTE).rstrip("/")
75
+ self.get_json(self.base_swagger_uri)
76
+
77
+ logging.info(f"Connecting to microscope at {self.base_uri}")
78
+
79
+ # register managers
80
+ self.positionersManager = positionersManager(self)
81
+ self.recordingManager = recordingManager(self)
82
+ self.lasersManager = lasersManager(self)
83
+ self.histoscanManager = histoscanManager(self)
84
+ self.experimentController = ExperimentController(self)
85
+ self.mdaController = mdaController(self)
86
+ self.objectiveController = objectiveController(self)
87
+ self.ledMatrixManager = ledMatrixManager(self)
88
+ self.settingsManager = settingsManager(self)
89
+ self.readnoiseManager = readnoiseManager(self)
90
+ self.viewManager = viewManager(self)
91
+ self.communicationManager = communicationManager(self)
92
+ self.uc2ConfigManager = uc2ConfigManager(self)
93
+ self.liveViewManager = liveViewManager(self)
94
+ self.autofocusManager = autofocusManager(self)
95
+
96
+ # initialize Socket.IO client
97
+ try:
98
+ self.socketClient = socketClient(host=self.host,
99
+ port=socket_port or self.port,
100
+ isHttps=self.isHttps)
101
+ except Exception as e:
102
+ logging.error(f"Failed to connect Socket.IO client: {e}")
103
+ self.socketClient = None
104
+
105
+ @property
106
+ def base_uri(self):
107
+ scheme = "https" if self.isHttps else "http"
108
+ return f"{scheme}://{self.host}:{self.port}{self.route}"
109
+
110
+ @property
111
+ def base_root_uri(self):
112
+ """Server root without the API suffix - serves /data, /jupyter, /openapi.json"""
113
+ base = self.base_uri
114
+ return base[:-4] if base.endswith("/api") else base
115
+
116
+ @property
117
+ def base_swagger_uri(self):
118
+ return self.base_root_uri + "/openapi.json"
119
+
120
+ def get_json(self, path, payload={}, headers={}, timeout=30):
121
+ """Perform an HTTP GET request and return the JSON response"""
122
+ if not path.startswith("http"):
123
+ path = self.base_uri + path
124
+ r = requests.get(path, params=payload, headers=headers, verify=False, timeout=timeout)
125
+ r.raise_for_status()
126
+ return r.json()
127
+
128
+ def post_json(self, path, payload={}, headers={}, wait_on_task="auto", params=None, timeout=30):
129
+ """Make an HTTP POST request and return the JSON response.
130
+
131
+ `params` goes into the query string: FastAPI puts scalar arguments of a
132
+ POST endpoint there, only pydantic-model arguments come from the body.
133
+ """
134
+ if not path.startswith("http"):
135
+ path = self.base_uri + path
136
+ r = requests.post(path, json=payload, params=params, headers=headers,
137
+ verify=False, timeout=timeout)
138
+ r.raise_for_status()
139
+ r = r.json()
140
+ return r
141
+
142
+
@@ -3,4 +3,4 @@ from imswitchclient import *
3
3
 
4
4
  __author__ = """Benedict Diederich"""
5
5
  __email__ = 'benedictdied@gmail.com'
6
- __version__ = "v0.1.7"#'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,6 @@
1
+ """Backwards-compatible alias.
2
+
3
+ The client used to exist twice, and this copy pointed at `/imswitch/api/v2`,
4
+ a route the server does not serve. Everything lives in `ImSwitchClient.py` now.
5
+ """
6
+ from .ImSwitchClient import ImSwitchClient, parseBaseUrl # noqa: F401
@@ -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)