|
| 1 | +#!/usr/bin/python |
| 2 | +# -*- coding: utf-8 |
| 3 | +# |
| 4 | +# Copyright 2019 Mick Phillips (mick.phillips@gmail.com) |
| 5 | +# |
| 6 | +# This program is free software: you can redistribute it and/or modify |
| 7 | +# it under the terms of the GNU General Public License as published by |
| 8 | +# the Free Software Foundation, either version 3 of the License, or |
| 9 | +# (at your option) any later version. |
| 10 | +# |
| 11 | +# This program is distributed in the hope that it will be useful, |
| 12 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +# GNU General Public License for more details. |
| 15 | +# |
| 16 | +# You should have received a copy of the GNU General Public License |
| 17 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 18 | + |
| 19 | +"""Adds support for Aurox devices |
| 20 | +
|
| 21 | +Requires package hidapi.""" |
| 22 | + |
| 23 | +import hid |
| 24 | +import microscope.devices |
| 25 | +from enum import Enum |
| 26 | + |
| 27 | +## Clarity constants. These may differ across products, so mangle names. |
| 28 | +# USB IDs |
| 29 | +_Clarity__VENDORID = 0x1F0A |
| 30 | +_Clarity__PRODUCTID = 0x0088 |
| 31 | +# Base status |
| 32 | +_Clarity__SLEEP = 0x7f |
| 33 | +_Clarity__RUN = 0x0f |
| 34 | +# Door status |
| 35 | +_Clarity__DOOROPEN = 0x01 |
| 36 | +_Clarity__DOORCLOSED = 0x02 |
| 37 | +# Disk position/status |
| 38 | +_Clarity__SLDPOS0 = 0x00 #disk out of beam path, wide field |
| 39 | +_Clarity__SLDPOS1 = 0x01 #disk pos 1, low sectioning |
| 40 | +_Clarity__SLDPOS2 = 0x02 #disk pos 2, mid sectioning |
| 41 | +_Clarity__SLDPOS3 = 0x03 #disk pos 3, high sectioning |
| 42 | +_Clarity__SLDERR = 0xff #An error has occurred in setting slide position (end stops not detected) |
| 43 | +_Clarity__SLDMID = 0x10 #slide in mid position (was =0x03 for SD62) |
| 44 | +# Filter position/status |
| 45 | +_Clarity__FLTPOS1 = 0x01 #Filter in position 1 |
| 46 | +_Clarity__FLTPOS2 = 0x02 #Filter in position 2 |
| 47 | +_Clarity__FLTPOS3 = 0x03 #Filter in position 3 |
| 48 | +_Clarity__FLTPOS4 = 0x04 #Filter in position 4 |
| 49 | +_Clarity__FLTERR = 0xff #An error has been detected in the filter drive (eg filters not present) |
| 50 | +_Clarity__FLTMID = 0x10 #Filter in mid position |
| 51 | +# Calibration LED state |
| 52 | +_Clarity__CALON = 0x01 #CALibration led power on |
| 53 | +_Clarity__CALOFF = 0x02 #CALibration led power off |
| 54 | +# Error status |
| 55 | +_Clarity__CMDERROR = 0xff #Reply to a command that was not understood |
| 56 | +# Commands |
| 57 | +_Clarity__GETVERSION = 0x00 #Return 3-byte version number byte1.byte2.byte3 |
| 58 | +# State commands: single command byte immediately followed by any data. |
| 59 | +_Clarity__GETONOFF = 0x12 #No data out, returns 1 byte on/off status |
| 60 | +_Clarity__GETDOOR = 0x13 #No data out, returns 1 byte shutter status, or SLEEP if device sleeping |
| 61 | +_Clarity__GETSLIDE = 0x14 #No data out, returns 1 byte disk-slide status, or SLEEP if device sleeping |
| 62 | +_Clarity__GETFILT = 0x15 #No data out, returns 1 byte filter position, or SLEEP if device sleeping |
| 63 | +_Clarity__GETCAL = 0x16 #No data out, returns 1 byte CAL led status, or SLEEP if device sleeping |
| 64 | +_Clarity__GETSERIAL = 0x19 #No data out, returns 4 byte BCD serial number (little endian) |
| 65 | +_Clarity__FULLSTAT = 0x1f #No data, Returns 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,?? |
| 66 | +# Run state action commands |
| 67 | +_Clarity__SETONOFF = 0x21 #1 byte out on/off status, echoes command or SLEEP |
| 68 | +_Clarity__SETSLIDE = 0x23 #1 byte out disk position, echoes command or SLEEP |
| 69 | +_Clarity__SETFILT = 0x24 #1 byte out filter position, echoes command or SLEEP |
| 70 | +_Clarity__SETCAL = 0x25 #1 byte out CAL led status, echoes command or SLEEP |
| 71 | +# Service mode commands. Stops disk spinning for alignment. |
| 72 | +_Clarity__SETSVCMODE1 = 0xe0 #1 byte for service mode. SLEEP activates service mode. RUN returns to normal mode. |
| 73 | + |
| 74 | + |
| 75 | +class Clarity(microscope.devices.FilterWheelBase): |
| 76 | + _slide_to_sectioning = {__SLDPOS0: 'bypass', |
| 77 | + __SLDPOS1: 'low', |
| 78 | + __SLDPOS2: 'mid', |
| 79 | + __SLDPOS3: 'high', |
| 80 | + __SLDMID: 'mid',} |
| 81 | + _positions = 4 |
| 82 | + |
| 83 | + def __init__(self, *args, **kwargs): |
| 84 | + super().__init__(self, *args, **kwargs) |
| 85 | + from threading import Lock |
| 86 | + self._lock = Lock() |
| 87 | + self._hid = None |
| 88 | + self.add_setting("sectioning", "enum", |
| 89 | + self.get_slide_position, |
| 90 | + lambda val: self.set_slide_position(val), |
| 91 | + self._slide_to_sectioning) |
| 92 | + |
| 93 | + def _send_command(self, command, param=0, max_length=16, timeout_ms=100): |
| 94 | + if not self._hid: |
| 95 | + raise Exception("Not connected to device.") |
| 96 | + with self._lock: |
| 97 | + buffer = [0x00] * max_length |
| 98 | + buffer[0] = command |
| 99 | + buffer[1] = param |
| 100 | + result = self._hid.write(buffer) |
| 101 | + response = self._hid.read(max_length, timeout_ms) |
| 102 | + if response[0] != command: |
| 103 | + return None |
| 104 | + else: |
| 105 | + return response[1:] |
| 106 | + |
| 107 | + @property |
| 108 | + def is_connected(self): |
| 109 | + return self._hid is not None |
| 110 | + |
| 111 | + def open(self): |
| 112 | + try: |
| 113 | + h = hid.device() |
| 114 | + h.open(vendor_id=__VENDORID, product_id=__PRODUCTID) |
| 115 | + h.set_nonblocking(False) |
| 116 | + except: |
| 117 | + raise |
| 118 | + self._hid = h |
| 119 | + |
| 120 | + def close(self): |
| 121 | + if self.is_connected: |
| 122 | + self._hid.close() |
| 123 | + self._hid = None |
| 124 | + |
| 125 | + def get_id(self): |
| 126 | + return self._send_command(__GETSERIAL) |
| 127 | + |
| 128 | + def _on_enable(self): |
| 129 | + if not self.is_connected: |
| 130 | + self.open() |
| 131 | + self._send_command(__SETONOFF, __RUN) |
| 132 | + return self._send_command(__GETONOFF) == __RUN |
| 133 | + |
| 134 | + def _on_disable(self): |
| 135 | + self._send_command(__SETONOFF, __SLEEP) |
| 136 | + |
| 137 | + def set_calibration(self, state): |
| 138 | + if state: |
| 139 | + result = self._send_command(__SETCAL, __CALON) |
| 140 | + else: |
| 141 | + result = self._send_command(__SETCAL, __CALOFF) |
| 142 | + return result |
| 143 | + |
| 144 | + def get_slide_position(self): |
| 145 | + """Get the current slide position""" |
| 146 | + result = self._slide_to_sectioning.get(self._send_command(__GETSLIDE), None) |
| 147 | + if result is None: |
| 148 | + raise Exception("Slide position error.") |
| 149 | + return result |
| 150 | + |
| 151 | + def set_slide_position(self, position): |
| 152 | + """Set the slide position""" |
| 153 | + result = self._send_command(__SETSLIDE, position) |
| 154 | + if result is None: |
| 155 | + raise Exception("Slide position error.") |
| 156 | + return result |
| 157 | + |
| 158 | + def get_slides(self): |
| 159 | + return (self._slide_to_sectioning) |
| 160 | + |
| 161 | + def get_status(self): |
| 162 | + # Fetch 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,?? |
| 163 | + result = self._send_command(__FULLSTAT) |
| 164 | + status = {} |
| 165 | + status['on'] = result[3] == __RUN |
| 166 | + slide = result[4] |
| 167 | + status['slide'] = (slide, self._slide_to_sectioning.get(slide, None)) |
| 168 | + status['filter'] = (result[6], self._filters.get(result[6], None)) |
| 169 | + status['calibration'] == result[7] == __CALON |
| 170 | + return status |
| 171 | + |
| 172 | + # Implemented by FilterWheelBase |
| 173 | + #def get_filters(self): |
| 174 | + # pass |
| 175 | + |
| 176 | + def get_position(self): |
| 177 | + """Return the current filter position""" |
| 178 | + result = self._send_command(__GETFILT) |
| 179 | + if result == __FLTERR: |
| 180 | + raise Exception("Filter position error.") |
| 181 | + return result |
| 182 | + |
| 183 | + def set_position(self, pos): |
| 184 | + """Set the filter position""" |
| 185 | + result = self._send_command(__SETFILT, pos) |
| 186 | + if result is None: |
| 187 | + raise Exception("Filter position error.") |
| 188 | + return result |
0 commit comments