Exemple #1
0
    def __init__(self, name, role, port, axes, ustepsize, **kwargs):
        """
        port (str): port name (only if sn is not specified)
        axes (list of str): names of the axes, from the 1st to the 3rd.
        ustepsize (list of float): size of a microstep in m  
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if len(axes) != 3:
            raise ValueError("Axes must be a list of 3 axis names (got %s)" %
                             (axes, ))
        self._axes_names = axes  # axes names in order

        if len(axes) != len(ustepsize):
            raise ValueError("Expecting %d ustepsize (got %s)" %
                             (len(axes), ustepsize))
        for sz in ustepsize:
            if sz > 10e-3:  # sz is typically ~1µm, so > 1 cm is very fishy
                raise ValueError("ustepsize should be in meter, but got %g" %
                                 (sz, ))
        self._ustepsize = ustepsize

        self._serial = self._openSerialPort(port)
        self._port = port
        self._ser_access = threading.Lock()
        self._target = 1  # TODO: need to be selected by user? When is it not 1?

        self._resynchonise()

        modl, vmaj, vmin = self.GetVersion()
        if modl != 3110:
            logging.warning(
                "Controller TMCM-%d is not supported, will try anyway", modl)
        if name is None and role is None:  # For scan only
            return

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        axes_def = {}
        for n, sz in zip(self._axes_names, self._ustepsize):
            # Mov abs supports ±2³¹ but the actual position is only within ±2²³
            rng = [(-2**23) * sz, (2**23 - 1) * sz]
            # Probably not that much, but there is no info unless the axis has
            # limit switches and we run a referencing
            axes_def[n] = model.Axis(range=rng, unit="m")
        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver_name)
        self._hwVersion = "TMCM-%d (firmware %d.%02d)" % (modl, vmaj, vmin)

        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        # TODO: add support for changing speed. cf p.68: axis param 4 + p.81 + TMC 429 p.6
        self.speed = model.VigilantAttribute({}, unit="m/s", readonly=True)
        self._updateSpeed()
Exemple #2
0
    def __init__(self, name, role, port, bands, _scan=False, **kwargs):
        """
        port (string): name of the serial port to connect to. Can be a pattern,
         in which case, all the ports fitting the pattern will be tried, and the
         first one which looks like an FW102C will be used.
        bands (dict 1<=int<=12 -> 2-tuple of floats > 0 or str):
          filter position -> lower and higher bound of the wavelength (m) of the
          light which goes _through_. If it's a list, it implies that the filter
          is multi-band.
        _scan (bool): only for internal usage
        raise IOError if no device answering or not a compatible device
        """
        self._ser_access = threading.Lock()
        self._port = self._findDevice(port)
        logging.info("Found FW102C device on port %s", self._port)
        if _scan:
            return

        # check bands contains correct data
        self._maxpos = self.GetMaxPosition()
        if not bands:
            raise ValueError("Argument bands must contain at least one band")
        try:
            for pos, band in bands.items():
                if not 1 <= pos <= self._maxpos:
                    raise ValueError("Filter position should be between 1 and "
                                     "%d, but got %d." % (self._maxpos, pos))
                # To support "weird" filter, we accept strings
                if isinstance(band, basestring):
                    if not band.strip():
                        raise ValueError("Name of filter %d is empty" % pos)
                else:
                    self._checkBand(band)
        except Exception:
            logging.exception("Failed to parse bands %s", bands)
            raise

        axes = {"band": model.Axis(choices=bands)}
        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)
        self._hwVersion = self._idn

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        self._speed = self.GetSpeed()

        curpos = self.GetPosition()
        self.position = model.VigilantAttribute({"band": curpos},
                                                readonly=True)

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {
            model.MD_FILTER_NAME: name,
            model.MD_OUT_WL: self._axes["band"].choices[curpos]
        }
Exemple #3
0
    def __init__(self, name, role, port, baudrate=9600, idn=None, **kwargs):
        '''
        port (str): port name. Can be a pattern, in which case it will pick the
          first one which responds well
        baudrate (int): the baudrate setting for the RS232 connection
        idn (str or None): If present, a regex to match the *IDN command. For
         instance "KEITHLEY.+MODEL 6485.+12345678".
        '''
        model.Detector.__init__(self, name, role, **kwargs)

        self._ser_access = threading.Lock()
        self._serial = None
        self._file = None
        self._port, self._idn = self._findDevice(
            port, baudrate, idn)  # sets ._serial and ._file
        logging.info("Found SPCI device on port %s", self._port)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "serial driver: %s" % (driver_name, )
        self._hwVersion = self._idn

        # Just for logging, check if there are any errors reported
        while True:
            n, msg = self.ReadNextError()
            if n is not None:
                logging.warning("Discarding previous error %s (%d)", msg, n)
            else:
                break

        stat = self.ReadStatusByte()
        if stat & (1 << 2):  # Bit 2 = error available
            # It seems that some status is not bad anyway
            logging.warning("Status byte is %d", stat)

        self.ClearStatus()

        self._lfr = self.GetLineFrequency()

        # Force range to auto
        self._sendOrder(":CURR:RANG:AUTO ON")
        self._checkError()
        # Prepare to measure current
        self.ConfigureCurrent()
        self._checkError()

        # TODO: that's probably very Keithley 6485
        rate = self.GetIntegrationRate()
        self._checkError()
        # Note: the lowest noise is at rate between 1 and 10, so ~20ms to 200ms
        # The max rate is the line frequency (=> 1 s)
        self.dwellTime = model.FloatContinuous(rate / self._lfr,
                                               (0.01 / self._lfr, 1),
                                               unit="s",
                                               setter=self._setDwellTime)

        self._shape = (float("inf"), )  # only one point, with float values
        self._generator = None
        self.data = BasicDataFlow(self)
        self._metadata[model.MD_DET_TYPE] = model.MD_DT_NORMAL
Exemple #4
0
    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)
        snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
Exemple #5
0
 def __init__(self, port):
     """
     port (string): serial port to use
     """
     self.port = port
     self._serial = self._openSerialPort(port)
     self.flushInput() # can have some \x00 bytes at the beginning
     self.driver = driver.getSerialDriver(port)
Exemple #6
0
    def __init__(self, name, role, port, axes, **kwargs):
        """
        port (string): name of the serial port to connect to the controllers
        axes (dict string=> 2-tuple (0<=int<=15, 0<=int<=1)): the configuration of the network.
         for each axis name the controller address and channel
         Note that even if it's made of several controllers, each controller is 
         _not_ seen as a child from the odemis model point of view.
        """

        ser = PIRedStone.openSerialPort(port)
#        ser = FakePIRedStone.openSerialPort(port) # use FakePIRedStone for testing

        # Not to be mistaken with axes which is a simple public view
        self._axis_to_child = {} # axis name -> (PIRedStone, channel)
        axes_def = {} # axis name -> Axis
        # TODO also a rangesRel : min and max of a step
        position = {}
        speed = {}
        controllers = {} # address => PIRedStone
        for axis, (add, channel) in axes.items():
            if not add in controllers:
                controllers[add] = PIRedStone(ser, add)
#                controllers[add] = FakePIRedStone(ser, add) # use FakePIRedStone for testing
            controller = controllers[add]
            self._axis_to_child[axis] = (controller, channel)

            position[axis] = controller.getPosition(channel)
            # TODO request also the ranges from the arguments?
            # For now we put very large one
            ad = model.Axis(canAbs=False, unit="m", range=(-1, 1),
                            speed=(10e-6, 0.5))
            # Just to make sure it doesn't go too fast
            speed[axis] = 0.1 # m/s
            axes_def[axis] = ad

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)
        
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(position, unit="m", readonly=True)

        # min speed = don't be crazy slow. max speed from hardware spec
        self.speed = model.MultiSpeedVA(speed, range=[10e-6, 0.5], unit="m/s",
                                        setter=self._setSpeed)
        self._setSpeed(speed)

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver.getSerialDriver(port))
        hwversions = []
        for axis, (ctrl, channel) in self._axis_to_child.items():
            hwversions.append("'%s': %s" % (axis, ctrl.versionReport()))
        self._hwVersion = ", ".join(hwversions)

        # to acquire before sending anything on the serial port
        self.ser_access = threading.Lock()

        self._action_mgr = ActionManager(self)
        self._action_mgr.start()
Exemple #7
0
 def __init__(self, port):
     """
     port (string): serial port to use
     raise IOError: if port cannot be used
     """
     self.port = port
     self._serial = self._openSerialPort(port)
     self.flushInput()  # can have some \x00 bytes at the beginning
     self.driver = driver.getSerialDriver(port)
Exemple #8
0
    def __init__(self, name, role, port, bands, _scan=False, **kwargs):
        """
        port (string): name of the serial port to connect to. Can be a pattern,
         in which case, all the ports fitting the pattern will be tried, and the
         first one which looks like an FW102C will be used.
        bands (dict 1<=int<=12 -> 2-tuple of floats > 0 or str):
          filter position -> lower and higher bound of the wavelength (m) of the
          light which goes _through_. If it's a list, it implies that the filter
          is multi-band.
        _scan (bool): only for internal usage
        raise IOError if no device answering or not a compatible device
        """
        self._ser_access = threading.Lock()
        self._port = self._findDevice(port)
        logging.info("Found FW102C device on port %s", self._port)
        if _scan:
            return

        # check bands contains correct data
        self._maxpos = self.GetMaxPosition()
        if not bands:
            raise ValueError("Argument bands must contain at least one band")
        try:
            for pos, band in bands.items():
                if not 1 <= pos <= self._maxpos:
                    raise ValueError("Filter position should be between 1 and "
                                     "%d, but got %d." % (self._maxpos, pos))
                # To support "weird" filter, we accept strings
                if isinstance(band, basestring):
                    if not band.strip():
                        raise ValueError("Name of filter %d is empty" % pos)
                else:
                    driver.checkLightBand(band)
        except Exception:
            logging.exception("Failed to parse bands %s", bands)
            raise

        curpos = self.GetPosition()
        if curpos not in bands:
            logging.info("Current position %d is not configured, will add it", curpos)
            bands[curpos] = "unknown"

        axes = {"band": model.Axis(choices=bands)}
        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)
        self._hwVersion = self._idn

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time

        self._speed = self.GetSpeed()

        self.position = model.VigilantAttribute({"band": curpos}, readonly=True)
Exemple #9
0
    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()
Exemple #10
0
    def __init__(self, name, role, port, light_name, max_power, **kwargs):
        '''
        port (str): port name. Can be a pattern, in which case it will pick the
          first one which responds well
        ligth_name (str): the name of the component that is controlled by this
          power supplier
        max_power (float): maximum power, in W. Will be set at initialisation.
        '''
        # TODO: allow to pass the serial number, to select the right device
        model.PowerSupplier.__init__(self, name, role, **kwargs)

        self._light_name = light_name
        self._ser_access = threading.Lock()
        self._port = self._findDevice(port)  # sets ._serial
        logging.info("Found Cobolt DPSS device on port %s", self._port)

        self._sn = self.GetSerialNumber()

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "serial driver: %s" % (driver_name, )
        self._hwVersion = "Cobolt DPSS (s/n: %s)" % (self._sn, )

        # Reset sequence
        # TODO: do a proper one. For now it's just everything we can throw, so
        # that it's a bit easier to debug
        self._sendCommand("ilk?")
        self._sendCommand("leds?")
        self._sendCommand("@cobasky?")
        self._sendCommand("cf")  # Clear fault
        # self._sendCommand("@cob1") # used to force the laser on after interlock opened error

        # will take care of executing switch asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        # Dict str -> bool: component name -> turn on/off
        self.supplied = model.VigilantAttribute({light_name: False},
                                                readonly=True)
        self._updateSupplied()

        self.SetOutputPower(max_power)
Exemple #11
0
    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 ValueError("No Omicron xX device found for ports '%s'" % 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()
Exemple #12
0
    def __init__(self, name, role, port, light_name, max_power, **kwargs):
        '''
        port (str): port name. Can be a pattern, in which case it will pick the
          first one which responds well
        ligth_name (str): the name of the component that is controlled by this
          power supplier
        max_power (float): maximum power, in W. Will be set at initialisation.
        '''
        # TODO: allow to pass the serial number, to select the right device
        model.PowerSupplier.__init__(self, name, role, **kwargs)

        self._light_name = light_name
        self._ser_access = threading.Lock()
        self._port = self._findDevice(port)  # sets ._serial
        logging.info("Found Cobolt DPSS device on port %s", self._port)

        self._sn = self.GetSerialNumber()

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "serial driver: %s" % (driver_name,)
        self._hwVersion = "Cobolt DPSS (s/n: %s)" % (self._sn,)

        # Reset sequence
        # TODO: do a proper one. For now it's just everything we can throw, so
        # that it's a bit easier to debug
        self._sendCommand("ilk?")
        self._sendCommand("leds?")
        self._sendCommand("@cobasky?")
        self._sendCommand("cf")  # Clear fault
        # self._sendCommand("@cob1") # used to force the laser on after interlock opened error

        # will take care of executing switch asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time

        # Dict str -> bool: component name -> turn on/off
        self.supplied = model.VigilantAttribute({light_name: False}, readonly=True)
        self._updateSupplied()

        self.SetOutputPower(max_power)
Exemple #13
0
    def __init__(self,
                 name,
                 role,
                 port,
                 prot_time=1e-3,
                 prot_curr=30e-6,
                 relay_cycle=None,
                 powered=None,
                 **kwargs):
        '''
        port (str): port name
        prot_time (float): protection trip time (in s)
        prot_curr (float): protection current threshold (in Amperes)
        relay_cycle (None or 0<float): if not None, will power cycle the relay
          with the given delay (in s)
        powered (list of str or None): set of the HwComponents controlled by the relay
        Raise an exception if the device cannot be opened
        '''
        if powered is None:
            powered = []
        self.powered = powered

        model.PowerSupplier.__init__(self, name, role, **kwargs)

        # get protection time (s) and current (A) properties
        if not 0 <= prot_time < 1e3:
            raise ValueError("prot_time should be a time (in s) but got %s" %
                             (prot_time, ))
        self._prot_time = prot_time
        if not 0 <= prot_curr <= 100e-6:
            raise ValueError("prot_curr (%s A) is not between 0 and 100.e-6" %
                             (prot_curr, ))
        self._prot_curr = prot_curr

        # TODO: catch errors and convert to HwError
        self._ser_access = threading.Lock()

        self._port = self._findDevice(port)  # sets ._serial
        logging.info("Found PMT Control device on port %s", self._port)

        # Get identification of the PMT control device
        self._idn = self._getIdentification()

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "serial driver: %s" % (driver_name, )
        self._hwVersion = "%s" % (self._idn, )

        # Set protection current and time
        self._setProtectionCurrent(self._prot_curr)
        self._setProtectionTime(self._prot_time)

        # gain, powerSupply and protection VAs
        self.protection = model.BooleanVA(True,
                                          setter=self._setProtection,
                                          getter=self._getProtection)
        self._setProtection(True)

        gain_rng = (MIN_VOLT, MAX_VOLT)
        gain = self._getGain()
        self.gain = model.FloatContinuous(gain,
                                          gain_rng,
                                          unit="V",
                                          setter=self._setGain)

        self.powerSupply = model.BooleanVA(True, setter=self._setPowerSupply)
        self._setPowerSupply(True)

        # will take care of executing supply asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

        # relay initialization
        if relay_cycle is not None:
            logging.info("Power cycling the relay for %f s", relay_cycle)
            self.setRelay(False)
            time.sleep(relay_cycle)

        # Reset if no powered provided
        if not powered:
            self.setRelay(True)
        else:
            self._supplied = {}
            self.supplied = model.VigilantAttribute(self._supplied,
                                                    readonly=True)
            self._updateSupplied()
Exemple #14
0
    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()
Exemple #15
0
    def __init__(self,
                 name,
                 role,
                 port,
                 axes,
                 inverted=None,
                 param_file=None,
                 **kwargs):
        """
        :param axes (dict: str -> dict): axis name --> axis parameters
            Each axis is specified by a set of parameters.
            After successful configuration with the pmconfig.py script, the only required parameter for a default motor
            is the address which was set during the configuration process.
            The spc parameter (conversion between motor steps and encoder counts) is typically saved in the flash
            memory of the controller during the configuration process. The flash value is overridden by the
            value in the parameter dict.
            Depending on the type of motor, the encoder_resolution and range might need to be adjusted.

            Axis parameters:
                axis_number (0 <= int <= 127): typically 1-3 for x-z, required
                closed_loop (bool): True for closed loop (with encoder), default to False
                encoder_resolution (float): number of encoder counts per meter, default to 1.22e-9
                motorstep_resolution (float): number of motor steps per m, default to 5e-6
                range (tuple of float): in m, default to STROKE_RANGE
                speed (float): speed in m/s
                unit (str), default to m
        :param param_file (str or None): (absolute or relative) path to a tmcm.tsv file which will be used to initialise
            the axis parameters.
        """
        self._axis_map = {}  # axis name -> axis number used by controller
        self._closed_loop = {}  # axis name (str) -> bool (True if closed loop)
        self._speed = {}  # axis name (str) -> speed in unit/s
        self._speed_steps = {
        }  # axis name (str) -> int, speed in steps per meter
        self._counts_per_meter = {}  # axis name (str) -> float
        self._steps_per_meter = {}  # axis name (str) -> float
        self._portpattern = port

        # Parse axis parameters and create axis
        axes_def = {}  # axis name -> Axis object
        for axis_name, axis_par in axes.items():
            if 'axis_number' in axis_par:
                axis_num = axis_par['axis_number']
                if axis_num not in range(128):
                    raise ValueError(
                        "Invalid axis number %s, needs to be 0 <= int <= 127."
                        % axis_num)
                elif axis_num in self._axis_map.values():
                    axname = self._axis_map[axis_num]
                    raise ValueError(
                        "Invalid axis number %s, already assigned to axis %s."
                        % (axis_num, axname))
                else:
                    self._axis_map[axis_name] = axis_par['axis_number']
            else:
                raise ValueError("Axis %s has no axis number." % axis_name)

            if 'closed_loop' in axis_par:
                closed_loop = axis_par['closed_loop']
            else:
                closed_loop = False
                logging.info(
                    "Axis parameter \"closed_loop\" not specified for axis %s. Assuming open-loop.",
                    axis_name)
            self._closed_loop[axis_name] = closed_loop

            if 'motorstep_resolution' in axis_par:
                self._steps_per_meter[
                    axis_name] = 1 / axis_par['motorstep_resolution']
            else:
                self._steps_per_meter[
                    axis_name] = 1 / DEFAULT_MOTORSTEP_RESOLUTION
                logging.info(
                    "Axis %s has no motorstep resolution, assuming %s.",
                    axis_name, DEFAULT_MOTORSTEP_RESOLUTION)

            if 'encoder_resolution' in axis_par:
                self._counts_per_meter[
                    axis_name] = 1 / axis_par['encoder_resolution']
            else:
                self._counts_per_meter[
                    axis_name] = 1 / DEFAULT_ENCODER_RESOLUTION
                logging.info("Axis %s has no encoder resolution, assuming %s.",
                             axis_name, DEFAULT_ENCODER_RESOLUTION)

            if 'range' in axis_par:
                axis_range = [
                    float(axis_par['range'][0]),
                    float(axis_par['range'][1])
                ]
            else:
                axis_range = STROKE_RANGE
                logging.info("Axis %s has no range. Assuming %s", axis_name,
                             axis_range)

            if 'speed' in axis_par:
                self._speed[axis_name] = axis_par['speed']
            else:
                self._speed[axis_name] = DEFAULT_AXIS_SPEED
                logging.info(
                    "Axis %s was not given a speed value. Assuming %s",
                    axis_name, self._speed[axis_name])
            self._speed_steps[axis_name] = int(
                round(self._speed[axis_name] *
                      self._steps_per_meter[axis_name]))

            if 'unit' in axis_par:
                axis_unit = axis_par['unit']
            else:
                axis_unit = "m"
                logging.info("Axis %s has no unit. Assuming %s", axis_name,
                             axis_unit)

            ad = model.Axis(canAbs=closed_loop,
                            unit=axis_unit,
                            range=axis_range)
            axes_def[axis_name] = ad

        Actuator.__init__(self,
                          name,
                          role,
                          axes=axes_def,
                          inverted=inverted,
                          **kwargs)
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
        self._ser_access = threading.RLock()

        # Connect to hardware
        self._port = None  # port number
        min_axis = min(self._axis_map.values())
        self._serial = self._findDevice(port, min_axis)
        self._recovering = False

        # Get version
        hwVersions = []
        for ax_name, ax_num in self._axis_map.items():
            ver = self.getVersion(ax_num)
            sn = self.getSerialNumber(ax_num)
            hwVersions.append("Axis %s ('%s') version: %s, " %
                              (ax_num, ax_name, ver) +
                              "serial number: %s" % sn)
        self._hwVersion = ", ".join(hwVersions)
        logging.debug("Hardware versions: %s", hwVersions)

        # Configuration
        for axis in self._axis_map.values():
            self.setWaveform(axis, WAVEFORM_DELTA)

        driver_name = getSerialDriver(self._port)
        self._swVersion = "Serial driver: %s" % (driver_name, )

        # Position and referenced VAs
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self.referenced = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
        for axname in self._axis_map.keys():
            self.referenced.value[
                axname] = False  # just assume they haven't been referenced

        self.speed = model.VigilantAttribute(self._speed,
                                             unit="m/s",
                                             readonly=True)

        # Write parameters from parameter file
        if param_file:
            if not os.path.isabs(param_file):
                param_file = os.path.join(os.path.dirname(__file__),
                                          param_file)
            try:
                f = open(param_file)
            except Exception as ex:
                raise ValueError("Failed to open file %s: %s" %
                                 (param_file, ex))
            try:
                axis_params = self.parse_tsv_config(f)
            except Exception as ex:
                raise ValueError("Failed to parse file %s: %s" %
                                 (param_file, ex))
            f.close()
            logging.debug("Extracted param file config: %s", axis_params)
            self.apply_params(axis_params)
Exemple #16
0
    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()
Exemple #17
0
    def __init__(self, name, role, port, sources, _serial=None, **kwargs):
        """
        port (string): name of the serial port to connect to. Can be a pattern,
         in which case, all the ports fitting the pattern will be tried, and the
         first one which looks like an LLE will be used.
        sources (dict string -> 5-tuple of float): the light sources (by colour).
         The string is one of the seven names for the sources: "red", "cyan",
         "green", "UV", "yellow", "blue", "teal". They correspond to fix
         number in the LLE (cf documentation). The tuple contains the wavelength
         in m for the 99% low, 25% low, centre/max, 25% high, 99% high. They do
         no have to be extremely precise. The most important is the centre, and
         that they are all increasing values. If the device doesn't have the
         source it can be skipped.
        _serial (serial): for internal use only, directly use a opened serial
         connection
        """
        # start with this opening the port: if it fails, we are done
        if _serial is not None:
            self._try_recover = False
            self._serial = _serial
            self._port = ""
        else:
            self._serial, self._port = self._findDevice(port)
            logging.info("Found LLE device on port %s", self._port)
            self._try_recover = True

        # to acquire before sending anything on the serial port
        self._ser_access = threading.Lock()

        # Init the LLE
        self._initDevice()

        if _serial is not None:  # used for port testing => only simple init
            return

        # parse source and do some sanity check
        if not sources or not isinstance(sources, dict):
            logging.error(
                "sources argument must be a dict of source name -> wavelength 5 points"
            )
            raise ValueError("Incorrect sources argument")

        self._source_id = []  # source number for each spectra
        self._gy = []  # indexes of green and yellow source
        self._rcubt = []  # indexes of other sources
        spectra = []  # list of the 5 wavelength points
        for cn, wls in sources.items():
            cn = cn.lower()
            if cn not in COLOUR_TO_SOURCE:
                raise ValueError(
                    "Sources argument contains unknown colour '%s'" % cn)
            if len(wls) != 5:
                raise ValueError(
                    "Sources colour '%s' doesn't have exactly 5 wavelength points"
                    % cn)
            prev_wl = 0
            for wl in wls:
                if 0 > wl or wl > 100e-6:
                    raise ValueError(
                        "Sources colour '%s' has unexpected wavelength = %f nm"
                        % (cn, wl * 1e9))
                if prev_wl > wl:
                    raise ValueError(
                        "Sources colour '%s' has unsorted wavelengths" % cn)
            self._source_id.append(COLOUR_TO_SOURCE[cn])
            if cn in ["green", "yellow"]:
                self._gy.append(len(spectra))
            else:
                self._rcubt.append(len(spectra))
            spectra.append(tuple(wls))

        model.Emitter.__init__(self, name, role, **kwargs)

        self._shape = ()
        self._max_power = 0.4  # W (According to doc: ~400mW)
        self.power = model.FloatContinuous(0., (0., self._max_power), unit="W")

        # emissions is list of 0 <= floats <= 1.
        self._intensities = [0.] * len(spectra)  # start off
        self.emissions = model.ListVA(list(self._intensities),
                                      unit="",
                                      setter=self._setEmissions)
        self.spectra = model.ListVA(spectra, unit="m", readonly=True)

        self._prev_intensities = [None] * len(
            spectra)  # => will update for sure
        self._updateIntensities()  # turn off every source

        self.power.subscribe(self._updatePower)
        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (
            odemis.__version__, driver.getSerialDriver(self._port))
        self._hwVersion = "Lumencor Light Engine"  # hardware doesn't report any version

        # Update temperature every 10s
        current_temp = self.GetTemperature()
        self.temperature = model.FloatVA(current_temp,
                                         unit=u"°C",
                                         readonly=True)
        self._temp_timer = util.RepeatingTimer(10, self._updateTemperature,
                                               "LLE temperature update")
        self._temp_timer.start()
Exemple #18
0
    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, ))
Exemple #19
0
    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()
Exemple #20
0
    def __init__(self, name, role, port, axes, inverted=None, **kwargs):
        """
        :param axes (dict: {"x", "y", "z"} --> dict): axis name --> axis parameters
            Each axis is specified by a set of parameters.
            After successful configuration with the pmconfig.py script, the only required parameter for a default motor
            is the address which was set during the configuration process.
            The spc parameter (conversion between motor steps and encoder counts) is typically saved in the flash
            memory of the controller during the configuration process. The flash value is overridden by the
            value in the parameter dict.
            Depending on the type of motor, the encoder_resolution and range might need to be adjusted.

            Axis parameters:
                axis_number (0 <= int <= 127): typically 1-3 for x-z, required
                closed_loop (bool): True for closed loop (with encoder), default to True
                encoder_resolution (float): encoder resolution in m/step
                spc (float): motor steps per encoder count, default to value in non-volatile memory
                limit_type (0 <= int <= 2): type of limit switch, 0: no limit, 1: active high, 2: active low, default 0
                range (tuple of float): in m, default to (0, STROKE_RANGE)
                speed (float): speed in m/s
                unit (str), default to m
        """
        self._axis_map = {}  # axis name -> axis number used by controller
        self._closed_loop = {}  # axis name (str) -> bool (True if closed loop)
        self._speed_steps = {
        }  # axis name (str) -> int, speed in steps per meter
        self._portpattern = port

        # Conversion factors
        # Count refers to encoder counts, step refers to motor steps. The encoder counts are fixed and given
        # as a parameter to the axis, the motor counts are determined during configuration and are usually
        # stored in flash memory.
        # ._steps_per_meter is redundant, but convenient
        self._steps_per_count = {}  # axis name (str) -> float
        self._steps_per_meter = {}  # axis name (str) -> float
        self._counts_per_meter = {}  # axis name (str) -> float

        # Parse axis parameters and create axis
        axes_def = {}  # axis name -> Axis object
        for axis_name, axis_par in axes.items():
            if 'axis_number' in axis_par:
                axis_num = axis_par['axis_number']
                if axis_num not in range(128):
                    raise ValueError(
                        "Invalid axis number %s, needs to be 0 <= int <= 127."
                        % axis_num)
                elif axis_num in self._axis_map.values():
                    axname = self._axis_map[axis_num]
                    raise ValueError(
                        "Invalid axis number %s, already assigned to axis %s."
                        % (axis_num, axname))
                else:
                    self._axis_map[axis_name] = axis_par['axis_number']
            else:
                raise ValueError("Axis %s has no axis number." % axis_name)

            if 'closed_loop' in axis_par:
                closed_loop = axis_par['closed_loop']
            else:
                closed_loop = False
                logging.info(
                    "Axis mode (closed/open loop) not specified for axis %s. Assuming closed loop.",
                    axis_name)
            self._closed_loop[axis_name] = closed_loop

            if 'encoder_resolution' in axis_par:
                self._counts_per_meter[axis_name] = 1 / axis_par[
                    'encoder_resolution']  # approximately 5e-6 m / step
            else:
                self._counts_per_meter[axis_name] = DEFAULT_COUNTS_PER_METER
                logging.info(
                    "Axis %s has no encoder resolution, assuming %s." %
                    (axis_name, 1 / DEFAULT_COUNTS_PER_METER))

            if 'limit_type' in axis_par:
                limit_type = axis_par['limit_type']
            else:
                logging.info("Axis %s has not limit switch." % axis_name)
                limit_type = 0

            if 'range' in axis_par:
                axis_range = axis_par['range']
            else:
                axis_range = (0, STROKE_RANGE)
                logging.info("Axis %s has no range. Assuming %s", axis_name,
                             axis_range)

            if 'spc' in axis_par:
                self._steps_per_count[axis_name] = axis_par[
                    'spc']  # approximately 5e-6 m / step
            else:
                logging.info(
                    "Axis %s has no spc parameter, will use value from flash."
                    % axis_name)
                # None for now, will read value from flash later.
                self._steps_per_count[axis_name] = None

            if 'speed' in axis_par:
                self._speed = axis_par['speed']
            else:
                self._speed = DEFAULT_AXIS_SPEED
                logging.info(
                    "Axis %s was not given a speed value. Assuming %s",
                    axis_name, self._speed)

            if 'unit' in axis_par:
                axis_unit = axis_par['unit']
            else:
                axis_unit = "m"
                logging.info("Axis %s has no unit. Assuming %s", axis_name,
                             axis_unit)

            ad = model.Axis(canAbs=closed_loop,
                            unit=axis_unit,
                            range=axis_range)
            axes_def[axis_name] = ad

        Actuator.__init__(self,
                          name,
                          role,
                          axes=axes_def,
                          inverted=inverted,
                          **kwargs)
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
        self._ser_access = threading.RLock()

        # Connect to hardware
        self._port = None  # port number
        min_axis = min(self._axis_map.values())
        self._serial = self._findDevice(port, min_axis)
        self._recovering = False

        # Get version
        hwVersions = []
        for ax_name, ax_num in self._axis_map.items():
            ver = self.getVersion(ax_num)
            sn = self.getSerialNumber(ax_num)
            hwVersions.append("Axis %s ('%s') version: %s, " %
                              (ax_num, ax_name, ver) +
                              "serial number: %s" % sn)
        self._hwVersion = ", ".join(hwVersions)

        # Configuration
        for axis in self._axis_map.values():
            self.setWaveform(axis, WAVEFORM_DELTA)

        driver_name = getSerialDriver(self._port)
        self._swVersion = "Serial driver: %s" % (driver_name, )

        # Position and referenced VAs
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self.referenced = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()
        for axname in self._axis_map.keys():
            self.referenced.value[
                axname] = False  # just assume they haven't been referenced

        # Load values from flash, write spc if provided, otherwise read spc
        for axname, axis in self._axis_map.items():
            # Load values from flash (most importantly spc parameter)
            self.initFromFlash(axis)

            if self._steps_per_count[axname]:
                # Write SPC if provided
                # Value that's written to register needs to be multiplied by (65536 * 4) (see manual)
                self.writeParam(axis, 11,
                                self._steps_per_count[axname] * (65536 * 4))
            else:
                # Read spc from flash. If value is not reasonable, use default
                val = int(self.readParam(axis, 11))
                if not 20000 <= val <= 150000:
                    # that's not a reasonable value, the flash was probably not configured
                    logging.warning(
                        "Axis %s spc value not configured properly, current value: %s"
                        % (axis, val))
                    logging.info("Axis %s using spc value %s" %
                                 (axis, DEFAULT_SPC))
                    val = DEFAULT_SPC
                else:
                    val = val / (65536 * 4)
                    logging.info("Axis %s is using spc value from flash: %s" %
                                 (axis, val))
                self._steps_per_count[axname] = val
            self._steps_per_meter[axname] = self._steps_per_count[
                axname] * self._counts_per_meter[axname]
            self._speed_steps[axis_name] = round(
                self._speed * self._steps_per_meter[axis_name])

        # Limit switch
        for axis in self._axis_map.values():
            self.setLimitType(axis, limit_type)
Exemple #21
0
    def __init__(self, name, role, port, sources, _serial=None, **kwargs):
        """
        port (string): name of the serial port to connect to. Can be a pattern,
         in which case, all the ports fitting the pattern will be tried, and the
         first one which looks like an LLE will be used.
        sources (dict string -> 5-tuple of float): the light sources (by colour).
         The string is one of the seven names for the sources: "red", "cyan",
         "green", "UV", "yellow", "blue", "teal". They correspond to fix
         number in the LLE (cf documentation). The tuple contains the wavelength
         in m for the 99% low, 25% low, centre/max, 25% high, 99% high. They do
         no have to be extremely precise. The most important is the centre, and
         that they are all increasing values. If the device doesn't have the
         source it can be skipped.
        _serial (serial): for internal use only, directly use a opened serial
         connection
        """
        # start with this opening the port: if it fails, we are done
        if _serial is not None:
            self._try_recover = False
            self._serial = _serial
            self._port = ""
        else:
            self._serial, self._port = self._findDevice(port)
            logging.info("Found LLE device on port %s", self._port)
            self._try_recover = True

        # to acquire before sending anything on the serial port
        self._ser_access = threading.Lock()

        # Init the LLE
        self._initDevice()

        if _serial is not None: # used for port testing => only simple init
            return

        # parse source and do some sanity check
        if not sources or not isinstance(sources, dict):
            logging.error("sources argument must be a dict of source name -> wavelength 5 points")
            raise ValueError("Incorrect sources argument")

        self._source_id = [] # source number for each spectra
        self._gy = [] # indexes of green and yellow source
        self._rcubt = [] # indexes of other sources
        spectra = [] # list of the 5 wavelength points
        for cn, wls in sources.items():
            cn = cn.lower()
            if cn not in COLOUR_TO_SOURCE:
                raise ValueError("Sources argument contains unknown colour '%s'" % cn)
            if len(wls) != 5:
                raise ValueError("Sources colour '%s' doesn't have exactly 5 wavelength points" % cn)
            prev_wl = 0
            for wl in wls:
                if 0 > wl or wl > 100e-6:
                    raise ValueError("Sources colour '%s' has unexpected wavelength = %f nm"
                                     % (cn, wl * 1e9))
                if prev_wl > wl:
                    raise ValueError("Sources colour '%s' has unsorted wavelengths" % cn)
            self._source_id.append(COLOUR_TO_SOURCE[cn])
            if cn in ["green", "yellow"]:
                self._gy.append(len(spectra))
            else:
                self._rcubt.append(len(spectra))
            spectra.append(tuple(wls))

        model.Emitter.__init__(self, name, role, **kwargs)

        self._shape = ()
        self._max_power = 0.4 # W (According to doc: ~400mW)
        self.power = model.FloatContinuous(0., (0., self._max_power), unit="W")

        # emissions is list of 0 <= floats <= 1.
        self._intensities = [0.] * len(spectra) # start off
        self.emissions = model.ListVA(list(self._intensities), unit="",
                                      setter=self._setEmissions)
        self.spectra = model.ListVA(spectra, unit="m", readonly=True)

        self._prev_intensities = [None] * len(spectra) # => will update for sure
        self._updateIntensities() # turn off every source

        self.power.subscribe(self._updatePower)
        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver.getSerialDriver(self._port))
        self._hwVersion = "Lumencor Light Engine" # hardware doesn't report any version

        # Update temperature every 10s
        current_temp = self.GetTemperature()
        self.temperature = model.FloatVA(current_temp, unit=u"°C", readonly=True)
        self._temp_timer = util.RepeatingTimer(10, self._updateTemperature,
                                               "LLE temperature update")
        self._temp_timer.start()
Exemple #22
0
    def __init__(self, name, role, port, turret=None, _noinit=False, **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.
        inverted (None): it is not allowed to invert the axes
        _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
        self._serial = self.openSerialPort(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

        # 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, **kwargs)

        # 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

        pos = {"wavelength": self.GetWavelength(),
               "grating": self.GetGrating()}
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(pos, unit="m", readonly=True)

        # store focal length and inclusion angle for the polynomial computation
        try:
            self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name]
            self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])
        except KeyError:
            self._focal_length = None
            self._inclusion_angle = None
Exemple #23
0
    def __init__(self, name, role, port, turret=None, _noinit=False, **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.
        inverted (None): it is not allowed to invert the axes
        _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

        # 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, **kwargs)

        # 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

        pos = {"wavelength": self.GetWavelength(),
               "grating": self.GetGrating()}
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(pos, unit="m", readonly=True)

        # store focal length and inclusion angle for the polynomial computation
        try:
            self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name]
            self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])
        except KeyError:
            self._focal_length = None
            self._inclusion_angle = None
Exemple #24
0
    def __init__(self, name, role, port, axes, ustepsize, refproc=None, temp=False, **kwargs):
        """
        port (str): port name (use /dev/fake for a simulator)
        axes (list of str): names of the axes, from the 1st to the 3rd.
        ustepsize (list of float): size of a microstep in m (the smaller, the
          bigger will be a move for a given distance in m)
        refproc (str or None): referencing (aka homing) procedure type. Use
          None to indicate it's not possible (no reference/limit switch) or the
          name of the procedure. For now only "2xFinalForward" is accepted.
        temp (bool): if True, will read the temperature from the analogue input
         (10 mV <-> 1 °C)
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if len(axes) != 3:
            raise ValueError("Axes must be a list of 3 axis names (got %s)" % (axes,))
        self._axes_names = axes # axes names in order

        if len(axes) != len(ustepsize):
            raise ValueError("Expecting %d ustepsize (got %s)" %
                             (len(axes), ustepsize))

        if refproc not in {REFPROC_2XFF, REFPROC_FAKE, None}:
            raise ValueError("Reference procedure %s unknown" % (refproc, ))
        self._refproc = refproc

        for sz in ustepsize:
            if sz > 10e-3: # sz is typically ~1µm, so > 1 cm is very fishy
                raise ValueError("ustepsize should be in meter, but got %g" % (sz,))
        self._ustepsize = ustepsize

        try:
            self._serial = self._openSerialPort(port)
        except serial.SerialException:
            raise HwError("Failed to find device %s on port %s. Ensure it is "
                          "connected to the computer." % (name, port))
        self._port = port
        self._ser_access = threading.Lock()
        self._target = 1 # Always one, when directly connected via USB

        self._resynchonise()

        modl, vmaj, vmin = self.GetVersion()
        if modl != 3110:
            logging.warning("Controller TMCM-%d is not supported, will try anyway",
                            modl)
        if name is None and role is None: # For scan only
            return

        if port != "/dev/fake": # TODO: support programs in simulator
            # Detect if it is "USB bus powered" by using the fact that programs
            # don't run when USB bus powered
            addr = 80 # big enough to not overlap with REFPROC_2XFF programs
            prog = [(9, 50, 2, 1), # Set global param 50 to 1
                    (28,), # STOP
                    ]
            self.UploadProgram(prog, addr)
            if not self._isFullyPowered():
                # Only a warning, at the power can be connected afterwards
                logging.warning("Device %s has no power, the motor will not move", name)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time

        axes_def = {}
        for n, sz in zip(self._axes_names, self._ustepsize):
            # Mov abs supports ±2³¹ but the actual position is only within ±2²³
            rng = [(-2 ** 23) * sz, (2 ** 23 - 1) * sz]
            # Probably not that much, but there is no info unless the axis has
            # limit switches and we run a referencing
            axes_def[n] = model.Axis(range=rng, unit="m")
        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        for i, a in enumerate(self._axes_names):
            self._init_axis(i)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        self._hwVersion = "TMCM-%d (firmware %d.%02d)" % (modl, vmaj, vmin)

        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        # TODO: add support for changing speed. cf p.68: axis param 4 + p.81 + TMC 429 p.6
        self.speed = model.VigilantAttribute({}, unit="m/s", readonly=True)
        self._updateSpeed()

        if refproc is not None:
            # str -> boolean. Indicates whether an axis has already been referenced
            axes_ref = dict([(a, False) for a in axes])
            self.referenced = model.VigilantAttribute(axes_ref, readonly=True)

        if temp:
            # One sensor is at the top, one at the bottom of the sample holder.
            # The most interesting is the temperature difference, so just
            # report both.
            self.temperature = model.FloatVA(0, unit=u"°C", readonly=True)
            self.temperature1 = model.FloatVA(0, unit=u"°C", readonly=True)
            self._temp_timer = util.RepeatingTimer(10, self._updateTemperatureVA,
                                                  "TMCM temperature update")
            self._updateTemperatureVA() # make sure the temperature is correct
            self._temp_timer.start()
