BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
aurox.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 Mick Phillips <mick.phillips@gmail.com>
4##
5## This file is part of Microscope.
6##
7## Microscope is free software: you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation, either version 3 of the License, or
10## (at your option) any later version.
11##
12## Microscope is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
19
20"""Adds support for Aurox devices
21
22Requires package hidapi."""
23
24import time
25from threading import Lock
26
27import hid
28
29import microscope
31
32
33# Clarity constants. These may differ across products, so mangle names.
34# USB IDs
35_Clarity__VENDORID = 0x1F0A
36_Clarity__PRODUCTID = 0x0088
37# Base status
38_Clarity__SLEEP = 0x7F
39_Clarity__RUN = 0x0F
40# Door status
41_Clarity__DOOROPEN = 0x01
42_Clarity__DOORCLOSED = 0x02
43# Disk position/status
44_Clarity__SLDPOS0 = 0x00 # disk out of beam path, wide field
45_Clarity__SLDPOS1 = 0x01 # disk pos 1, low sectioning
46_Clarity__SLDPOS2 = 0x02 # disk pos 2, mid sectioning
47_Clarity__SLDPOS3 = 0x03 # disk pos 3, high sectioning
48_Clarity__SLDERR = 0xFF # An error has occurred in setting slide position (end stops not detected)
49_Clarity__SLDMID = 0x10 # slide between positions (was =0x03 for SD62)
50# Filter position/status
51_Clarity__FLTPOS1 = 0x01 # Filter in position 1
52_Clarity__FLTPOS2 = 0x02 # Filter in position 2
53_Clarity__FLTPOS3 = 0x03 # Filter in position 3
54_Clarity__FLTPOS4 = 0x04 # Filter in position 4
55_Clarity__FLTERR = 0xFF # An error has been detected in the filter drive (eg filters not present)
56_Clarity__FLTMID = 0x10 # Filter between positions
57# Calibration LED state
58_Clarity__CALON = 0x01 # CALibration led power on
59_Clarity__CALOFF = 0x02 # CALibration led power off
60# Error status
61_Clarity__CMDERROR = 0xFF # Reply to a command that was not understood
62# Commands
63_Clarity__GETVERSION = 0x00 # Return 3-byte version number byte1.byte2.byte3
64# State commands: single command byte immediately followed by any data.
65_Clarity__GETONOFF = 0x12 # No data out, returns 1 byte on/off status
66_Clarity__GETDOOR = 0x13 # No data out, returns 1 byte shutter status, or SLEEP if device sleeping
67_Clarity__GETSLIDE = 0x14 # No data out, returns 1 byte disk-slide status, or SLEEP if device sleeping
68_Clarity__GETFILT = 0x15 # No data out, returns 1 byte filter position, or SLEEP if device sleeping
69_Clarity__GETCAL = 0x16 # No data out, returns 1 byte CAL led status, or SLEEP if device sleeping
70_Clarity__GETSERIAL = (
71 0x19 # No data out, returns 4 byte BCD serial number (little endian)
72)
73_Clarity__FULLSTAT = 0x1F # No data, Returns 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,??
74# Run state action commands
75_Clarity__SETONOFF = 0x21 # 1 byte out on/off status, echoes command or SLEEP
76_Clarity__SETSLIDE = 0x23 # 1 byte out disk position, echoes command or SLEEP
77_Clarity__SETFILT = 0x24 # 1 byte out filter position, echoes command or SLEEP
78_Clarity__SETCAL = 0x25 # 1 byte out CAL led status, echoes command or SLEEP
79# Service mode commands. Stops disk spinning for alignment.
80_Clarity__SETSVCMODE1 = 0xE0 # 1 byte for service mode. SLEEP activates service mode. RUN returns to normal mode.
81
82
83class Clarity(microscope.devices.FilterWheelBase):
84 _slide_to_sectioning = {
85 __SLDPOS0: "bypass",
86 __SLDPOS1: "low",
87 __SLDPOS2: "mid",
88 __SLDPOS3: "high",
89 }
90 _positions = 4
91 _resultlen = {
92 __GETONOFF: 1,
93 __GETDOOR: 1,
94 __GETSLIDE: 1,
95 __GETFILT: 1,
96 __GETCAL: 1,
97 __GETSERIAL: 4,
98 __FULLSTAT: 10,
99 }
100
101 def __init__(self, **kwargs):
102 super().__init__(positions=Clarity._positions, **kwargs)
103 self._lock = Lock()
104 self._hid = None
105 self.add_setting(
106 "sectioning",
107 "enum",
109 lambda val: self.set_slide_position(val),
111 )
112
113 def _send_command(self, command, param=0, max_length=16, timeout_ms=100):
114 """Send a command to the Clarity and return its response"""
115 if not self._hid:
116 self.open()
117 with self._lock:
118 # The device expects a list of 16 integers
119 buffer = [0x00] * max_length # The 0th element must be 0.
120 buffer[1] = command # The 1st element is the command
121 buffer[2] = param # The 2nd element is any command argument.
122 result = self._hid.write(buffer)
123 if result == -1:
124 # Nothing to read back. Check hid error state.
125 err = self._hid.error()
126 if err != "":
127 self.close()
128 raise microscope.DeviceError(err)
129 else:
130 return None
131 while True:
132 # Read responses until we see the response to our command.
133 # (We should get the correct response on the first read.)
134 response = self._hid.read(result - 1, timeout_ms)
135 if not response:
136 # No response
137 return None
138 elif response[0] == command:
139 break
140 bytes = self._resultlen.get(command, None)
141 if bytes is None:
142 return response[1:]
143 elif bytes == 1:
144 return response[1]
145 else:
146 return response[1:]
147
148 @property
149 def is_connected(self):
150 return self._hid is not None
151
152 def open(self):
153 h = hid.device()
154 h.open(vendor_id=__VENDORID, product_id=__PRODUCTID)
155 h.set_nonblocking(False)
156 self._hid = h
157
158 def close(self):
159 if self.is_connected:
160 self._hid.close()
161 self._hid = None
162
163 def get_id(self):
164 return self._send_command(__GETSERIAL)
165
166 def _do_enable(self):
167 if not self.is_connected:
168 self.open()
169 self._send_command(__SETONOFF, __RUN)
170 return self._send_command(__GETONOFF) == __RUN
171
172 def _do_disable(self):
173 self._send_command(__SETONOFF, __SLEEP)
174
175 def set_calibration(self, state):
176 if state:
177 result = self._send_command(__SETCAL, __CALON)
178 else:
179 result = self._send_command(__SETCAL, __CALOFF)
180 return result
181
183 """Get the current slide position"""
184 result = self._send_command(__GETSLIDE)
185 if result is None:
186 raise microscope.DeviceError("Slide position error.")
187 return result
188
189 def set_slide_position(self, position, blocking=True):
190 """Set the slide position"""
191 result = self._send_command(__SETSLIDE, position)
192 if result is None:
193 raise microscope.DeviceError("Slide position error.")
194 while blocking and self.moving():
195 pass
196 return result
197
198 def get_slides(self):
199 return self._slide_to_sectioning
200
201 def get_status(self):
202 # Fetch 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,??
203 result = self._send_command(__FULLSTAT)
204 if result is None:
205 return
206 # A status dict to populate and return
207 status = {}
208 # A list to track states, any one of which mean the device is busy.
209 busy = []
210 # Disk running
211 status["on"] = result[3] == __RUN
212 # Door open
213 # Note - it appears that the __DOOROPEN and __DOORCLOSED status states
214 # are switched, or that the DOOR is in fact an internal shutter. I'll
215 # interpret 'door' as the external door here, as that is what the user
216 # can see. When the external door is open, result[4] == __DOORCLOSED
217 door = result[4] == __DOORCLOSED
218 status["door open"] = door
219 busy.append(door)
220 # Slide position
221 slide = result[5]
222 if slide == __SLDMID:
223 # Slide is moving
224 status["slide"] = (None, "moving")
225 busy.append(True)
226 else:
227 status["slide"] = (
228 slide,
229 self._slide_to_sectioning.get(slide, None),
230 )
231 # Filter position
232 filter = result[6]
233 if filter == __FLTMID:
234 # Filter is moving
235 status["filter"] = (None, "moving")
236 busy.append(True)
237 else:
238 status["filter"] = result[6]
239 # Calibration LED on
240 status["calibration"] = result[7] == __CALON
241 # Slide or filter moving
242 status["busy"] = any(busy)
243 return status
244
245 # Implemented by FilterWheelBase
246 # def get_filters(self):
247 # pass
248
249 def moving(self):
250 """Report whether or not the device is between positions."""
251 # Wait a short time to avoid false negatives when called
252 # immediately after initiating a move. Trial and error
253 # indicates a delay of 50ms is required.
254 time.sleep(0.05)
255 # Can return false negatives on long moves, so OR 5 readings.
256 moving = False
257 for i in range(5):
258 moving = moving or any(
259 (
260 self.get_slide_position() == __SLDMID,
261 self.get_position() == __FLTMID,
262 )
263 )
264 time.sleep(0.01)
265 return moving
266
267 def _do_get_position(self):
268 """Return the current filter position"""
269 result = self._send_command(__GETFILT)
270 if result == __FLTERR:
271 raise microscope.DeviceError("Filter position error.")
272 return result
273
274 def _do_set_position(self, pos, blocking=True):
275 """Set the filter position"""
276 result = self._send_command(__SETFILT, pos)
277 if result is None:
278 raise microscope.DeviceError("Filter position error.")
279 while blocking and self.moving():
280 pass
281 return result
282
283 def _do_shutdown(self) -> None:
284 pass
def set_slide_position(self, position, blocking=True)
Definition: aurox.py:189
def _send_command(self, command, param=0, max_length=16, timeout_ms=100)
Definition: aurox.py:113