def __init__(self, name, role, ports, **kwargs): """ ports (string): pattern of the name of the serial ports to try to connect to find the devices. It can have a "glob", for example: "/dev/ttyUSB*" """ model.Emitter.__init__(self, name, role, **kwargs) self._ports = ports self._master, self._devices = self._getAvailableDevices(ports) if not self._devices: raise HwError( "No Omicron xX device found for ports '%s', check " "that '%s' is turned on and connected to the computer." % (ports, name)) spectra = [ ] # list of tuples: 99% low, 25% low, centre, 25% high, 99% high in m max_power = [] # list of float (W) for d in self._devices: spectra.append(d.wavelength) max_power.append(d.max_power) self._shape = () # power of the whole device (=> max power of the device with max power) self.power = model.FloatContinuous(0., (0., max(max_power)), unit="W") self.power.subscribe(self._updatePower) # ratio of power per device # => if some device don't support max power, clamped before 1 self.emissions = model.ListVA([0.] * len(self._devices), unit="", setter=self._setEmissions) # info on what device is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True) # Ensure the whole Hub is turned on if self._master: try: self._master.PowerOn() except OXXError: raise HwError( "Failed to power on the master device, check the interlock." ) # make sure everything is off (turning on the HUB will turn on the lights) self._updateIntensities(self.power.value, self.emissions.value) # set SW version driver_name = self._devices[0].acc.driver self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
def _openDevice(self, sn=None): """ sn (None or str): serial number return (0 <= int < 8): device ID raises: HwError if the device doesn't exist or cannot be opened """ sn_str = create_string_buffer(8) for i in range(MAXDEVNUM): try: self._dll.PH_OpenDevice(i, sn_str) except PHError as ex: if ex.errno == -1: # ERROR_DEVICE_OPEN_FAIL == no device with this idx pass else: logging.warning("Failure to open device %d: %s", i, ex) continue if sn is None or sn_str.value == sn: return i else: logging.info("Skipping device %d, with S/N %s", i, sn_str.value) else: # TODO: if a PHError happened indicate the error in the message raise HwError( "No PicoHarp300 found, check the device is turned on and connected to the computer" )
def __init__(self, name, role, axes, ranges=None, choices=None, **kwargs): """ axes (set of string): names of the axes ranges (dict string -> float,float): min/max of the axis choices (dict string -> set): alternative to ranges, these are the choices of the axis """ assert len(axes) > 0 # Special file "stage.fail" => will cause simulation of hardware error if os.path.exists("stage.fail"): raise HwError("stage.fail file present, simulating error") # Transform the input style of the Stage to the style of the GenericComponent if ranges is None: ranges = {} if choices is None: choices = {} axes_dict = {} for a in axes: d = {} if a in ranges: d["range"] = ranges[a] elif a in choices: d["choices"] = choices[a] axes_dict[a] = d GenericComponent.__init__(self, name, role, vas=None, axes=axes_dict, **kwargs)
def _tryRecover(self): self._recovering = True self.state._set_value(HwError("Connection lost, reconnecting..."), force_write=True) # Retry to open the serial port (in case it was unplugged) # _ser_access should already be acquired, but since it's an RLock it can be acquired # again in the same thread try: with self._ser_access: while True: if self._serial: self._serial.close() self._serial = None try: logging.debug("Searching for the device on port %s", self._portpattern) min_axis = min(self._axis_map.values()) self._port = self._findDevice(self._portpattern, min_axis) except IOError: time.sleep(2) except Exception: logging.exception( "Unexpected error while trying to recover device") raise else: # We found it back! break # it now should be accessible again self.state._set_value(model.ST_RUNNING, force_write=True) logging.info("Recovered device on port %s", self._port) finally: self._recovering = False
def _openSerialPort(port): """ Opens the given serial port the right way for a PiezoMotor PMD device. port (string): the name of the serial port (e.g., /dev/ttyUSB0) return (serial): the opened serial port raise HwError: if the serial port cannot be opened (doesn't exist, or already opened) """ # For debugging purpose if port == "/dev/fake": return PMDSimulator(timeout=0.1) try: ser = serial.Serial( port=port, baudrate=BAUDRATE, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=0.3, # s ) except IOError: raise HwError("Failed to find a PMD controller on port '%s'. " "Check that the device is turned on and " "connected to the computer." % (port, )) return ser
def __init__(self, name, role, axes, ranges=None, **kwargs): """ axes (set of string): names of the axes ranges (dict string -> float,float): min/max of the axis """ assert len(axes) > 0 if ranges is None: ranges = {} axes_def = {} self._position = {} init_speed = {} for a in axes: rng = ranges.get(a, (-0.1, 0.1)) axes_def[a] = model.Axis(unit="m", range=rng, speed=(0., 10.)) # start at the centre self._position[a] = (rng[0] + rng[1]) / 2 init_speed[a] = 1.0 # we are fast! model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs) # Special file "stage.fail" => will cause simulation of hardware error if os.path.exists("stage.fail"): raise HwError("stage.fail file present, simulating error") self._executor = model.CancellableThreadPoolExecutor(max_workers=1) # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute({}, unit="m", readonly=True) self._updatePosition() self.speed = model.MultiSpeedVA(init_speed, (0., 10.), "m/s")
def _sendCommand(self, cmd): """ cmd (str): command to be sent to Power Control unit. returns (str): answer received from the Power Control unit raises: IOError: if an ERROR is returned by the Power Control firmware. """ cmd = (cmd + "\n").encode('latin1') with self._ser_access: logging.debug("Sending command %s" % to_str_escape(cmd)) self._serial.write(cmd) ans = b'' char = None while char != b'\n': char = self._serial.read() if not char: logging.error("Timeout after receiving %s", to_str_escape(ans)) # TODO: See how you should handle a timeout before you raise # an HWError raise HwError( "Power Control Unit connection timeout. " "Please turn off and on the power to the box.") # Handle ERROR coming from Power control unit firmware ans += char logging.debug("Received answer %s", to_str_escape(ans)) ans = ans.decode('latin1') if ans.startswith("ERROR"): raise PowerControlError(ans.split(' ', 1)[1]) return ans.rstrip()
def __init__(self, name, role, axes, ranges=None, **kwargs): """ axes (set of string): names of the axes """ assert len(axes) > 0 if ranges is None: ranges = {} axes_def = {} self._position = {} init_speed = {} for a in axes: rng = ranges.get(a, [-0.1, 0.1]) axes_def[a] = model.Axis(unit="m", range=rng, speed=[0., 10.]) # start at the centre self._position[a] = (rng[0] + rng[1]) / 2 init_speed[a] = 10.0 # we are super fast! model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs) # Special file "stage.fail" => will cause simulation of hardware error if os.path.exists("stage.fail"): raise HwError("stage.fail file present, simulating error") # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute(self._applyInversionAbs( self._position), unit="m", readonly=True) self.speed = model.MultiSpeedVA(init_speed, [0., 10.], "m/s")
def _findDevice(self, port=None, serialnum=None): """ Look for a compatible device. Requires at least one of the arguments port and serialnum. port (str): port (e.g. "/dev/ttyUSB0") or pattern for port ("/dev/ttyUSB*"), "/dev/fake" will start the simulator serialnum (str): serial number return (str): the name of the port used raises: HwError: if no device on the ports with the given serial number is found """ # At least one of the arguments port and serialnum must be specified if not port and not serialnum: raise ValueError( "At least one of the arguments 'port' and 'serialnum' must be specified." ) # For debugging purpose if port == "/dev/fake": return port # If no ports specified, check all available ports if port: names = list(serial.tools.list_ports.grep(port)) else: names = serial.tools.list_ports.comports( ) # search all serial ports # Look for serial number if available, otherwise make sure only one port matches the port pattern. if serialnum: for port in names: if serialnum in port.serial_number: return port.device # Found it! else: raise HwError( "Beam controller device with serial number %s not found for port %s. " % (serialnum, names) + "Check the connection.") else: if len(names) == 1: port = names[0] return port.device elif len(names) > 1: raise HwError( "Multiple ports detected for beam controller. Please specify a serial number." ) else: raise HwError( "Beam controller device not found for port %s. Check the connection." % port)
def _findDevice(self, ports, baudrate=19200): """ Look for a compatible device ports (str): pattern for the port name baudrate (0<int) return: (str): the name of the port used (str): the hardware version string Note: will also update ._file and ._serial raises: IOError: if no devices are found """ # For debugging purpose if ports == "/dev/fake": self._serial = ESPSimulator(timeout=1) self._file = None ve = self.GetVersion() return ports, ve if os.name == "nt": raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use self._file = open(n) # Open in RO, just to check for lock try: fcntl.flock( self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) # Raises IOError if cannot lock except IOError: logging.info("Port %s is busy, will not use", n) continue self._serial = self._openSerialPort(n, baudrate) try: ve = self.GetVersion() except ESPError as e: # Can happen if the device has received some weird characters # => try again (now that it's flushed) logging.info( "Device answered by an error %s, will try again", e) ve = self.GetVersion() return n, ve except (IOError, ESPError) as e: logging.debug(e) logging.info( "Skipping device on port %s, which didn't seem to be compatible", n) # not possible to use this port? next one! continue else: raise HwError( "Failed to find a device on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def _sendCommand(self, cmd): """ cmd (byte str): command to be sent to PMT Control unit. returns (byte str): answer received from the PMT Control unit raises: PMTControlError: if an ERROR is returned by the PMT Control firmware. HwError: in case the of connection timeout """ cmd = cmd + b"\n" with self._ser_access: logging.debug("Sending command %s", to_str_escape(cmd)) try: self._serial.write(cmd) except IOError: logging.warn("Failed to send command to PMT Control firmware, " "trying to reconnect.") if self._recovering: raise else: self._tryRecover() # send command again logging.debug( "Sending command %s again after auto-reconnect" % to_str_escape(cmd)) return self._sendCommand(cmd[:-1]) # cmd without \n ans = b'' char = None while char != b'\n': try: char = self._serial.read() except IOError: logging.warn("Failed to read from PMT Control firmware, " "trying to reconnect.") if self._recovering: raise else: self._tryRecover() # don't send command again raise IOError( "Failed to read from PMT Control firmware, " "restarted serial connection.") if not char: logging.error("Timeout after receiving %s", to_str_escape(ans)) # TODO: See how you should handle a timeout before you raise # an HWError raise HwError( "PMT Control Unit connection timeout. " "Please turn off and on the power to the box.") # Handle ERROR coming from PMT control unit firmware ans += char logging.debug("Received answer %s", to_str_escape(ans)) if ans.startswith(b"ERROR"): raise PMTControlError(ans.split(b' ', 1)[1]) return ans.rstrip()
def _openConnection(cls, address, sn=None): """ return (Accesser) """ if address == "fake": host, port = "fake", 23 elif address == "autoip": conts = cls._scanOverIP() if sn is not None: for hn, host, port in conts: # Open connection to each controller and ask for their SN dev = cls(None, None, address=host, axes=["a"], stepsize=[1e-6]) _, _, devsn = dev.GetIdentification() if sn == devsn: break else: raise HwError( "Failed to find New Focus controller %s over the " "network. Ensure it is turned on and connected to " "the network." % (sn, )) else: # just pick the first one # TODO: only pick the ones of model 8742 try: hn, host, port = conts[0] logging.info("Connecting to New Focus %s", hn) except IndexError: raise HwError( "Failed to find New Focus controller over the " "network. Ensure it is turned on and connected to " "the network.") else: # split the (IP) port, separated by a : if ":" in address: host, ipport_str = address.split(":") port = int(ipport_str) else: host = address port = 23 # default return IPAccesser(host, port)
def __init__(self, name, role, sn=None, max_power=0.1, inversed=False, **kwargs): """ sn (None or str): serial number. If None, it will pick the first device found. max_power (0<float): maxium power emitted in W. """ model.Emitter.__init__(self, name, role, **kwargs) self._sn = sn self._max_power = max_power # Just find the first BlinkStick led controller if sn is None: self._bstick = blinkstick.find_first() else: # Note: doesn't work with v1.1.7: # need fix on get_string(), reported here: https://github.com/arvydas/blinkstick-python/pull/35 logging.warning( "Using sn to select the device doesn't currently work") self._bstick = blinkstick.find_by_serial(sn) if self._bstick is None: raise HwError( "Failed to find a Blinkstick for component %s. " "Check that the device is connected to the computer." % (name, )) self._bstick.set_inverse(inversed) time.sleep(0.1) # Device apparently needs some time to recover self._shape = () # list of 5-tuples of floats self.spectra = model.ListVA([(380e-9, 390e-9, 560e-9, 730e-9, 740e-9)], unit="m", readonly=True) self.power = model.ListContinuous([ 0., ], ((0., ), (max_power, )), unit="W", cls=(int, long, float), setter=self._setPower) self.power.subscribe(self._updatePower, init=True) self._swVersion = "Blinkstick v%s" % (blinkstick.__version__, ) # These functions report wrong values on Linux with v1.1.7 # man = self._bstick.get_manufacturer() # desc = self._bstick.get_description() # rsn = self._bstick.get_serial() man = self._bstick.device.manufacturer desc = self._bstick.device.product rsn = self._bstick.device.serial_number self._hwVersion = "%s %s (s/n: %s)" % (man, desc, rsn)
def _findDevice(self, ports, baudrate=9600): """ Look for a compatible device ports (str): pattern for the port name baudrate (0<int) midn (str or None): regex to match the *IDN answer return: (str): the name of the port used (str): the identification string Note: will also update ._file and ._serial raises: IOError: if no device are found """ # For debugging purpose if ports == "/dev/fake": self._serial = RemconSimulator(timeout=1) self._file = None idn = self.GetVersion() return ports, idn if os.name == "nt": raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use self._file = open(n) # Open in RO, just to check for lock try: fcntl.flock( self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) # Raises IOError if cannot lock except IOError: logging.info("Port %s is busy, will not use", n) continue self._serial = self._openSerialPort(n, baudrate) try: self.NullCommand( ) # stop if it's not the right hardware before disturbing it idn = self.GetVersion() except RemconError: # Can happen if the device has received some weird characters # => try again (now that it's flushed) logging.info("Device answered by an error, will try again") idn = self.GetVersion() return n, idn except (IOError, RemconError): logging.info( "Skipping device on port %s, which didn't seem to be compatible", n) # not possible to use this port? next one! continue else: raise HwError( "Check that Remcon32 is running, and check the connection " "to the SEM PC. No Zeiss SEM found on ports %s" % (ports, ))
def _findDevice(self, ports): """ Look for a compatible device ports (str): pattern for the port name baudrate (0<int) return: (str): the name of the port used Note: will also update ._file and ._serial raises: IOError: if no devices are found """ # For debugging purpose if ports == "/dev/fake": self._serial = LakeshoreSimulator(timeout=1) self._file = None return ports if os.name == "nt": raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use self._file = open(n) # Open in RO, just to check for lock try: fcntl.flock( self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) # Raises IOError if cannot lock except IOError: logging.info("Port %s is busy, will not use", n) continue self._serial = self._openSerialPort(n) manufacturer, md, _, _ = self.GetIdentifier( ) # if value is incorrect, will throw an exception wile unpacking if manufacturer != "LSCI": raise IOError("Invalid device manufacturer") if md != "MODEL335": raise IOError("The model is %s, not MODEL335." % (md, )) return n except (IOError, LakeshoreError) as e: logging.debug(e) logging.info( "Skipping device on port %s, which didn't seem to be compatible", n) # not possible to use this port? next one! continue else: raise HwError( "Failed to find a device on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def _findDevice(self, ports): """ Look for a compatible device ports (str): pattern for the port name return (str): the name of the port used It also sets ._serial and ._idn to contain the opened serial port, and the identification string. raises: IOError: if no device are found """ # For debugging purpose if ports == "/dev/fake": self._serial = PowerControlSimulator(timeout=1) return ports if os.name == "nt": raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: self._file = open(n) # Open in RO, just to check for lock try: fcntl.flock(self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: logging.info("Port %s is busy, will wait and retry", n) time.sleep(11) fcntl.flock(self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) self._serial = self._openSerialPort(n) try: idn = self._getIdentification() except PowerControlError: # Can happen if the device has received some weird characters # => try again (now that it's flushed) logging.info("Device answered by an error, will try again") idn = self._getIdentification() # Check that we connect to the right device if not idn.startswith("Delmic Analog Power"): logging.info("Connected to wrong device on %s, skipping.", n) continue return n except (IOError, PowerControlError): # not possible to use this port? next one! logging.debug( "Skipping port %s which doesn't seem the right device", n) continue else: raise HwError( "Failed to find a Power Control device on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def _getIdentities(self): """ Return the ids of connected EEPROMs """ try: ans = self._sendCommand("SID") except PowerControlError as e: # means there is no power provided raise HwError("There is no power provided to the Power Control Unit. " "Please make sure the board is turned on.") return [x for x in ans.split(',') if x != '']
def _findDevice(self, ports): """ Look for a compatible device ports (str): pattern for the port name return (str): the name of the port used It also sets ._serial and ._idn to contain the opened serial port, and the identification string. raises: IOError: if no device are found """ # TODO: For debugging purpose # if ports == "/dev/fake": # self._serial = DPSSSimulator(timeout=1) # return ports if os.name == "nt": raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use self._file = open(n) # Open in RO, just to check for lock try: fcntl.flock( self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) # Raises IOError if cannot lock except IOError: logging.info("Port %s is busy, will not use", n) continue self._serial = self._openSerialPort(n) try: sn = self.GetSerialNumber() except DPSSError: # Can happen if the device has received some weird characters # => try again (now that it's flushed) logging.info("Device answered by an error, will try again") sn = self.GetSerialNumber() return n except (IOError, DPSSError): logging.info( "Skipping device on port %s, which didn't seem to be a Cobolt", n) # not possible to use this port? next one! continue else: raise HwError( "Failed to find a Cobolt device on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def _findDevice(cls, ports): """ Look for a compatible device ports (str): pattern for the port name return serial, port: serial: serial port found, and open port (str): the name of the port used raises: IOError: if no device are found """ # We are called very early, so no attribute is to be expected if os.name == "nt": # TODO #ports = ["COM" + str(n) for n in range (15)] raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: ser = cls.openSerialPort(n) dev = LLE(None, None, port=None, sources=None, _serial=ser) except serial.SerialException: # not possible to use this port? next one! continue # Try to connect and get back some answer. # The LLE only answers back for the temperature try: for i in range(3): # 3 times in a row good answer? temp = dev.GetTemperature() # avoid 0 and 255 (= only 000's or 1111's), which is bad sign if not (0 < temp < 250): raise IOError() except Exception: logging.debug( "Port %s doesn't seem to have a LLE device connected", n) continue return ser, n # found it! else: raise HwError( "Failed to find a Lumencor Light Engine on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def _findDevice(self, ports): """ Look for a compatible device ports (str): pattern for the port name return (str): the name of the port used It also sets ._serial and ._idn to contain the opened serial port, and the identification string. raises: IOError: if no device are found """ if os.name == "nt": # TODO # ports = ["COM" + str(n) for n in range(15)] raise NotImplementedError("Windows not supported") else: names = glob.glob(ports) for n in names: try: self._serial = self._openSerialPort(n) except serial.SerialException: # not possible to use this port? next one! continue # check whether it looks like a FW102C try: # If any garbage was previously received, make it discarded. self._serial.write("\r") # can have some \x00 bytes at the beginning + "CMD_NOT_DEFINED" self._flushInput() idn = self.GetIdentification() if re.match(self.re_idn, idn): self._idn = idn return n # found it! except Exception: logging.debug( "Port %s doesn't seem to have a FW102C device connected", n) else: raise HwError( "Failed to find a filter wheel FW102C on ports '%s'. " "Check that the device is turned on and connected to " "the computer." % (ports, ))
def __init__(self, name, role, children, address, daemon=None, **kwargs): """ Parameters ---------- address: str server address and port of the Microscope server, e.g. "PYRO:Microscope@localhost:4242" timeout: float Time in seconds the client should wait for a response from the server. """ model.HwComponent.__init__(self, name, role, daemon=daemon, **kwargs) self._proxy_access = threading.Lock() try: self.server = Proxy(address) self.server._pyroTimeout = 30 # seconds self._swVersion = self.server.get_software_version() self._hwVersion = self.server.get_hardware_version() except Exception as err: raise HwError( "Failed to connect to XT server '%s'. Check that the " "uri is correct and XT server is" " connected to the network. %s" % (address, err)) # create the scanner child try: kwargs = children["scanner"] except (KeyError, TypeError): raise KeyError("SEM was not given a 'scanner' child") self._scanner = Scanner(parent=self, daemon=daemon, **kwargs) self.children.value.add(self._scanner) # create the stage child, if requested if "stage" in children: ckwargs = children["stage"] self._stage = Stage(parent=self, daemon=daemon, **ckwargs) self.children.value.add(self._stage) # create a focuser, if requested if "focus" in children: ckwargs = children["focus"] self._focus = Focus(parent=self, daemon=daemon, **ckwargs) self.children.value.add(self._focus)
def _getSerialPort(self, sn): """ sn (str): serial number of the device return (str): serial port name (eg: "/dev/ttyUSB0" on Linux) """ if sys.platform.startswith('linux'): # Look for each USB device, if the serial number is good sn_paths = glob.glob('/sys/bus/usb/devices/*/serial') for p in sn_paths: try: f = open(p) snp = f.read().strip() except IOError: logging.debug("Failed to read %s, skipping device", p) if snp == sn: break else: # There is a known problem with the APT devices that prevent # them from connecting to USB if they are connected via a hub # and powered on before the host PC. raise HwError("No USB device with S/N %s. " "Check that the Thorlabs filter flipper was " "turned on *after* the host computer." % sn) # Deduce the tty: # .../3-1.2/serial => .../3-1.2/3-1.2:1.0/ttyUSB1 sys_path = os.path.dirname(p) usb_num = os.path.basename(sys_path) tty_paths = glob.glob("%s/%s/ttyUSB?*" % (sys_path, usb_num + ":1.0")) if not tty_paths: raise ValueError("Failed to find tty for device with S/N %s" % sn) tty = os.path.basename(tty_paths[0]) # Convert to /dev # Note: that works because udev rules create a dev with the same name # otherwise, we would need to check the char numbers return "/dev/%s" % (tty, ) else: # TODO: Windows version raise NotImplementedError("OS not yet supported")
def __init__(self, name, role, ports, **kwargs): """ ports (string): pattern of the name of the serial ports to try to connect to find the devices. It can have a "glob", for example: "/dev/ttyUSB*" """ model.Emitter.__init__(self, name, role, **kwargs) self._ports = ports self._devices = self._getAvailableDevices(ports) if not self._devices: raise HwError("No Omicron xX device found for ports '%s', check " "they are turned on and connected to the computer." % ports) spectra = [] # list of tuples: 99% low, 25% low, centre, 25% high, 99% high in m max_power = [] # list of float (W) for d in self._devices: wl = d.wavelength # Lasers => spectrum is almost just one wl, but make it 2 nm wide # to avoid a bandwidth of exactly 0. spectra.append((wl - 1e-9, wl - 0.5e-9, wl, wl + 0.5e-9, wl + 1e-9)) max_power.append(d.max_power) self._shape = () # power of the whole device (=> max power of the device with max power) self.power = model.FloatContinuous(0., (0., max(max_power)), unit="W") self.power.subscribe(self._updatePower) # ratio of power per device # => if some device don't support max power, clamped before 1 self.emissions = model.ListVA([0.] * len(self._devices), unit="", setter=self._setEmissions) # info on what device is which wavelength self.spectra = model.ListVA(spectra, unit="m", readonly=True) # make sure everything is off self._updateIntensities(self.power.value, self.emissions.value) # set HW and SW version driver_name = driver.getSerialDriver(self._devices[0].port) self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name) self._hwVersion = "Omicron xX" # TODO: get version from GetFirmware()
def __init__(self, name, role, port, turret=None, calib=None, _noinit=False, dependencies=None, **kwargs): """ port (string): name of the serial port to connect to. turret (None or 1<=int<=3): turret number set-up. If None, consider that the current turret known by the device is correct. calib (None or list of (int, int and 5 x (float or str))): calibration data, as saved by Winspec. Data can be either in float or as an hexadecimal value "hex:9a,99,99,99,99,79,40,40" blaze in nm, groove gl/mm, center adjust, slope adjust, focal length, inclusion angle, detector angle inverted (None): it is not allowed to invert the axes dependencies (dict str -> Component): "ccd" should be the CCD used to acquire the spectrum. _noinit (boolean): for internal use only, don't try to initialise the device """ if kwargs.get("inverted", None): raise ValueError("Axis of spectrograph cannot be inverted") # start with this opening the port: if it fails, we are done try: self._serial = self.openSerialPort(port) except serial.SerialException: raise HwError( "Failed to find spectrograph %s (on port '%s'). " "Check the device is turned on and connected to the " "computer. You might need to turn it off and on again." % (name, port)) self._port = port # to acquire before sending anything on the serial port self._ser_access = threading.Lock() self._try_recover = False if _noinit: return self._initDevice() self._try_recover = True try: self._ccd = dependencies["ccd"] except (TypeError, KeyError): # TODO: only needed if there is calibration info (for the pixel size) # otherwise it's fine without CCD. raise ValueError("Spectrograph needs a dependency 'ccd'") # according to the model determine how many gratings per turret model_name = self.GetModel() self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3) if turret is not None: if turret < 1 or turret > self.max_gratings: raise ValueError( "Turret number given is %s, while expected a value between 1 and %d" % (turret, self.max_gratings)) self.SetTurret(turret) self._turret = turret else: self._turret = self.GetTurret() # for now, it's fixed (and it's unlikely to be useful to allow less than the max) max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s", readonly=True) gchoices = self.GetGratingChoices() # remove the choices which are not valid for the current turret for c in gchoices: t = 1 + (c - 1) // self.max_gratings if t != self._turret: del gchoices[c] # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?) # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?) # cf http://www.roperscientific.de/gratingcalcmaster.html # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?) # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !? axes = { "wavelength": model.Axis(unit="m", range=(0, 2400e-9), speed=(max_speed, max_speed)), "grating": model.Axis(choices=gchoices) } # provides a ._axes model.Actuator.__init__(self, name, role, axes=axes, dependencies=dependencies, **kwargs) # First step of parsing calib parmeter: convert to (int, int) -> ... calib = calib or () if not isinstance(calib, collections.Iterable): raise ValueError("calib parameter must be in the format " "[blz, gl, ca, sa, fl, ia, da], " "but got %s" % (calib, )) dcalib = {} for c in calib: if not isinstance(c, collections.Iterable) or len(c) != 7: raise ValueError("calib parameter must be in the format " "[blz, gl, ca, sa, fl, ia, da], " "but got %s" % (c, )) gt = (c[0], c[1]) if gt in dcalib: raise ValueError( "calib parameter contains twice calibration for " "grating (%d nm, %d gl/mm)" % gt) dcalib[gt] = c[2:] # store calibration for pixel -> wavelength conversion and wavelength offset # int (grating number 1 -> 9) -> center adjust, slope adjust, # focal length, inclusion angle/2, detector angle self._calib = {} # TODO: read the info from MONO-EESTATUS (but it's so # huge that it's not fun to parse). There is also detector angle. dfl = FOCAL_LENGTH_OFFICIAL[model_name] # m dia = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name]) # rad for i in gchoices: # put default values self._calib[i] = (0, 0, dfl, dia, 0) try: blz = self._getBlaze(i) # m gl = self._getGrooveDensity(i) # gl/m except ValueError: logging.warning("Failed to parse info of grating %d" % i, exc_info=True) continue # parse calib info gt = (int(blz * 1e9), int(gl * 1e-3)) if gt in dcalib: calgt = dcalib[gt] ca = self._readCalibVal(calgt[0]) # ratio sa = self._readCalibVal(calgt[1]) # ratio fl = self._readCalibVal(calgt[2]) * 1e-3 # mm -> m ia = math.radians(self._readCalibVal(calgt[3])) # ° -> rad da = math.radians(self._readCalibVal(calgt[4])) # ° -> rad self._calib[i] = ca, sa, fl, ia, da logging.info( "Calibration data for grating %d (%d nm, %d gl/mm) " "-> %s" % (i, gt[0], gt[1], self._calib[i])) else: logging.warning("No calibration data for grating %d " "(%d nm, %d gl/mm)" % (i, gt[0], gt[1])) # set HW and SW version self._swVersion = "%s (serial driver: %s)" % ( odemis.__version__, driver.getSerialDriver(port)) self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown")) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time # for storing the latest calibrated wavelength value self._wl = (None, None, None ) # grating id, raw center wl, calibrated center wl # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute({}, unit="m", readonly=True) self._updatePosition()
def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs): """ address (str): ip address (use "autoip" to automatically scan and find the controller, "fake" for a simulator) axes (list of str): names of the axes, from the 1st to the 4th, if present. if an axis is not connected, put a "". stepsize (list of float): size of a step in m (the smaller, the bigger will be a move for a given distance in m) sn (str or None): serial number of the device (eg, "11500"). If None, the driver will use whichever controller is first found. inverted (set of str): names of the axes which are inverted (IOW, either empty or the name of the axis) """ if not 1 <= len(axes) <= 4: raise ValueError( "Axes must be a list of 1 to 4 axis names (got %s)" % (axes, )) if len(axes) != len(stepsize): raise ValueError("Expecting %d stepsize (got %s)" % (len(axes), stepsize)) self._name_to_axis = {} # str -> int: name -> axis number for i, n in enumerate(axes): if n == "": # skip this non-connected axis continue self._name_to_axis[n] = i + 1 for sz in stepsize: if sz > 10e-3: # sz is typically ~1µm, so > 1 cm is very fishy raise ValueError("stepsize should be in meter, but got %g" % (sz, )) self._stepsize = stepsize self._address = address self._sn = sn self._accesser = self._openConnection(address, sn) self._recover = False self._resynchonise() if name is None and role is None: # For scan only return # Seems to really be the device, so handle connection errors fully self._recover = True modl, fw, sn = self.GetIdentification() if modl != "8742": logging.warning("Controller %s is not supported, will try anyway", modl) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time # Let the controller check the actuators are connected self.MotorCheck() axes_def = {} speed = {} for n, i in self._name_to_axis.items(): sz = self._stepsize[i - 1] # TODO: allow to pass the range in m in the arguments # Position supports ±2³¹, probably not that much in reality, but # there is no info. rng = [(-2**31) * sz, (2**31 - 1) * sz] # Check the actuator is connected mt = self.GetMotorType(i) if mt in {MT_NONE, MT_UNKNOWN}: raise HwError( "Controller failed to detect motor %d, check the " "actuator is connected to the controller" % (i, )) max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt] srng = (0, self._speedToMS(i, max_stp_s)) speed[n] = self._speedToMS(i, self.GetVelocity(i)) axes_def[n] = model.Axis(range=rng, speed=srng, unit="m") model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs) self._swVersion = "%s (IP connection)" % (odemis.__version__, ) self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn) # Note that the "0" position is just the position at which the # controller turned on self.position = model.VigilantAttribute({}, unit="m", readonly=True) self._updatePosition() max_speed = max(a.speed[1] for a in axes_def.values()) self.speed = model.MultiSpeedVA(speed, range=(0, max_speed), unit="m/s", setter=self._setSpeed)
def __init__(self, name, role, port, pin_map=None, delay=None, init=None, ids=None, termination=None, **kwargs): ''' port (str): port name pin_map (dict of str -> int): names of the components and the pin where the component is connected. delay (dict str -> float): time to wait for each component after it is turned on. init (dict str -> boolean): turn on/off the corresponding component upon initialization. ids (list str): EEPROM ids expected to be detected during initialization. termination (dict str -> bool/None): indicate for every component if it should be turned off on termination (False), turned on (True) or left as-is (None). Raise an exception if the device cannot be opened ''' if pin_map: self.powered = list(pin_map.keys()) else: self.powered = [] model.PowerSupplier.__init__(self, name, role, **kwargs) # TODO: catch errors and convert to HwError self._ser_access = threading.Lock() self._file = None self._port = self._findDevice(port) # sets ._serial and ._file logging.info("Found Power Control device on port %s", self._port) # Get identification of the Power control device self._idn = self._getIdentification() driver_name = driver.getSerialDriver(self._port) self._swVersion = "serial driver: %s" % (driver_name, ) self._hwVersion = "%s" % (self._idn, ) pin_map = pin_map or {} self._pin_map = pin_map delay = delay or {} # fill the missing pairs with 0 values self._delay = dict.fromkeys(pin_map, 0) self._delay.update(delay) self._last_start = dict.fromkeys(self._delay, time.time()) # only keep components that should be changed on termination termination = termination or {} self._termination = { k: v for k, v in termination.items() if v is not None } for comp in self._termination: if comp not in pin_map: raise ValueError( "Component %s in termination not found in pin_map." % comp) # will take care of executing switch asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time self._supplied = {} self.supplied = model.VigilantAttribute(self._supplied, readonly=True) self._updateSupplied() init = init or {} # Remove all None's from the dict, so it can be passed as-is to _doSupply() init = {k: v for k, v in init.items() if v is not None} for comp in init: if comp not in pin_map: raise ValueError("Component %s in init not found in pin_map." % comp) try: self._doSupply(init, apply_delay=False) except IOError as ex: # This is in particular to handle some cases where the device resets # when turning on the power. One or more trials and the logging.exception("Failure during turning on initial power.") raise HwError( "Device error when initialising power: %s. " "Try again or contact support if the problem persists." % (ex, )) self.memoryIDs = model.VigilantAttribute(None, readonly=True, getter=self._getIdentities) if ids: mem_ids = self.memoryIDs.value for eid in ids: if eid not in mem_ids: raise HwError("EEPROM id %s was not detected. Make sure " "all EEPROM components are connected." % (eid, ))
def __init__(self, errcode): if errcode in ERRCODE: text = "Error %d: %s" % (errcode, ERRCODE[errcode]) else: text = "Unknown error code." HwError.__init__(self, text)
def __init__(self, name, role, dependencies, settle_time=0, **kwargs): ''' dependencies (dict string->model.HwComponent): the dependencies There must a dependency "detector" and at least one of the dependencies "pmt-control" and "pmt-signal". "pmt-control" takes a PMTControl or spectrograph object, "pmt-signal" an extra detector to be activated during the acquisition (eg, for shutter control) settle_time (0 < float): time to wait after turning on the gain to have it fully working. Raise an ValueError exception if the dependencies are not compatible ''' # we will fill the set of dependencies with Components later in ._dependencies model.Detector.__init__(self, name, role, dependencies=dependencies, **kwargs) if settle_time < 0: raise ValueError("Settle time of %g s for '%s' is negative" % (settle_time, name)) elif settle_time > 10: # a large value is a sign that the user mistook in units raise ValueError("Settle time of %g s for '%s' is too long" % (settle_time, name)) self._settle_time = settle_time # Check the dependencies pmt = dependencies["detector"] if not isinstance(pmt, ComponentBase): raise ValueError("Dependency detector is not a component.") if not hasattr(pmt, "data") or not isinstance(pmt.data, DataFlowBase): raise ValueError( "Dependency detector is not a Detector component.") self._pmt = pmt self._shape = pmt.shape # copy all the VAs and Events from the PMT to here (but .state and .dependencies). pmtVAs = model.getVAs(pmt) for key, value in pmtVAs.items(): if not hasattr(self, key): setattr(self, key, value) pmtEvents = model.getEvents(pmt) for key, value in pmtEvents.items(): setattr(self, key, value) if "pmt-control" in dependencies: ctrl = dependencies["pmt-control"] self._control = ctrl if not isinstance(ctrl, ComponentBase): raise ValueError("Dependency pmt-control is not a component.") # Duplicate control unit VAs # In case of counting PMT these VAs are not available since a # spectrograph is given instead of the control unit. try: if model.hasVA(ctrl, "gain"): gain = ctrl.gain.range[0] self.gain = model.FloatContinuous(gain, ctrl.gain.range, unit="V", setter=self._setGain) self._last_gain = gain self._setGain(gain) # Just start with no gain if model.hasVA(ctrl, "powerSupply"): self.powerSupply = ctrl.powerSupply # Turn on the controller self.powerSupply.value = True except IOError: # FIXME: needs to be handled directly by PMTControl (at least automatic reconnect) raise HwError( "PMT Control Unit connection timeout. " "Please turn off and on the power to the box and " "then restart Odemis.") # Protection VA should be available anyway if not model.hasVA(ctrl, "protection"): raise ValueError( "Given component appears to be neither a PMT control " "unit or a spectrograph since protection VA is not " "available.") else: self._control = None if "pmt-signal" in dependencies: self._signal = dependencies["pmt-signal"] if not isinstance(self._signal, ComponentBase): raise ValueError("Dependency pmt-signal is not a component.") if not hasattr(self._signal, "data") or not isinstance( self._signal.data, model.DataFlowBase): raise ValueError( "Dependency pmt-signal doesn't have an attribute .data of type DataFlow." ) else: self._signal = None self.data = PMTDataFlow(self, self._pmt, self._control, self._signal)
def __init__(self, errcode): if errcode in ERRCODE: text = "Error %d: %s" % (errcode, ERRCODE[errcode]) else: text = "Unknown error code." HwError.__init__(self, text)
def __init__(self, name, role, children=None, sn=None, port=None, axis="rz", inverted=None, positions=None, **kwargs): """ children (dict string->model.HwComponent): they are not actually used. This is currently in place just to enforce PMT control to be initialized before the Fiber Flipper since we need the relay reset to happen before the flipper is turned on. sn (str): serial number (recommended) port (str): port name (only if sn is not specified) axis (str): name of the axis inverted (set of str): names of the axes which are inverted (IOW, either empty or the name of the axis) positions (None, or list of 2 tuples (value, str)): positions values and their corresponding name. If None: 0 and Pi/2 are used, without names. """ if (sn is None and port is None) or (sn is not None and port is not None): raise ValueError("sn or port argument must be specified (but not both)") if sn is not None: if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8: logging.warning("Serial number '%s' is unexpected for a MFF " "device (should be 8 digits starting with %s).", sn, SN_PREFIX_MFF) self._port = self._getSerialPort(sn) self._sn = sn else: self._port = port # The MFF returns no serial number from GetInfo(), so find via USB try: self._sn = self._getSerialNumber(port) logging.info("Found serial number %s for device %s", self._sn, name) except LookupError: self._sn = None self._serial = self._openSerialPort(self._port) self._ser_access = threading.RLock() # reentrant, so that recovery can keep sending messages self._recover = False self._initHw() # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time if positions is None: positions = ((0, None), (math.radians(90), None)) else: if len(positions) != 2 or any(len(p) != 2 for p in positions): raise ValueError("Positions must be exactly 2 tuples of 2 values") # TODO: have the standard inverted Actuator functions work on enumerated axis if inverted and axis in inverted: positions = (positions[1], positions[0]) self._pos_to_jog = {positions[0][0]: 1, positions[1][0]: 2} self._status_to_pos = {STA_FWD_HLS: positions[0][0], STA_RVS_HLS: positions[1][0]} if positions[0][1] is None: choices = set(p[0] for p in positions) else: choices = dict(positions) # TODO: add support for speed axes = {axis: model.Axis(unit="rad", choices=choices)} model.Actuator.__init__(self, name, role, axes=axes, **kwargs) driver_name = driver.getSerialDriver(self._port) self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name) try: snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo() except IOError: # This is the first communication with the hardware, if it fails # it can be a sign the device is in a bad state. (it is known to # fail when turned on and plugged in before the host computer is # turned on) logging.exception("GetInfo() failed.") raise HwError("USB device with S/N %s seems in bad state. " "Check that the Thorlabs filter flipper was " "turned on *after* the host computer." % sn) self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv) # It has worked at least once, so if it fails, there are hopes self._recover = True self.position = model.VigilantAttribute({}, readonly=True) self._updatePosition()
def __init__(self, name, role, sn=None, port=None, axis="rz", inverted=None, **kwargs): """ sn (str): serial number (recommended) port (str): port name (only if sn is not specified) axis (str): name of the axis inverted (set of str): names of the axes which are inverted (IOW, either empty or the name of the axis) """ if (sn is None and port is None) or (sn is not None and port is not None): raise ValueError( "sn or port argument must be specified (but not both)") if sn is not None: if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8: logging.warning( "Serial number '%s' is unexpected for a MFF " "device (should be 8 digits starting with %s).", sn, SN_PREFIX_MFF) self._port = self._getSerialPort(sn) else: self._port = port self._serial = self._openSerialPort(self._port) self._ser_access = threading.Lock() # Ensure we don't receive anything self.SendMessage(HW_STOP_UPDATEMSGS) self._serial.flushInput() # Documentation says it should be done first, though it doesn't seem # required self.SendMessage(HW_NO_FLASH_PROGRAMMING) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time # TODO: have the standard inverted Actuator functions work on enumerated # use a different format than the standard Actuator if inverted and axis in inverted: self._pos_to_jog = {POS_UP: 2, POS_DOWN: 1} self._status_to_pos = {STA_RVS_HLS: POS_UP, STA_FWD_HLS: POS_DOWN} else: self._pos_to_jog = {POS_UP: 1, POS_DOWN: 2} self._status_to_pos = {STA_FWD_HLS: POS_UP, STA_RVS_HLS: POS_DOWN} # TODO: add support for speed axes = { axis: model.Axis(unit="rad", choices=set(self._pos_to_jog.keys())) } model.Actuator.__init__(self, name, role, axes=axes, **kwargs) driver_name = driver.getSerialDriver(self._port) self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name) try: snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo() except IOError: # This is the first communication with the hardware, if it fails # it can be a sign the device is in a bad state. (it is known to # fail when turned on and plugged in before the host computer is # turned on) raise HwError("No USB device with S/N %s. " "Check that the Thorlabs filter flipper was " "turned on *after* the host computer." % sn) self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv) self.position = model.VigilantAttribute({}, readonly=True) self._updatePosition()