Exemple #25
0
    def test_getSerialDriver(self):
        # very simple to fit any platform => just check it doesn't raise exception

        name = getSerialDriver("booo")
        self.assertEqual("Unknown", name)
Exemple #26
0
    def __init__(self, name, role, port, turret=None, calib=None,
                 _noinit=False, children=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
        children (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 = children["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 child '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, **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()
Exemple #27
0
    def __init__(self, name, role, port, pin_map=None, delay=None, init=None, ids=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.
        Raise an exception if the device cannot be opened
        '''
        if pin_map:
            self.powered = 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())

        # 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()
        for k, v in init.items():
            if v is None:
                del init[k]
        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,))
Exemple #28
0
 def test_getSerialDriver(self):
     # very simple to fit any platform => just check it doesn't raise exception
     
     name = getSerialDriver("booo")
     self.assertEqual("Unknown", name)
Exemple #29
0
    def __init__(self, name, role, port, prot_time=1e-3, prot_curr=50e-6,
                 relay_cycle=None, powered=None, **kwargs):
        '''
        port (str): port name
        prot_time (float): protection trip time (in s)
        prot_curr (float): protection current threshold (in Amperes)
        relay_cycle (None or 0<float): if not None, will power cycle the relay
          with the given delay (in s)
        Raise an exception if the device cannot be opened
        '''
        if powered is None:
            powered = []
        self.powered = powered

        model.PowerSupplier.__init__(self, name, role, **kwargs)

        # get protection time (s) and current (A) properties
        if not 0 <= prot_time < 1e3:
            raise ValueError("prot_time should be a time (in s) but got %s" % (prot_time,))
        self._prot_time = prot_time
        if not 0 <= prot_curr <= 100e-6:
            raise ValueError("prot_curr (%s A) is not between 0 and 100.e-6" % (prot_curr,))
        self._prot_curr = prot_curr

        # TODO: catch errors and convert to HwError
        self._ser_access = threading.Lock()

        self._port = self._findDevice(port)  # sets ._serial
        logging.info("Found PMT Control device on port %s", self._port)

        # Get identification of the PMT control device
        self._idn = self._getIdentification()

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "serial driver: %s" % (driver_name,)
        self._hwVersion = "%s" % (self._idn,)

        # Set protection current and time
        self._setProtectionCurrent(self._prot_curr)
        self._setProtectionTime(self._prot_time)

        # gain, powerSupply and protection VAs
        self.protection = model.BooleanVA(True, setter=self._setProtection,
                                          getter=self._getProtection)
        self._setProtection(True)

        gain_rng = (MIN_GAIN, MAX_GAIN)
        gain = self._getGain()
        self.gain = model.FloatContinuous(gain, gain_rng, unit="V",
                                          setter=self._setGain)

        self.powerSupply = model.BooleanVA(True, setter=self._setPowerSupply)
        self._setPowerSupply(True)

        # will take care of executing supply asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time

        # relay initialization
        if relay_cycle is not None:
            logging.info("Power cycling the relay for %f s", relay_cycle)
            self.setRelay(False)
            time.sleep(relay_cycle)

        # Reset if no powered provided
        if not powered:
            self.setRelay(True)
        else:
            self._supplied = {}
            self.supplied = model.VigilantAttribute(self._supplied, readonly=True)
            self._updateSupplied()