def __init__(self, com, baud=9600, timeout=2.0, **kwargs): super().__init__(**kwargs) self.connection = serial.Serial( port=com, baudrate=baud, timeout=timeout, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, ) # If the laser is currently on, then we need to use 7-byte mode; otherwise we need to # use 16-byte mode. self._write(b"S?") response = self._readline() _logger.info("Current laser state: [%s]", response.decode()) self._write(b"STAT3") option_codes = self._readline() if not option_codes.startswith(b"OC "): raise microscope.DeviceError("Failed to get option codes '%s'" % option_codes.decode()) if option_codes[9:12] == b"AP1": self._has_apc = True else: _logger.warning("Laser is missing APC option. Will return set" " power instead of actual power") self._has_apc = False
def _do_shutdown(self): status = TMC.SBC_Close(self.serial_number) if status: message = "Error" if status in TMC.errors_dict.keys(): message = TMC.errors_dict[status] raise microscope.DeviceError(message)
def __init__(self, shared_serial: microscope._utils.SharedSerial) -> None: self._serial = shared_serial self._serial.readlines() # discard anything that may be on the line if self.get_system_type() != "iChrome-MLE": raise microscope.DeviceError("not an iChrome MLE device")
def _send_command(self, command, param=0, max_length=16, timeout_ms=100): """Send a command to the Clarity and return its response""" if not self._hid: self.open() with self._lock: # The device expects a list of 16 integers buffer = [0x00] * max_length # The 0th element must be 0. buffer[1] = command # The 1st element is the command buffer[2] = param # The 2nd element is any command argument. result = self._hid.write(buffer) if result == -1: # Nothing to read back. Check hid error state. err = self._hid.error() if err != "": self.close() raise microscope.DeviceError(err) else: return None while True: # Read responses until we see the response to our command. # (We should get the correct response on the first read.) response = self._hid.read(result - 1, timeout_ms) if not response: # No response return None elif response[0] == command: break bytes = self._resultlen.get(command, None) if bytes is None: return response[1:] elif bytes == 1: return response[1] else: return response[1:]
def show_power_uW(self) -> float: """Returns actual laser power in µW.""" answer = self.command(b"show power") if not answer.startswith(b"PIC = ") and not answer.endswith(b" uW "): raise microscope.DeviceError( "failed to parse power from answer: %s" % answer) return float(answer[7:-5])
def move_by(self, delta): status = TMC.SBC_MoveRelative(self.serial_number, self.axis, delta) if status: message = "Error" if status in TMC.errors_dict.keys(): message = TMC.errors_dict[status] raise microscope.DeviceError(message)
def command_and_answer(self, *TX_tokens: bytes) -> bytes: # Command contains two or more tokens. The first token for a # TX (transmitted) command string is one of the two keywords # GET, SET (to query or to set). The second token is the # command name. assert len(TX_tokens) >= 2, "invalid command with less than two tokens" assert TX_tokens[0] in (b"GET", b"SET"), "invalid command (not SET/GET)" TX_command = b" ".join(TX_tokens) + b"\n" with self._serial.lock: self._serial.write(TX_command) answer = self._serial.readline() RX_tokens = answer.split(maxsplit=2) # A received answer has at least two tokens. The first token # is A or E (for success or failure). The second token is the # command name (second token of the transmitted command). if ( len(RX_tokens) < 2 or RX_tokens[0] != b"A" or RX_tokens[1] != TX_tokens[1] ): raise microscope.DeviceError( "command %s failed: %s" % (TX_command, answer) ) return answer
def command(self, command: bytes) -> bytes: """Run command and return answer after minimal validation. The output of a command has the format:: \r\nANSWER\r\n[OK]\r\n The returned bytes only include `ANSWER` without its own final `\r\n`. This means that the return value might be an empty array of bytes. """ # We expect to be on 'talk usual' mode without prompt so each # command will end with [OK] on its own line. with self._serial.lock: self._serial.write(command + b"\r\n") # An answer always starts with \r\n so there will be one # before [OK] even if this command is not a query. answer = self._serial.read_until(b"\r\n[OK]\r\n") if not answer.startswith(b"\r\n"): raise microscope.DeviceError( "answer to command %s does not start with CRLF." " This may be leftovers from a previous command:" " %s" % (command, answer) ) if not answer.endswith(b"\r\n[OK]\r\n"): raise microscope.DeviceError( "Command %s failed or failed to read answer: %s" % (command, answer) ) # If an error occurred, the answer still ends in [OK]. We # need to check if the second line (first line is \r\n) is an # error code with the format "%SYS-L-XXX, error description" # where L is the error level (I for Information, W for # Warning, E for Error, and F for Fatal), and XXX is the error # code number. if answer[2:7] == b"%SYS-" and answer[7] != ord(b"I"): # Errors of level I (information) should not raise an # exception since they can be replies to normal commands. raise microscope.DeviceError( "Command %s failed: %s" % (command, answer) ) # Exclude the first \r\n, the \r\n from a possible answer, and # the final [OK]\r\n return answer[2:-8]
def param_set(self, name: bytes, value: bytes) -> None: """Change parameter (`param-set!` operator).""" answer = self._param_command(b"(param-set! '%s %s)" % (name, value)) status = int(answer) if status < 0: raise microscope.DeviceError( "Failed to set parameter %s (return value %d)" % (name.decode(), status))
def move_to(self, pos): # pos: int status = TMC.SBC_MoveToPosition(self.serial_number, self.axis, pos) if status: message = "Error" if status in TMC.errors_dict.keys(): message = TMC.errors_dict[status] raise microscope.DeviceError(message)
def set_slide_position(self, position, blocking=True): """Set the slide position""" result = self._send_command(__SETSLIDE, position) if result is None: raise microscope.DeviceError("Slide position error.") while blocking and self.moving(): pass return result
def _do_set_position(self, pos, blocking=True): """Set the filter position""" result = self._send_command(__SETFILT, pos) if result is None: raise microscope.DeviceError("Filter position error.") while blocking and self.moving(): pass return result
def _readline(self): """Read a line from connection without leading and trailing whitespace. We override from SerialDeviceMixin """ response = self.connection.readline().strip() if self.connection.readline().strip() != b"OK": raise microscope.DeviceError( "Did not get a proper answer from the laser serial comm.") return response
def get_is_on(self) -> bool: state = self._conn.status_laser() if state == b"ON": return True elif state == b"OFF": return False else: raise microscope.DeviceError("Unexpected laser status: %s" % state.decode())
def show_max_power(self) -> float: # There should be a cleaner way to get these, right? We can # query the current limits (mA) but how do we go from there to # the power limits (mW)? table = self.command(b"show satellite") key = _get_table_value(table, b"Pmax") if not key.endswith(b" mW"): raise microscope.DeviceError("failed to parse power from %s" % key) return float(key[:-3])
def get_css(self) -> bytes: """Get the global channel status map.""" with self._serial.lock: self._serial.write(b"CSS?\n") answer = self._serial.readline() if not answer.startswith(b"CSS"): raise microscope.DeviceError( "answer to 'CSS?' should start with 'CSS'" " but got '%s' instead" % answer.decode) return answer[3:-2] # remove initial b'CSS' and final b'\r\n'
def set_css(self, css: bytes) -> None: """Set status for any number of channels.""" assert len(css) % 6 == 0, "css must be multiple of 6 (6 per channel)" with self._serial.lock: self._serial.write(b"CSS" + css + b"\n") answer = self._serial.readline() if not answer.startswith(b"CSS"): raise microscope.DeviceError( "answer to 'CSS?' should start with 'CSS'" " but got '%s' instead" % answer.decode)
def enable(self): # homing # !!! Might need to wait for homing to finish axis by axis for channel_nr in range(self.n_axes): status = TMC.SBC_Home(self.serial_number, channel_nr + 1) if status: message = "Error" if status in TMC.errors_dict.keys(): message = TMC.errors_dict[status] raise microscope.DeviceError(message)
def get_light_state(self) -> bool: """On (True) or off (False) state""" # We use CHACT (actual light state) instead of CH (light # state) because CH checks both the TTL inputs and channel # state switches. state = self._conn.get_command(b"CHACT", self._index_bytes) if state == b"1": return True elif state == b"0": return False else: raise microscope.DeviceError("unexpected answer")
def __init__(self, conn: _iChromeConnection, laser_number: int) -> None: self._conn = conn self._param_prefix = b"laser%d:" % laser_number # We Need to confirm that indeed there is a laser at this # position. There is no command to check this, we just try to # read a parameter and check if it works. try: self.get_label() except microscope.DeviceError as ex: raise microscope.DeviceError( "failed to get label, probably no laser %d" % laser_number) from ex
def wait_until_idle(self, timeout: float = 10.0) -> None: """Wait, or error, until device is idle. A device is busy if *any* of its axis is busy. """ sleep_interval = 0.1 for _ in range(int(timeout / sleep_interval)): if not self.is_busy(): break time.sleep(sleep_interval) else: raise microscope.DeviceError("device still busy after %f seconds" % timeout)
def _get_table_value(table: bytes, key: bytes) -> bytes: """Get the value for a key in a table/multiline output. Some commands return something like a table of key/values. There may be even empty lines on this table. This searches for the first line with a specific key (hopefully there's only one line with such key) and returns the associated value. """ # Key might be the first line, hence '(?:^|\r\n)' match = re.search(b"(?:^|\r\n) *" + key + b": (.*)\r\n", table) if match is None: raise microscope.DeviceError("failed to find key %s on table: %s" % (key, table)) return match.group(1)
def __init__(self, port: str): # From the Toptica iBeam SMART manual: # Direct connection via COMx with 115200,8,N,1 and serial # interface handshake "none". That means that no hardware # handshake (DTR, RTS) and no software handshake (XON,XOFF) of # the underlying operating system is supported. serial_conn = serial.Serial( port=port, baudrate=115200, timeout=1.0, bytesize=serial.EIGHTBITS, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, xonxoff=False, rtscts=False, dsrdtr=False, ) self._serial = _SharedSerial(serial_conn) # We don't know what is the current verbosity state and so we # don't know yet what we should be reading back. So blindly # set to the level we want, flush all output, and then check # if indeed this is a Toptica iBeam device. with self._serial.lock: self._serial.write(b"echo off\r\n") self._serial.write(b"prompt off\r\n") # The talk level we want is 'usual'. In theory we should # be able to use 'quiet' which only answers queries but in # practice 'quiet' does not answer some queries like 'show # serial'. self._serial.write(b"talk usual\r\n") self._serial.readlines() # discard all pending lines # Empty command does nothing and returns nothing extra so we # use it to ensure this at least behaves like a Toptica iBeam. try: self.command(b"") except microscope.DeviceError as e: raise microscope.InitialiseError( "Failed to confirm Toptica iBeam on %s" % (port) ) from e answer = self.command(b"show serial") if not answer.startswith(b"SN: "): raise microscope.DeviceError( "Failed to parse serial from %s" % answer ) _logger.info("got connection to Toptica iBeam %s", answer.decode())
def _do_trigger(self) -> None: """Convenience fallback. This only provides a convenience fallback for devices that don't support queuing multiple patterns and software trigger, i.e., devices that take only one pattern at a time. This is not the case of most devices. Devices that support queuing patterns, should override this method. .. todo:: instead of a convenience fallback, we should have a separate mixin for this. """ if self._patterns is None: raise microscope.DeviceError("no pattern queued to apply") self._pattern_idx += 1 self.apply_pattern(self._patterns[self._pattern_idx, :])
def _do_get_power(self) -> float: if not self.get_is_on(): return 0.0 if self._has_apc: query = b"P" scale = 0xCCC else: query = b"PP" scale = 0xFFF self._write(query + b"?") answer = self._readline() if not answer.startswith(query): raise microscope.DeviceError("failed to read power from '%s'" % answer.decode()) level = int(answer[len(query):], 16) return float(level) / float(scale)
def _fetch_data(self): if self._acquiring and self._triggered > 0: if random.randint(0, 100) < self._error_percent: _logger.info("Raising exception") raise microscope.DeviceError( "Exception raised in TestCamera._fetch_data" ) _logger.info("Sending image") time.sleep(self._exposure_time) self._triggered -= 1 # Create an image dark = int(32 * np.random.rand()) light = int(255 - 128 * np.random.rand()) width = self._roi.width // self._binning.h height = self._roi.height // self._binning.v image = self._image_generator.get_image( width, height, dark, light, index=self._sent ) self._sent += 1 return image
def _param_command(self, command: bytes) -> bytes: """Run command and return raw answer (minus prompt and echo).""" command = command + b"\r\n" with self._serial.lock: self._serial.write(command) answer = self._serial.read_until(b"\r\n> ") # When we read, we are reading the whole command console # including the prompt and even the command is echoed back. assert answer[:len(command)] == command and answer[-4:] == b"\r\n> " # Errors are indicated by the string "Error: " at the # beginning of a new line. if answer[len(command):len(command) + 7] == b"Error: ": base_command = command[:-2] error_msg = answer[len(command) + 8:-4] raise microscope.DeviceError( "error on command '%s': %s" % (base_command.decode(), error_msg.decode())) # Return the answer minus the "echoed" command and the prompt # for the next command. return answer[len(command):-4]
def _raise_status(self, func: typing.Callable) -> None: error_code = self._status.contents.value raise microscope.DeviceError( "mro_%s() failed (error code %d)" % (func.__name__, error_code) )
def get_slide_position(self): """Get the current slide position""" result = self._send_command(__GETSLIDE) if result is None: raise microscope.DeviceError("Slide position error.") return result
def _do_get_position(self): """Return the current filter position""" result = self._send_command(__GETFILT) if result == __FLTERR: raise microscope.DeviceError("Filter position error.") return result