Ejemplo n.º 1
0
    def __init__(self, name, role, parent, ranges=None, **kwargs):
        axes = {
            "pressure":
            model.Axis(unit="Pa",
                       choices={
                           PRESSURE_VENTED: "vented",
                           PRESSURE_PUMPED: "vacuum"
                       })
        }
        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes,
                                **kwargs)

        # last official position
        if self.GetStatus() == 0:
            self._position = PRESSURE_PUMPED
        else:
            self._position = PRESSURE_VENTED

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({"pressure": self._position},
                                                unit="Pa",
                                                readonly=True)
        # Almost the same as position, but gives the current position
        self.pressure = model.VigilantAttribute(self._position,
                                                unit="Pa",
                                                readonly=True)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
Ejemplo n.º 2
0
    def __init__(self, name, role, parent, axes, ranges=None, **kwargs):
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}

        # Just z axis
        a = axes[0]
        # The maximum, obviously, is not 1 meter. We do not actually care
        # about the range since Tescan API will adjust the value set if the
        # required one is out of limits.
        rng = [0, 1]
        axes_def[a] = model.Axis(unit="m", range=rng)

        # start at the centre
        self._position[a] = parent._device.GetWD() * 1e-3

        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes_def,
                                **kwargs)

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(self._applyInversionAbs(
            self._position),
                                                unit="m",
                                                readonly=True)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
Ejemplo n.º 3
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

        self.parent = parent

        axes_def = {
            # Ranges are from the documentation
            "z": model.Axis(unit="m", range=(FOCUS_RANGE[0] * 1e-3, FOCUS_RANGE[1] * 1e-3)),
        }

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Focus position polling")
        self._pos_poll.start()
Ejemplo n.º 4
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

        fwd_info = parent.fwd_info()
        axes_def = {
            "z": model.Axis(unit=fwd_info["unit"], range=fwd_info["range"]),
        }

        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes_def,
                                **kwargs)

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

        # RO, as to modify it the server must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
    def __init__(self, name, role, positions, has_pressure=True, **kwargs):
        """
        Initialises the component
        positions (list of str): each pressure positions supported by the
          component (among the allowed ones)
        has_pressure (boolean): if True, has a pressure VA with the current
         pressure.
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?

        chp = {}
        for p in positions:
            try:
                chp[PRESSURES[p]] = p
            except KeyError:
                raise ValueError("Pressure position %s is unknown" % (p, ))
        axes = {"vacuum": model.Axis(unit="Pa", choices=chp)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_VENTED  # last official position
        self._goal = PRESSURE_VENTED
        self._time_goal = 0  # time the goal was/will be reached
        self._time_start = 0  # time the move started

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({"vacuum": self._position},
                                                unit="Pa",
                                                readonly=True)
        if has_pressure:
            # Almost the same as position, but gives the current position
            self.pressure = model.VigilantAttribute(self._position,
                                                    unit="Pa",
                                                    readonly=True)

            self._press_timer = util.RepeatingTimer(
                1, self._updatePressure, "Simulated pressure update")
            self._press_timer.start()
        else:
            self._press_timer = None

        # Indicates whether the chamber is opened or not
        # Just pretend it's always closed, and allow the user to change that
        # for instance via CLI.
        self.opened = model.BooleanVA(False)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
Ejemplo n.º 7
0
    def __init__(self, timeout=0.3):
        self.timeout = timeout
        self._input_buf = ""  # use str internally instead of bytes, makes indexing easier
        self._output_buf = ""

        self.waveform = {1: WAVEFORM_PARK, 2: WAVEFORM_PARK, 3: WAVEFORM_PARK}
        self.target_pos = {1: 0, 2: 0, 3: 0}
        self.current_pos = {1: 0, 2: 0, 3: 0}
        self.speed = 0
        self.is_moving = False
        self.status = "0000"
        self.indexing = True

        self.executor = CancellableThreadPoolExecutor(1)
Ejemplo n.º 8
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

        self.parent = parent

        axes_def = {
            # Ranges are from the documentation
            "z": model.Axis(unit="m", range=(FOCUS_RANGE[0] * 1e-3, FOCUS_RANGE[1] * 1e-3)),
        }

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Position polling")
        self._pos_poll.start()
Ejemplo n.º 9
0
    def __init__(self, name, role, parent, axes, ranges=None, **kwargs):
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}

        # Just z axis
        a = axes[0]
        # The maximum, obviously, is not 1 meter. We do not actually care
        # about the range since Tescan API will adjust the value set if the
        # required one is out of limits.
        rng = [0, 1]
        axes_def[a] = model.Axis(unit="m", range=rng)

        # start at the centre
        self._position[a] = parent._device.GetWD() * 1e-3

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    self._applyInversionAbs(self._position),
                                    unit="m", readonly=True)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
Ejemplo n.º 10
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """
        axes_def = {}
        self._position = {}

        rng = [-0.5, 0.5]
        axes_def["x"] = model.Axis(unit="m", range=rng)
        axes_def["y"] = model.Axis(unit="m", range=rng)
        axes_def["z"] = model.Axis(unit="m", range=rng)

        # Demand calibrated stage
        if parent._device.StgIsCalibrated() != 1:
            logging.warning(
                "Stage was not calibrated. We are performing calibration now.")
            parent._device.StgCalibrate()

        #Wait for stage to be stable after calibration
        while parent._device.StgIsBusy() != 0:
            # If the stage is busy (movement is in progress), current position is
            # updated approximately every 500 ms
            time.sleep(0.5)

        x, y, z, rot, tilt = parent._device.StgGetPosition()
        self._position["x"] = -x * 1e-3
        self._position["y"] = -y * 1e-3
        self._position["z"] = -z * 1e-3

        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes_def,
                                **kwargs)

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

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(self._applyInversionAbs(
            self._position),
                                                unit="m",
                                                readonly=True)
Ejemplo n.º 11
0
    def __init__(self, name, role, parent, rng=None, **kwargs):
        """
        inverted (set of str): names of the axes which are inverted
        rng (dict str -> (float,float)): axis name -> min/max of the position on
          this axis. Note: if the axis is inverted, the range passed will be
          inverted. Also, if the hardware reports position outside of the range,
          move might fail, as it is considered outside of the range.
        """

        if rng is None:
            rng = {}

        if "x" not in rng:
            rng["x"] = (5e-3, 152e-3)
        if "y" not in rng:
            rng["y"] = (5e-3, 152e-3)
        if "z" not in rng:
            rng["z"] = (5e-3, 40e-3)

        axes_def = {
            # Ranges are from the documentation
            "x": model.Axis(unit="m", range=(rng["x"][0], rng["x"][1])),
            "y": model.Axis(unit="m", range=(rng["y"][0], rng["y"][1])),
            "z": model.Axis(unit="m", range=(rng["z"][0], rng["z"][1])),
        }

        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes_def,
                                **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
Ejemplo n.º 12
0
    def __init__(self, name, role, children, backlash, **kwargs):
        """
        children (dict str -> Stage): dict containing one component, the stage 
        to wrap
        backlash (dict str -> float): for each axis of the stage, the additional 
        distance to move (and the direction). If an axis of the stage is not 
        present, then it’s the same as having 0 as backlash (=> no antibacklash 
        motion is performed for this axis)

        """
        if len(children) != 1:
            raise ValueError("AntiBacklashActuator needs 1 child")

        self._child = children.values()[0]
        self._backlash = backlash
        axes_def = self._child.axes

        # look for axes in backlash not existing in the child
        missing = set(backlash.keys()) - set(axes_def.keys())
        if missing:
            raise ValueError("Child actuator doesn't have the axes %s",
                             missing)

        model.Actuator.__init__(self,
                                name,
                                role,
                                axes=axes_def,
                                children=children,
                                **kwargs)

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

        # Duplicate VAs which are just identical
        # TODO: shall we "hide" the antibacklash move by not updating position
        # while doing this move?
        self.position = self._child.position

        if (hasattr(self._child, "referenced") and isinstance(
                self._child.referenced, model.VigilantAttributeBase)):
            self.referenced = self._child.referenced
        if (hasattr(self._child, "speed") and isinstance(
                self._child.speed, model.VigilantAttributeBase)):
            self.speed = self._child.speed
Ejemplo n.º 13
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)
Ejemplo n.º 14
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)
Ejemplo n.º 15
0
    def __init__(self, name, role, children, backlash, **kwargs):
        """
        children (dict str -> Stage): dict containing one component, the stage
        to wrap
        backlash (dict str -> float): for each axis of the stage, the additional
        distance to move (and the direction). If an axis of the stage is not
        present, then it’s the same as having 0 as backlash (=> no antibacklash 
        motion is performed for this axis)

        """
        if len(children) != 1:
            raise ValueError("AntiBacklashActuator needs 1 child")

        for a, v in backlash.items():
            if not isinstance(a, basestring):
                raise ValueError("Backlash key must be a string but got '%s'" % (a,))
            if not isinstance(v, numbers.Real):
                raise ValueError("Backlash value of %s must be a number but got '%s'" % (a, v))

        self._child = children.values()[0]
        self._backlash = backlash
        axes_def = {}
        for an, ax in self._child.axes.items():
            axes_def[an] = copy.deepcopy(ax)
            axes_def[an].canUpdate = True

        # Whether currently a backlash shift is applied on an axis
        # If True, moving the axis by the backlash value would restore its expected position
        # _shifted_lock must be taken before modifying this attribute
        self._shifted = dict((a, False) for a in axes_def.keys())
        self._shifted_lock = threading.Lock()

        # look for axes in backlash not existing in the child
        missing = set(backlash.keys()) - set(axes_def.keys())
        if missing:
            raise ValueError("Child actuator doesn't have the axes %s" % (missing,))

        model.Actuator.__init__(self, name, role, axes=axes_def,
                                children=children, **kwargs)

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

        # Duplicate VAs which are just identical
        # TODO: shall we "hide" the antibacklash move by not updating position
        # while doing this move?
        self.position = self._child.position

        if model.hasVA(self._child, "referenced"):
            self.referenced = self._child.referenced
        if model.hasVA(self._child, "speed"):
            self.speed = self._child.speed
Ejemplo n.º 16
0
    def __init__(self, name, role, parent, rng=None, **kwargs):
        if rng is None:
            rng = {}
        stage_info = parent.stage_info()
        if "x" not in rng:
            rng["x"] = stage_info["range"]["x"]
        if "y" not in rng:
            rng["y"] = stage_info["range"]["y"]
        if "z" not in rng:
            rng["z"] = stage_info["range"]["z"]

        axes_def = {
            # Ranges are from the documentation
            "x": model.Axis(unit="m", range=rng["x"]),
            "y": model.Axis(unit="m", range=rng["y"]),
            "z": model.Axis(unit="m", range=rng["z"]),
        }

        model.Actuator.__init__(self,
                                name,
                                role,
                                parent=parent,
                                axes=axes_def,
                                **kwargs)
        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
Ejemplo n.º 17
0
    def __init__(self, name, role, positions, has_pressure=True, **kwargs):
        """
        Initialises the component
        positions (list of str): each pressure positions supported by the
          component (among the allowed ones)
        has_pressure (boolean): if True, has a pressure VA with the current
         pressure.
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?

        chp = {}
        for p in positions:
            try:
                chp[PRESSURES[p]] = p
            except KeyError:
                raise ValueError("Pressure position %s is unknown" % (p,))
        axes = {"pressure": model.Axis(unit="Pa", choices=chp)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_VENTED # last official position
        self._goal = PRESSURE_VENTED
        self._time_goal = 0 # time the goal was/will be reached
        self._time_start = 0 # time the move started

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    {"pressure": self._position},
                                    unit="Pa", readonly=True)
        if has_pressure:
            # Almost the same as position, but gives the current position
            self.pressure = model.VigilantAttribute(self._position,
                                        unit="Pa", readonly=True)

            self._press_timer = util.RepeatingTimer(1, self._updatePressure,
                                             "Simulated pressure update")
            self._press_timer.start()
        else:
            self._press_timer = None

        # Indicates whether the chamber is opened or not
        # Just pretend it's always closed, and allow the user to change that
        # for instance via CLI.
        self.opened = model.BooleanVA(False)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
Ejemplo n.º 18
0
    def __init__(self, name, role, parent, ranges=None, **kwargs):
        axes = {"pressure": model.Axis(unit="Pa",
                                       choices={PRESSURE_VENTED: "vented",
                                                PRESSURE_PUMPED: "vacuum"})}
        model.Actuator.__init__(self, name, role, parent=parent, axes=axes, **kwargs)

        # last official position
        if self.GetStatus() == 0:
            self._position = PRESSURE_PUMPED
        else:
            self._position = PRESSURE_VENTED

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    {"pressure": self._position},
                                    unit="Pa", readonly=True)
        # Almost the same as position, but gives the current position
        self.pressure = model.VigilantAttribute(self._position,
                                    unit="Pa", readonly=True)

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
Ejemplo n.º 19
0
    def __init__(self, name, role, children, backlash, **kwargs):
        """
        children (dict str -> Stage): dict containing one component, the stage 
        to wrap
        backlash (dict str -> float): for each axis of the stage, the additional 
        distance to move (and the direction). If an axis of the stage is not 
        present, then it’s the same as having 0 as backlash (=> no antibacklash 
        motion is performed for this axis)

        """
        if len(children) != 1:
            raise ValueError("AntiBacklashActuator needs 1 child")

        self._child = children.values()[0]
        self._backlash = backlash
        axes_def = self._child.axes

        # look for axes in backlash not existing in the child
        missing = set(backlash.keys()) - set(axes_def.keys())
        if missing:
            raise ValueError("Child actuator doesn't have the axes %s", missing)

        model.Actuator.__init__(self, name, role, axes=axes_def,
                                children=children, **kwargs)

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

        # Duplicate VAs which are just identical
        # TODO: shall we "hide" the antibacklash move by not updating position
        # while doing this move?
        self.position = self._child.position

        if (hasattr(self._child, "referenced") and
            isinstance(self._child.referenced, model.VigilantAttributeBase)):
            self.referenced = self._child.referenced
        if (hasattr(self._child, "speed") and
            isinstance(self._child.speed, model.VigilantAttributeBase)):
            self.speed = self._child.speed
Ejemplo n.º 20
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)
Ejemplo n.º 21
0
    def __init__(self, name, role, parent, rng=None, **kwargs):
        """
        inverted (set of str): names of the axes which are inverted
        rng (dict str -> (float,float)): axis name -> min/max of the position on
          this axis. Note: if the axis is inverted, the range passed will be
          inverted. Also, if the hardware reports position outside of the range,
          move might fail, as it is considered outside of the range.
        """

        if rng is None:
            rng = {}

        if "x" not in rng:
            rng["x"] = (5e-3, 152e-3)
        if "y" not in rng:
            rng["y"] = (5e-3, 152e-3)
        if "z" not in rng:
            rng["z"] = (5e-3, 40e-3)

        axes_def = {
            # Ranges are from the documentation
            "x": model.Axis(unit="m", range=(rng["x"][0], rng["x"][1])),
            "y": model.Axis(unit="m", range=(rng["y"][0], rng["y"][1])),
            "z": model.Axis(unit="m", range=(rng["z"][0], rng["z"][1])),
        }

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Position polling")
        self._pos_poll.start()
Ejemplo n.º 22
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """
        axes_def = {}
        self._position = {}

        rng = [-0.5, 0.5]
        axes_def["x"] = model.Axis(unit="m", range=rng)
        axes_def["y"] = model.Axis(unit="m", range=rng)
        axes_def["z"] = model.Axis(unit="m", range=rng)

        # Demand calibrated stage
        if parent._device.StgIsCalibrated() !=1:
            logging.warning("Stage was not calibrated. We are performing calibration now.")
            parent._device.StgCalibrate()

        #Wait for stage to be stable after calibration
        while parent._device.StgIsBusy() != 0:
            # If the stage is busy (movement is in progress), current position is
            # updated approximately every 500 ms
            time.sleep(0.5)
            
        x, y, z, rot, tilt = parent._device.StgGetPosition()
        self._position["x"] = -x * 1e-3
        self._position["y"] = -y * 1e-3
        self._position["z"] = -z * 1e-3

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    self._applyInversionAbs(self._position),
                                    unit="m", readonly=True)
Ejemplo n.º 23
0
class ChamberPressure(model.Actuator):
    """
    This is an extension of the model.Actuator class. It provides functions for
    adjusting the chamber pressure. It actually allows the user to evacuate or
    vent the chamber and get the current pressure of it.
    """
    def __init__(self, name, role, parent, ranges=None, **kwargs):
        axes = {"pressure": model.Axis(unit="Pa",
                                       choices={PRESSURE_VENTED: "vented",
                                                PRESSURE_PUMPED: "vacuum"})}
        model.Actuator.__init__(self, name, role, parent=parent, axes=axes, **kwargs)

        # last official position
        if self.GetStatus() == 0:
            self._position = PRESSURE_PUMPED
        else:
            self._position = PRESSURE_VENTED

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    {"pressure": self._position},
                                    unit="Pa", readonly=True)
        # Almost the same as position, but gives the current position
        self.pressure = model.VigilantAttribute(self._position,
                                    unit="Pa", readonly=True)

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

    def GetStatus(self):
        """
        return int: vacuum status, 
            -1 error 
            0 ready for operation
            1 pumping in progress
            2 venting in progress
            3 vacuum off (pumps are switched off, valves are closed)
            4 chamber open
        """
        with self.parent._acquisition_init_lock:
            status = self.parent._device.VacGetStatus()  # channel 0, reserved
        return status

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

    def _updatePosition(self):
        """
        update the position VA and .pressure VA
        """
        # it's read-only, so we change it via _value
        pos = self.parent._device.VacGetPressure(0)
        self.pressure._value = pos
        self.pressure.notify(pos)

        # .position contains the last known/valid position
        # it's read-only, so we change it via _value
        self.position._value = {"pressure": self._position}
        self.position.notify(self.position.value)

    @isasync
    def moveRel(self, shift):
        self._checkMoveRel(shift)

        # convert into an absolute move
        pos = {}
        for a, v in shift.items:
            pos[a] = self.position.value[a] + v

        return self.moveAbs(pos)

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        return self._executor.submit(self._changePressure, pos["pressure"])

    def _changePressure(self, p):
        """
        Synchronous change of the pressure
        p (float): target pressure
        """
        if p["pressure"] == PRESSURE_VENTED:
            self.parent._device.VacVent()
        else:
            self.parent._device.VacPump()

        start = time.time()
        while not self.GetStatus() == 0:
            if (time.time() - start) >= VACUUM_TIMEOUT:
                raise TimeoutError("Vacuum action timed out")
            # Update chamber pressure until pumping/venting process is done
            self._updatePosition()
        self._position = p
        self._updatePosition()

    def stop(self, axes=None):
        self._executor.cancel()
        logging.warning("Stopped pressure change")
Ejemplo n.º 24
0
class FW102c(model.Actuator):
    """
    Represents a Thorlabs filter wheel FW102C as an actuator.
    It provides one enumerated axis, whose actual band values are provided by
    the user at init.
    """
    # Regex matching the compatible identification strings
    re_idn = "THORLABS.*FW102C.*"

    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)

    def getMetadata(self):
        return self._metadata

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

        with self._ser_access:
            if self._serial:
                self._serial.close()
                self._serial = None

    def _findDevice(self, ports):
        """
        Look for a compatible device
        ports (str): pattern for the port name
        return (str): the name of the port used
        It also sets ._serial and ._idn to contain the opened serial port, and
        the identification string.
        raises:
            IOError: if no device are found
        """
        if os.name == "nt":
            # TODO
            # ports = ["COM" + str(n) for n in range(15)]
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(ports)

        for n in names:
            try:
                self._serial = self._openSerialPort(n)
            except serial.SerialException:
                # not possible to use this port? next one!
                continue

            # check whether it looks like a FW102C
            try:
                # If any garbage was previously received, make it discarded.
                self._serial.write("\r")
                # can have some \x00 bytes at the beginning + "CMD_NOT_DEFINED"
                self._flushInput()
                idn = self.GetIdentification()
                if re.match(self.re_idn, idn):
                    self._idn = idn
                    return n # found it!
            except Exception:
                logging.debug("Port %s doesn't seem to have a FW102C device connected", n)
        else:
            raise HwError("Failed to find a filter wheel FW102C on ports '%s'. "
                          "Check that the device is turned on and connected to "
                          "the computer." % (ports,))

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for the FW102C.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        return (serial): the opened serial port
        """
        ser = serial.Serial(
            port=port,
            baudrate=115200, # only correct if setting was not changed
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=10  # s (can take time when filter changes)
        )

        return ser

    def _flushInput(self):
        """
        Ensure there is no more data queued to be read on the bus (=serial port)
        """
        with self._ser_access:
            self._serial.flush()
            self._serial.flushInput()

            # Shouldn't be necessary, but just in case
            skipped = self._serial.read(1000) # More than 1000 chars => give up
            logging.debug("Skipping input %s", skipped.encode('string_escape'))

    re_err = r"Command error (.*)"
    def _sendQuery(self, com):
        """
        Send a command which expects an answer
        com (string): command to send (not including the ? and the \r)
        return (string): the answer without newline and suffix ("> ")
        raises
            IOError: if there is a timeout
            TLFWError: if the hardware reports an error
        """
        # TODO: handle IOError and automatically try to reconnect (cf LLE)

        assert(len(com) <= 50) # commands cannot be long
        full_com = com + "\r"
        with self._ser_access:
            logging.debug("Sending: '%s'", full_com.encode('string_escape'))
            self._serial.write(full_com)

            # ensure everything is received, before expecting an answer
            self._serial.flush()

            # Read until end of answer
            line = b""
            while True:
                char = self._serial.read() # empty if timeout
                if not char: # should always finish by a "> "
                    raise IOError("Controller timeout, after receiving '%s'" % line.encode('string_escape'))

                # normal char
                line += char
                if line[-2:] == "> ":
                    break

            logging.debug("Received: '%s'", line.encode('string_escape'))

        # remove echo + suffix + new line
        line = line[len(full_com):-2].rstrip("\r")

        # if it's an error message => raise an error
        m = re.match(self.re_err, line)
        if m:
            err = m.group(1)
            raise TLFWError("Device rejected command '%s': %s" % (com, err))

        return line

    def _sendCommand(self, com):
        """
        Send a command which does not expect any answer
        com (string): command to send (not including the ? and the \r)
        return when the command is finished processed
        raises
            IOError: if there is a timeout
            TLFWError: if the hardware reports an error
        """
        self._sendQuery(com)
        # don't return anything

    def GetIdentification(self):
        """
        return (str): model name as reported by the device
        """
        # answer is like "THORLABS FW102C/FW212C Filter Wheel version 1.04"
        return self._sendQuery("*idn?")

    def GetMaxPosition(self):
        """
        return (1<int): maximum number of positions available (eg, 6, 12)
        """
        ans = self._sendQuery("pcount?")
        return int(ans)

    def GetPosition(self):
        """
        return (1<=int<=maxpos): current position
        Note: might be different from the last position set if the user has
         manually changed it.
        """
        ans = self._sendQuery("pos?")
        return int(ans)

    def GetSpeed(self):
        """
        return (0 or 1): current "speed" of the wheel, the bigger the faster
        """
        ans = self._sendQuery("speed?")
        return int(ans)

    def SetPosition(self, pos):
        """
        pos (1<=int<=maxpos): current position
        returns when the new position is set
        raise Exception in case of error
        """
        assert(1 <= pos <= self._maxpos)

        # Estimate how long it'll take
        cur_pos = self.position.value["band"]
        p1, p2 = sorted([pos, cur_pos])
        dist = min(p2 - p1, (6 + p1) - p2)
        if self._speed == 0:
            dur_one = 2  # s
        else:
            dur_one = 1  # s
        maxdur = 1 + dist * dur_one * 2 # x 2 as a safe bet
        prev_timeout = self._serial.timeout
        try:
            self._serial.timeout = maxdur
            self._sendCommand("pos=%d" % pos)
        finally:
            self._serial.timeout = prev_timeout
        logging.debug("Move to pos %d finished", pos)

    # What we don't need:
    # speed?\r1\r>
    # trig?\r0\r>
    # sensors?\r0\r>

    def _doMoveBand(self, pos):
        """
        move to the position and updates the metadata and position once it's over
        """
        self.SetPosition(pos)
        self._updatePosition()

    # high-level methods (interface)
    def _updatePosition(self):
        """
        update the position VA
        Note: it should not be called while holding _ser_access
        """
        pos = {"band": self.GetPosition()}

        # it's read-only, so we change it via _value
        self.position._value = pos
        self.position.notify(self.position.value)

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        # TODO move to the +N next position? (and modulo number of axes)
        raise NotImplementedError("Relative move on enumerated axis not supported")

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)

        return self._executor.submit(self._doMoveBand, pos["band"])

    def stop(self, axes=None):
        self._executor.cancel()

    def selfTest(self):
        """
        check as much as possible that it works without actually moving the motor
        return (boolean): False if it detects any problem
        """
        try:
            pos = self.GetPosition()
            maxpos = self.GetMaxPosition()
            if 1 <= pos <= maxpos:
                return True
        except Exception:
            logging.exception("Selftest failed")

        return False

    @classmethod
    def scan(cls, port=None):
        """
        port (string): name of the serial port. If None, all the serial ports are tried
        returns (list of 2-tuple): name, args (port)
        Note: it's obviously not advised to call this function if a device is already under use
        """
        if port:
            ports = [port]
        else:
            if os.name == "nt":
                ports = ["COM" + str(n) for n in range(15)]
            else:
                ports = glob.glob('/dev/ttyS?*') + glob.glob('/dev/ttyUSB?*')

        logging.info("Serial ports scanning for Thorlabs filter wheel in progress...")
        found = []  # (list of 2-tuple): name, kwargs
        for p in ports:
            try:
                logging.debug("Trying port %s", p)
                dev = cls(None, None, p, bands=None, _scan=True)
            except (serial.SerialException, IOError):
                # not possible to use this port? next one!
                continue

            # Get some more info
            try:
                maxpos = dev.GetMaxPosition()
            except Exception:
                continue
            else:
                # create fake band argument
                bands = {}
                for i in range(1, maxpos + 1):
                    bands[i] = (i * 100e-9, (i + 1) * 100e-9)
                found.append((dev._idn, {"port": p, "bands": bands}))

        return found
Ejemplo n.º 25
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,))
Ejemplo n.º 26
0
class DPSS(model.PowerSupplier):
    '''
    Implements the PowerSupplier class to regulate the power supply of the
    Cobolt DPSS laser, connected via USB.
    '''

    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)

    # Wrapper for the actual firmware functions
    def GetSerialNumber(self):
        return self._sendCommand("sn?")

    def SetOutputPower(self, p):
        """
        p (0 < float): power in W
        """
        assert 1e-6 < p < 1e6
        self._sendCommand("p %.5f" % p)

    def SetLaser(self, state):
        """
        state (bool): True to turn on
        """
        v = 1 if state else 0
        self._sendCommand("l%d" % v)  # No space, as they are different commands

    @isasync
    def supply(self, sup):
        """
        Change the power supply to the defined state for each component given.
        This is an asynchronous method.
        sup dict(string-> boolean): name of the component and new state
        returns (Future): object to control the supply request
        """
        if not sup:
            return model.InstantaneousFuture()
        self._checkSupply(sup)

        return self._executor.submit(self._doSupply, sup)

    def _doSupply(self, sup):
        """
        supply power
        """
        for comp, val in sup.items():
            self.SetLaser(val)
        self._updateSupplied()

    def _updateSupplied(self):
        """
        update the supplied VA
        """
        res = self._sendCommand("l?")
        pwrd = (res == "1")

        # it's read-only, so we change it via _value
        self.supplied._set_value({self._light_name: pwrd}, force_write=True)

    def terminate(self):
        if self._executor:
            self._executor.cancel()
            self._executor.shutdown()
            self._executor = None

        if self._serial:
            self.SetLaser(False)  # TODO: allow to configure with argument to DPSS
            with self._ser_access:
                self._serial.close()
                self._serial = None
                self._file.close()

    def _sendCommand(self, cmd):
        """
        cmd (str): command to be sent to device (without the CR)
        returns (str): answer received from the device (without \n or \r)
        raises:
            DPSSError: if an ERROR is returned by the device.
        """
        cmd = cmd + "\r"
        with self._ser_access:
            logging.debug("Sending command %s", cmd.encode('string_escape'))
            self._serial.write(cmd)

            ans = ''
            while ans[-2:] != '\r\n':
                char = self._serial.read()
                if not char:
                    raise IOError("Timeout after receiving %s" % ans.encode('string_escape'))
                ans += char

            logging.debug("Received answer %s", ans.encode('string_escape'))

            # TODO: check for other error answer?
            # Normally the device either answers OK, or a value, for commands finishing with a "?"
            if ans.startswith("Syntax error"):
                raise DPSSError(ans)

            return ans.rstrip()

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for a Power control device.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        return (serial): the opened serial port
        """
        ser = serial.Serial(
            port=port,
            baudrate=115200,
            timeout=1  # s
        )

        # Purge
        ser.flush()
        ser.flushInput()

        # Try to read until timeout to be extra safe that we properly flushed
        while True:
            char = ser.read()
            if char == '':
                break

        return ser

    def _findDevice(self, ports):
        """
        Look for a compatible device
        ports (str): pattern for the port name
        return (str): the name of the port used
        It also sets ._serial and ._idn to contain the opened serial port, and
        the identification string.
        raises:
            IOError: if no device are found
        """
        # TODO: For debugging purpose
#         if ports == "/dev/fake":
#             self._serial = DPSSSimulator(timeout=1)
#             return ports

        if os.name == "nt":
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(ports)

        for n in names:
            try:
                # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use
                self._file = open(n)  # Open in RO, just to check for lock
                try:
                    fcntl.flock(self._file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)  # Raises IOError if cannot lock
                except IOError:
                    logging.info("Port %s is busy, will not use", n)
                    continue

                self._serial = self._openSerialPort(n)

                try:
                    sn = self.GetSerialNumber()
                except DPSSError:
                    # Can happen if the device has received some weird characters
                    # => try again (now that it's flushed)
                    logging.info("Device answered by an error, will try again")
                    sn = self.GetSerialNumber()
                return n
            except (IOError, DPSSError):
                logging.info("Skipping device on port %s, which didn't seem to be a Cobolt", n)
                # not possible to use this port? next one!
                continue
        else:
            raise HwError("Failed to find a Cobolt device on ports '%s'. "
                          "Check that the device is turned on and connected to "
                          "the computer." % (ports,))
Ejemplo n.º 27
0
class MFF(model.Actuator):
    """
    Represents one Thorlabs Motorized Filter Flipper (ie: MFF101 or MFF102)
    """
    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()

        # It'd be nice to know when a move is over, but it the MFF10x doesn't
        # report ends of move.
        # self.SendMessage(MOT_RESUME_ENDOFMOVEMSGS)

        # If we need constant status updates, then, we'll need to answer them
        # with MOT_ACK_DCSTATUSUPDATE at least once per second.
        # For now we don't track the current device status, so it's easy.
        # When requesting update messages, messages are sent at ~10Hz, even if
        # no change has happened.
        # self.SendMessage(HW_START_UPDATEMSGS) # Causes a lot of messages

        # We should make sure that the led is always off, but apparently, it's
        # off by default until explicitly asking for it (cf MOD_IDENTIFY)

    def terminate(self):
        self._recover = False  # to stop recovering if it's ongoing

        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

        with self._ser_access:
            if self._serial:
                self._serial.close()
                self._serial = None

    def _initHw(self):
        # 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)

    def _recoverHwError(self):
        """
        Returns when the device is back online
        """
        if self._serial:
            self._serial.close()
            self._serial = None

        # keep looking for a serial port with the right serial number
        while self._recover:
            time.sleep(1)
            try:
                self._port = self._getSerialPort(self._sn)
            except HwError:
                logging.debug("Waiting more for the device %s to come back", self._sn)
            except Exception:
                raise
            else:
                break
        else:
            raise IOError("Device disappeared, and driver terminated")

        # TODO: if it failed again, try again?
        logging.info("Found again device %s, on port %s", self._sn, self._port)
        self._serial = self._openSerialPort(self._port)

        # Reinit Hw
        self._initHw()

        # TODO: put back to last known position? Or at least force to a known position?
        self._updatePosition()

        self.state._set_value(model.ST_RUNNING, force_write=True)

    def SendMessage(self, msg, dest=0x50, src=1, p1=None, p2=None, data=None):
        """
        Send a message to a device and possibility wait for its response
        msg (APTSet or APTReq): the message definition
        dest (0<int): the destination ID (always 0x50 if directly over USB)
        p1 (None or 0<=int<=255): param1 (passed as byte2)
        p2 (None or 0<=int<=255): param2 (passed as byte3)
        data (None or bytes): data to be send further. Cannot be mixed with p1
          and p2
        return (None or bytes): the content of the response or None if it was
          an APTSet message
        raise:
           IOError: if failed to send or receive message
        """
        assert 0 <= dest < 0x80

        # create the message
        if data is None: # short message
            p1 = p1 or 0
            p2 = p2 or 0
            com = struct.pack("<HBBBB", msg.id, p1, p2, dest, src)
        else: # long message
            com = struct.pack("<HHBB", msg.id, len(data), dest | 0x80, src) + data

        trials = 0
        with self._ser_access:
            while True:
                trials += 1
                try:
                    logging.debug("Sending: '%s'", ", ".join("%02X" % ord(c) for c in com))
                    self._serial.write(com)

                    if isinstance(msg, APTReq):  # read the response
                        # ensure everything is sent, before expecting an answer
                        self._serial.flush()

                        # Read until end of answer
                        while True:
                            rid, res = self._ReadMessage()
                            if rid == msg.rid:
                                return res
                            logging.debug("Skipping unexpected message %X", rid)
                    else:
                        return
                except IOError as ex:
                    if not self._recover or trials >= 5:
                        raise
                    logging.warning("Failed to send message, trying to recover", exc_info=True)
                    self.state._set_value(ex, force_write=True)
                    self._recoverHwError()

    # Note: unused
    def WaitMessage(self, msg, timeout=None):
        """
        Wait until a specified message is received
        msg (APTMessage)
        timeout (float or None): maximum amount of time to wait
        return (bytes): the 2 params or the data contained in the message
        raise:
            IOError: if timeout happened
        """
        start = time.time()
        # Read until end of answer
        with self._ser_access:
            while True:
                if timeout is not None:
                    left = time.time() - start + timeout
                    if left <= 0:
                        raise IOError("No message %d received in time" % msg.id)
                else:
                    left = None

                mid, res = self._ReadMessage(timeout=left)
                if mid == msg.id:
                    return res
                # TODO: instead of discarding the message, it could go into a
                # queue, to be handled later
                logging.debug("Skipping unexpected message %X", mid)

    def _ReadMessage(self, timeout=None):
        """
        Reads the next message
        timeout (0 < float): maximum time to wait for the message
        return:
             mid (int): message ID
             data (bytes): bytes 3&4 or the data of the message
        raise:
           IOError: if failed to send or receive message
        """
        old_timeout = self._serial.timeout
        if timeout is not None:
            # Should be only for the first byte, but doing it for the first 6
            # should rarely matter
            self._serial.timeout = timeout
        try:
            # read the first (required) 6 bytes
            msg = b""
            for i in range(6):
                char = self._serial.read() # empty if timeout
                if not char:
                    raise IOError("Controller timed out, after receiving '%s'" % msg)

                msg += char
        finally:
            self._serial.timeout = old_timeout

        mid = struct.unpack("<H", msg[0:2])[0]
        if not (ord(msg[4]) & 0x80): # short message
            logging.debug("Received: '%s'", ", ".join("%02X" % ord(c) for c in msg))
            return mid, msg[2:4]

        # long message
        length = struct.unpack("<H", msg[2:4])[0]
        for i in range(length):
            char = self._serial.read() # empty if timeout
            if not char:
                raise IOError("Controller timed out, after receiving '%s'" % msg)

            msg += char

        logging.debug("Received: '%s'", ", ".join("%02X" % ord(c) for c in msg))
        return mid, msg[6:]

    # Low level functions
    def GetInfo(self):
        """
        returns:
            serial number (int)
            model number (str)
            type (int)
            firmware version (str)
            notes (str)
            hardware version (int)
            hardware state (int)
            number of channels (int)
        """
        res = self.SendMessage(HW_REQ_INFO)
        # Expects 0x54 bytes
        values = struct.unpack('<I8sHI48s12xHHH', res)
        sn, modl, typ, fmv, notes, hwv, state, nc = values

        # remove trailing 0's
        modl = modl.rstrip("\x00")
        notes = notes.rstrip("\x00")

        # Convert firmware version to a string
        fmvs = "%d.%d.%d" % ((fmv & 0xff0000) >> 16,
                             (fmv & 0xff00) >> 8,
                             fmv & 0xff)

        return sn, modl, typ, fmvs, notes, hwv, state, nc

    def MoveJog(self, pos):
        """
        Move the position. Note: this is asynchronous.
        pos (int): 1 or 2
        """
        assert pos in [1, 2]
        # p1 is chan ident, always 1
        self.SendMessage(MOT_MOVE_JOG, p1=1, p2=pos)

    def GetStatus(self):
        """
        return:
            pos (int): position count
            status (int): status, as a flag of STA_*
        """
        res = self.SendMessage(MOT_REQ_STATUSUPDATE)
        # expect 14 bytes
        c, pos, enccount, status = struct.unpack('<HiiI', res)

        return pos, status

    # high-level methods (interface)
    def _updatePosition(self):
        """
        update the position VA
        """
        _, status = self.GetStatus()
        pos = {}
        for axis in self.axes: # axes contains precisely one axis
            # status' flags should never be present simultaneously
            for f, p in self._status_to_pos.items():
                if f & status:
                    pos[axis] = p
                    break
            else:
                # This can happen if the mount is half-way
                logging.warning("Status %X doesn't contain position information", status)
                return # don't change position

        # it's read-only, so we change it via _value
        self.position._value = self._applyInversion(pos)
        self.position.notify(self.position.value)

    def _waitNoMotion(self, timeout=None):
        """
        Block as long as the controller reports motion
        timeout (0 < float): maximum time to wait for the end of the motion
        """
        start = time.time()

        # Read until end of motion
        while True:
            _, status = self.GetStatus()
            if not (status & STA_IN_MOTION):
                return

            if timeout is not None and (time.time() > start + timeout):
                raise IOError("Device still in motion after %g s" % (timeout,))

            # Give it a small break
            time.sleep(0.05) # 20Hz

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)

        # TODO move to the +N next position? (and modulo number of axes)
        raise NotImplementedError("Relative move on enumerated axis not supported")

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        return self._executor.submit(self._doMovePos, pos.values()[0])

    def stop(self, axes=None):
        self._executor.cancel()

    def _doMovePos(self, pos):
        jogp = self._pos_to_jog[pos]
        self.MoveJog(jogp)
        self._waitNoMotion(10) # by default, a move lasts ~0.5 s
        self._updatePosition()

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for a Thorlabs APT device.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        return (serial): the opened serial port
        """
        # For debugging purpose
        if port == "/dev/fake":
            return MFF102Simulator(timeout=1)

        ser = serial.Serial(
            port=port,
            baudrate=115200,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            rtscts=True,
            timeout=1  # s
        )

        # Purge (as recommended in the documentation)
        time.sleep(0.05) # 50 ms
        ser.flush()
        ser.flushInput()
        time.sleep(0.05) # 50 ms

        # Prepare the port
        ser.setRTS()

        return ser

    def _getSerialPort(self, sn):
        """
        sn (str): serial number of the device
        return (str): serial port name (eg: "/dev/ttyUSB0" on Linux)
        """
        if sys.platform.startswith('linux'):
            # Look for each USB device, if the serial number is good
            sn_paths = glob.glob('/sys/bus/usb/devices/*/serial')
            for p in sn_paths:
                try:
                    f = open(p)
                    snp = f.read().strip()
                except IOError:
                    logging.debug("Failed to read %s, skipping device", p)
                if snp == sn:
                    break
            else:
                # There is a known problem with the APT devices that prevent
                # them from connecting to USB if they are connected via a hub
                # and powered on before the host PC.
                raise HwError("No USB device with S/N %s. "
                              "Check that the Thorlabs filter flipper was "
                              "turned on *after* the host computer." % sn)

            # Deduce the tty:
            # .../3-1.2/serial => .../3-1.2/3-1.2:1.0/ttyUSB1
            sys_path = os.path.dirname(p)
            usb_num = os.path.basename(sys_path)
            tty_paths = glob.glob("%s/%s/ttyUSB?*" % (sys_path, usb_num + ":1.0"))
            if not tty_paths:
                raise ValueError("Failed to find tty for device with S/N %s" % sn)
            tty = os.path.basename(tty_paths[0])

            # Convert to /dev
            # Note: that works because udev rules create a dev with the same name
            # otherwise, we would need to check the char numbers
            return "/dev/%s" % (tty,)
        else:
            # TODO: Windows version
            raise NotImplementedError("OS not yet supported")

    def _getSerialNumber(self, port):
        """
        Get the serial number of the device (via USB info)
        port (str): port name of the device (eg: "/dev/ttyUSB0" on Linux)
        return (str): serial number
        """
        if sys.platform.startswith('linux'):
            # Go reverse from getSerialPort():
            # /sys/bus/usb-serial/devices/ttyUSB0
            # -> read the link and remove the last two levels
            try:
                tty = os.path.basename(port)
                sys_path = "/sys/bus/usb-serial/devices/" + tty
                usb_path = os.path.join(os.path.dirname(sys_path), os.readlink(sys_path))
                serial_path = usb_path + "/../../serial"
                f = open(serial_path)
                snp = f.read().strip()
                return snp
            except (IOError, OSError):
                raise LookupError("Failed to find serial number of %s" % (port,))
        else:
            # TODO: Windows version
            raise NotImplementedError("OS not yet supported")

    @classmethod
    def scan(cls):
        """
        returns (list of 2-tuple): name, args (sn)
        Note: it's obviously not advised to call this function if a device is already under use
        """
        logging.info("Serial ports scanning for Thorlabs MFFxxx in progress...")
        found = []  # (list of 2-tuple): name, kwargs

        if sys.platform.startswith('linux'):
            # Look for each USB device, if the serial number is potentially good
            sn_paths = glob.glob('/sys/bus/usb/devices/*/serial')
            for p in sn_paths:
                try:
                    f = open(p)
                    snp = f.read().strip()
                except IOError:
                    logging.debug("Failed to read %s, skipping device", p)
                if not (snp.startswith(SN_PREFIX_MFF) and len(snp) == 8):
                    continue

                # Deduce the tty:
                # .../3-1.2/serial => .../3-1.2/3-1.2:1.0/ttyUSB1
                sys_path = os.path.dirname(p)
                usb_num = os.path.basename(sys_path)
                logging.info("Looking at device %s with S/N=%s", usb_num, snp)
                tty_paths = glob.glob("%s/%s/ttyUSB?*" % (sys_path, usb_num + ":1.0"))
                if not tty_paths: # 0 or 1 paths
                    continue
                tty = os.path.basename(tty_paths[0])

                # Convert to /dev
                # Note: that works because udev rules create a dev with the same name
                # otherwise, we would need to check the char numbers
                port = "/dev/%s" % (tty,)

                # open and try to communicate
                try:
                    dev = cls(name="test", role="test", port=port)
                    _, modl, typ, fmv, notes, hwv, state, nc = dev.GetInfo()
                    found.append((modl, {"sn": snp, "axis": "rz"}))
                except Exception:
                    pass
        else:
            # TODO: Windows version
            raise NotImplementedError("OS not yet supported")

        return found
Ejemplo n.º 28
0
class FixedPositionsActuator(model.Actuator):
    """
    A generic actuator component which only allows moving to fixed positions
    defined by the user upon initialization. It is actually a wrapper to just
    one axis/actuator and it can also apply cyclic move e.g. in case the
    actuator moves a filter wheel.
    """

    def __init__(self, name, role, children, axis_name, positions, cycle=None, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis
        axis_name (str): axis name in the child actuator
        positions (set or dict value -> str): positions where the actuator is allowed to move
        cycle (float): if not None, it means the actuator does a cyclic move and this value represents a full cycle
        """
        # TODO: forbid inverted
        if len(children) != 1:
            raise ValueError("FixedPositionsActuator needs precisely one child")

        self._cycle = cycle
        self._move_sum = 0
        self._position = {}
        self._referenced = {}
        axis, child = children.items()[0]
        self._axis = axis
        self._child = child
        self._caxis = axis_name
        self._positions = positions
        # Executor used to reference and move to nearest position
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time

        if not isinstance(child, model.ComponentBase):
            raise ValueError("Child %s is not a component." % (child,))
        if not hasattr(child, "axes") or not isinstance(child.axes, dict):
            raise ValueError("Child %s is not an actuator." % child.name)

        if cycle is not None:
            # just an offset to reference switch position
            self._offset = self._cycle / len(self._positions)
            if not all(0 <= p < cycle for p in positions.keys()):
                raise ValueError("Positions must be between 0 and %s (non inclusive)" % (cycle,))

        ac = child.axes[axis_name]
        axes = {axis: model.Axis(choices=positions, unit=ac.unit)}  # TODO: allow the user to override the unit?

        model.Actuator.__init__(self, name, role, axes=axes, children=children, **kwargs)

        self._position = {}
        self.position = model.VigilantAttribute({}, readonly=True)

        logging.debug("Subscribing to position of child %s", child.name)
        child.position.subscribe(self._update_child_position, init=True)

        if model.hasVA(child, "referenced") and axis_name in child.referenced.value:
            self._referenced[axis] = child.referenced.value[axis_name]
            self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True)
            child.referenced.subscribe(self._update_child_ref)

        # If the axis can be referenced => do it now (and move to a known position)
        # In case of cyclic move always reference
        if not self._referenced.get(axis, True) or (self._cycle and axis in self._referenced):
            # The initialisation will not fail if the referencing fails
            f = self.reference({axis})
            f.add_done_callback(self._on_referenced)
        else:
            # If not at a known position => move to the closest known position
            nearest = util.find_closest(self._child.position.value[self._caxis], self._positions.keys())
            self.moveAbs({self._axis: nearest}).result()

    def _on_referenced(self, future):
        try:
            future.result()
        except Exception as e:
            self._child.stop({self._caxis})  # prevent any move queued
            self.state._set_value(e, force_write=True)
            logging.exception(e)

    def _update_child_position(self, value):
        p = value[self._caxis]
        if self._cycle is not None:
            p %= self._cycle
        self._position[self._axis] = p
        self._updatePosition()

    def _update_child_ref(self, value):
        self._referenced[self._axis] = value[self._caxis]
        self._updateReferenced()

    def _updatePosition(self):
        """
        update the position VA
        """
        # if it is an unsupported position report the nearest supported one
        real_pos = self._position[self._axis]
        nearest = util.find_closest(real_pos, self._positions.keys())
        if not util.almost_equal(real_pos, nearest):
            logging.warning("Reporting axis %s @ %s (known position), while physical axis %s @ %s",
                            self._axis, nearest, self._caxis, real_pos)
        pos = {self._axis: nearest}
        logging.debug("reporting position %s", pos)
        self.position._set_value(pos, force_write=True)

    def _updateReferenced(self):
        """
        update the referenced VA
        """
        # .referenced is copied to detect changes to it on next update
        self.referenced._set_value(self._referenced.copy(), force_write=True)

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        raise NotImplementedError("Relative move on fixed positions axis not supported")

    @isasync
    def moveAbs(self, pos):
        """
        Move the actuator to the defined position in m for each axis given.
        pos dict(string-> float): name of the axis and position in m
        """
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)
        f = self._executor.submit(self._doMoveAbs, pos)

        return f

    def _doMoveAbs(self, pos):
        axis, distance = pos.items()[0]
        logging.debug("Moving axis %s (-> %s) to %g", self._axis, self._caxis, distance)

        if self._cycle is None:
            move = {self._caxis: distance}
            self._child.moveAbs(move).result()
        else:
            # Optimize by moving through the closest way
            cur_pos = self._child.position.value[self._caxis]
            vector1 = distance - cur_pos
            mod1 = vector1 % self._cycle
            vector2 = cur_pos - distance
            mod2 = vector2 % self._cycle
            if abs(mod1) < abs(mod2):
                self._move_sum += mod1
                if self._move_sum >= self._cycle:
                    # Once we are about to complete a full cycle, reference again
                    # to get rid of accumulated error
                    self._move_sum = 0
                    # move to the reference switch
                    move_to_ref = (self._cycle - cur_pos) % self._cycle + self._offset
                    self._child.moveRel({self._caxis: move_to_ref}).result()
                    self._child.reference({self._caxis}).result()
                    move = {self._caxis: distance}
                else:
                    move = {self._caxis: mod1}
            else:
                move = {self._caxis:-mod2}
                self._move_sum -= mod2

            self._child.moveRel(move).result()

    def _doReference(self, axes):
        logging.debug("Referencing axis %s (-> %s)", self._axis, self._caxis)
        f = self._child.reference({self._caxis})
        f.result()

        # If we just did homing and ended up to an unsupported position, move to
        # the nearest supported position
        cp = self._child.position.value[self._caxis]
        if (cp not in self._positions):
            nearest = util.find_closest(cp, self._positions.keys())
            self._doMoveAbs({self._axis: nearest})

    @isasync
    def reference(self, axes):
        if not axes:
            return model.InstantaneousFuture()
        self._checkReference(axes)

        f = self._executor.submit(self._doReference, axes)
        return f
    reference.__doc__ = model.Actuator.reference.__doc__

    def stop(self, axes=None):
        """
        stops the motion
        axes (iterable or None): list of axes to stop, or None if all should be stopped
        """
        if axes is not None:
            axes = set()
            if self._axis in axes:
                axes.add(self._caxis)

        self._child.stop(axes=axes)

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown(wait=True)
            self._executor = None

        self._child.position.unsubscribe(self._update_child_position)
Ejemplo n.º 29
0
class AntiBacklashActuator(model.Actuator):
    """
    This is a stage wrapper that takes a stage and ensures that every move
    always finishes in the same direction.
    """
    def __init__(self, name, role, children, backlash, **kwargs):
        """
        children (dict str -> Stage): dict containing one component, the stage
        to wrap
        backlash (dict str -> float): for each axis of the stage, the additional
        distance to move (and the direction). If an axis of the stage is not
        present, then it’s the same as having 0 as backlash (=> no antibacklash 
        motion is performed for this axis)

        """
        if len(children) != 1:
            raise ValueError("AntiBacklashActuator needs 1 child")

        for a, v in backlash.items():
            if not isinstance(a, basestring):
                raise ValueError("Backlash key must be a string but got '%s'" % (a,))
            if not isinstance(v, numbers.Real):
                raise ValueError("Backlash value of %s must be a number but got '%s'" % (a, v))

        self._child = children.values()[0]
        self._backlash = backlash
        axes_def = {}
        for an, ax in self._child.axes.items():
            axes_def[an] = copy.deepcopy(ax)
            axes_def[an].canUpdate = True

        # Whether currently a backlash shift is applied on an axis
        # If True, moving the axis by the backlash value would restore its expected position
        # _shifted_lock must be taken before modifying this attribute
        self._shifted = dict((a, False) for a in axes_def.keys())
        self._shifted_lock = threading.Lock()

        # look for axes in backlash not existing in the child
        missing = set(backlash.keys()) - set(axes_def.keys())
        if missing:
            raise ValueError("Child actuator doesn't have the axes %s" % (missing,))

        model.Actuator.__init__(self, name, role, axes=axes_def,
                                children=children, **kwargs)

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

        # Duplicate VAs which are just identical
        # TODO: shall we "hide" the antibacklash move by not updating position
        # while doing this move?
        self.position = self._child.position

        if model.hasVA(self._child, "referenced"):
            self.referenced = self._child.referenced
        if model.hasVA(self._child, "speed"):
            self.speed = self._child.speed

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

    def _antiBacklashMove(self, axes):
        """
        Moves back the axes to their official position by reverting the anti-backlash shift
        axes (list of str): the axes to revert
        """
        sub_backlash = {}  # same as backlash but only contains the axes moved
        with self._shifted_lock:
            for a in axes:
                if self._shifted[a]:
                    if a in self._backlash:
                        sub_backlash[a] = self._backlash[a]
                    self._shifted[a] = False

        if sub_backlash:
            logging.debug("Running anti-backlash move %s", sub_backlash)
            self._child.moveRelSync(sub_backlash)

    def _doMoveRel(self, future, shift):
        # move with the backlash subtracted
        sub_shift = {}
        for a, v in shift.items():
            if a not in self._backlash:
                sub_shift[a] = v
            else:
                # optimisation: if move goes in the same direction as backlash
                # correction, then no need to do the correction
                # TODO: only do this if backlash correction has already been applied once?
                if v * self._backlash[a] >= 0:
                    sub_shift[a] = v
                else:
                    with self._shifted_lock:
                        if self._shifted[a]:
                            sub_shift[a] = v
                        else:
                            sub_shift[a] = v - self._backlash[a]
                            self._shifted[a] = True

        # Do the backlash + move
        axes = set(shift.keys())
        if not any(self._shifted):
            # a tiny bit faster as we don't sleep
            self._child.moveRelSync(sub_shift)
        else:
            # some antibacklash move needed afterwards => update might be worthy
            f = self._child.moveRel(sub_shift)
            done = False
            while not done:
                try:
                    f.result(timeout=0.01)
                except futures.TimeoutError:
                    pass  # Keep waiting for end of move
                else:
                    done = True

                # Check if there is already a new move to do
                nf = self._executor.get_next_future(future)
                if nf is not None and axes <= nf._update_axes:
                    logging.debug("Ending move control early as next move is an update containing %s", axes)
                    return

        # backlash move
        self._antiBacklashMove(shift.keys())

    def _doMoveAbs(self, future, pos):
        sub_pos = {}
        for a, v in pos.items():
            if a not in self._backlash:
                sub_pos[a] = v
            else:
                shift = v - self.position.value[a]
                with self._shifted_lock:
                    if shift * self._backlash[a] >= 0:
                        sub_pos[a] = v
                        self._shifted[a] = False
                    else:
                        sub_pos[a] = v - self._backlash[a]
                        self._shifted[a] = True

        # Do the backlash + move
        axes = set(pos.keys())
        if not any(self._shifted):
            # a tiny bit faster as we don't sleep
            self._child.moveAbsSync(sub_pos)
        else:  # some antibacklash move needed afterwards => update might be worthy
            f = self._child.moveAbs(sub_pos)
            done = False
            while not done:
                try:
                    f.result(timeout=0.01)
                except futures.TimeoutError:
                    pass  # Keep waiting for end of move
                else:
                    done = True

                # Check if there is already a new move to do
                nf = self._executor.get_next_future(future)
                if nf is not None and axes <= nf._update_axes:
                    logging.debug("Ending move control early as next move is an update containing %s", axes)
                    return

        # anti-backlash move
        self._antiBacklashMove(axes)

    def _createFuture(self, axes, update):
        """
        Return (CancellableFuture): a future that can be used to manage a move
        axes (set of str): the axes that are moved
        update (bool): if it's an update move
        """
        # TODO: do this via the __init__ of subclass of Future?
        f = CancellableFuture()  # TODO: make it cancellable too

        f._update_axes = set()  # axes handled by the move, if update
        if update:
            # Check if all the axes support it
            if all(self.axes[a].canUpdate for a in axes):
                f._update_axes = axes
            else:
                logging.warning("Trying to do a update move on axes %s not supporting update", axes)

        return f

    @isasync
    def moveRel(self, shift, update=False):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)

        f = self._createFuture(set(shift.keys()), update)
        return self._executor.submitf(f, self._doMoveRel, f, shift)

    @isasync
    def moveAbs(self, pos, update=False):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)

        f = self._createFuture(set(pos.keys()), update)
        return self._executor.submitf(f, self._doMoveAbs, f, pos)

    def stop(self, axes=None):
        self._child.stop(axes=axes)

    @isasync
    def reference(self, axes):
        f = self._child.reference(axes)
        return f
Ejemplo n.º 30
0
    def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs):
        """
        address (str): ip address (use "autoip" to automatically scan and find the
          controller, "fake" for a simulator)
        axes (list of str): names of the axes, from the 1st to the 4th, if present.
          if an axis is not connected, put a "".
        stepsize (list of float): size of a step in m (the smaller, the
          bigger will be a move for a given distance in m)
        sn (str or None): serial number of the device (eg, "11500"). If None, the
          driver will use whichever controller is first found.
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if not 1 <= len(axes) <= 4:
            raise ValueError("Axes must be a list of 1 to 4 axis names (got %s)" % (axes,))
        if len(axes) != len(stepsize):
            raise ValueError("Expecting %d stepsize (got %s)" %
                             (len(axes), stepsize))
        self._name_to_axis = {} # str -> int: name -> axis number
        for i, n in enumerate(axes):
            if n == "": # skip this non-connected axis
                continue
            self._name_to_axis[n] = i + 1

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

        self._address = address
        self._sn = sn
        self._accesser = self._openConnection(address, sn)
        self._recover = False

        self._resynchonise()

        if name is None and role is None: # For scan only
            return

        # Seems to really be the device, so handle connection errors fully
        self._recover = True

        modl, fw, sn = self.GetIdentification()
        if modl != "8742":
            logging.warning("Controller %s is not supported, will try anyway", modl)

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

        # Let the controller check the actuators are connected
        self.MotorCheck()

        axes_def = {}
        speed = {}
        for n, i in self._name_to_axis.items():
            sz = self._stepsize[i - 1]
            # TODO: allow to pass the range in m in the arguments
            # Position supports ±2³¹, probably not that much in reality, but
            # there is no info.
            rng = [(-2 ** 31) * sz, (2 ** 31 - 1) * sz]

            # Check the actuator is connected
            mt = self.GetMotorType(i)
            if mt in {MT_NONE, MT_UNKNOWN}:
                raise HwError("Controller failed to detect motor %d, check the "
                              "actuator is connected to the controller" %
                              (i,))
            max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt]
            srng = (0, self._speedToMS(i, max_stp_s))
            speed[n] = self._speedToMS(i, self.GetVelocity(i))

            axes_def[n] = model.Axis(range=rng, speed=srng, unit="m")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        self._swVersion = "%s (IP connection)" % (odemis.__version__,)
        self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn)

        # Note that the "0" position is just the position at which the
        # controller turned on
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed, range=(0, max_speed),
                                        unit="m/s", setter=self._setSpeed)
Ejemplo n.º 31
0
class PM8742(model.Actuator):
    """
    Represents one New Focus picomotor controller 8742.
    """
    def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs):
        """
        address (str): ip address (use "autoip" to automatically scan and find the
          controller, "fake" for a simulator)
        axes (list of str): names of the axes, from the 1st to the 4th, if present.
          if an axis is not connected, put a "".
        stepsize (list of float): size of a step in m (the smaller, the
          bigger will be a move for a given distance in m)
        sn (str or None): serial number of the device (eg, "11500"). If None, the
          driver will use whichever controller is first found.
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if not 1 <= len(axes) <= 4:
            raise ValueError("Axes must be a list of 1 to 4 axis names (got %s)" % (axes,))
        if len(axes) != len(stepsize):
            raise ValueError("Expecting %d stepsize (got %s)" %
                             (len(axes), stepsize))
        self._name_to_axis = {} # str -> int: name -> axis number
        for i, n in enumerate(axes):
            if n == "": # skip this non-connected axis
                continue
            self._name_to_axis[n] = i + 1

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

        self._address = address
        self._sn = sn
        self._accesser = self._openConnection(address, sn)
        self._recover = False

        self._resynchonise()

        if name is None and role is None: # For scan only
            return

        # Seems to really be the device, so handle connection errors fully
        self._recover = True

        modl, fw, sn = self.GetIdentification()
        if modl != "8742":
            logging.warning("Controller %s is not supported, will try anyway", modl)

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

        # Let the controller check the actuators are connected
        self.MotorCheck()

        axes_def = {}
        speed = {}
        for n, i in self._name_to_axis.items():
            sz = self._stepsize[i - 1]
            # TODO: allow to pass the range in m in the arguments
            # Position supports ±2³¹, probably not that much in reality, but
            # there is no info.
            rng = [(-2 ** 31) * sz, (2 ** 31 - 1) * sz]

            # Check the actuator is connected
            mt = self.GetMotorType(i)
            if mt in {MT_NONE, MT_UNKNOWN}:
                raise HwError("Controller failed to detect motor %d, check the "
                              "actuator is connected to the controller" %
                              (i,))
            max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt]
            srng = (0, self._speedToMS(i, max_stp_s))
            speed[n] = self._speedToMS(i, self.GetVelocity(i))

            axes_def[n] = model.Axis(range=rng, speed=srng, unit="m")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        self._swVersion = "%s (IP connection)" % (odemis.__version__,)
        self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn)

        # Note that the "0" position is just the position at which the
        # controller turned on
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed, range=(0, max_speed),
                                        unit="m/s", setter=self._setSpeed)

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown(wait=True)
            self._executor = None

        if self._accesser:
            self._accesser.terminate()
            self._accesser = None

    def _sendOrderCommand(self, cmd, val="", axis=None):
        return self._accesser.sendOrderCommand(cmd, val, axis)

    def _sendQueryCommand(self, cmd, val="", axis=None):
        """
        Same as accesser's sendQueryCommand, but with error recovery
        """
        trials = 0
        while True:
            try:
                return self._accesser.sendQueryCommand(cmd, val, axis)
            except IOError: # Typically due to timeout
                trials += 1
                if not self._recover and trials < 5:
                    raise
                self._recover = False
                try:
                    # can also happen just due to error
                    # => first read error and see if that explains anything
                    self._checkError()
                except IOError:
                    # Sometimes the hardware seems to lose connection
                    # => try to reconnect
                    logging.warning("Device seems disconnected, will try to reconnect")
                    # Sometimes the device gets confused and answers are shifted.
                    # Reset helps, but it also reset the current position, which
                    # is not handy.
                    # self._accesser.sendOrderCommand("RS")
                    self._accesser.terminate()
                    time.sleep(0.5)
                    self._accesser = self._openConnection(self._address, self._sn)
                    self._checkError()
                    logging.info("Recovered lost connection to device %s", self.name)
                finally:
                    self._recover = True

    # Low level functions
    def GetIdentification(self):
        """
        return (str, str, str):
             Model name
             Firmware version (and date)
             serial number
        """
        resp = self._sendQueryCommand("*IDN")
        # expects something like this:
        # New_Focus 8742 v2.2 08/01/13 11511
        try:
            m = re.match("\w+ (?P<model>\w+) (?P<fw>v\S+ \S+) (?P<sn>\d+)", resp)
            modl, fw, sn = m.groups()
        except Exception:
            raise IOError("Failed to decode firmware answer '%s'" %
                          resp.encode('string_escape'))

        return modl, fw, sn

    def GetMotorType(self, axis):
        """
        Read the motor type.
        The motor check action must have been performed before to get correct
          values.
        axis (1<=int<=4): axis number
        return (0<=int<=3): the motor type
        """
        resp = self._sendQueryCommand("QM", axis=axis)
        return int(resp)

    def GetVelocity(self, axis):
        """
        Read the max speed
        axis (1<=int<=4): axis number
        return (0<=int<=2000): the speed in step/s
        """
        resp = self._sendQueryCommand("VA", axis=axis)
        return int(resp)

    def SetVelocity(self, axis, val):
        """
        Write the max speed
        axis (1<=int<=4): axis number
        val (1<=int<=2000): the speed in step/s
        """
        if not 1 <= val <= 2000:
            raise ValueError("Velocity outside of the range 0->2000")
        self._sendOrderCommand("VA", "%d" % (val,), axis)

    def GetAccel(self, axis):
        """
        Read the acceleration
        axis (1<=int<=4): axis number
        return (0<=int): the acceleration in step/s²
        """
        resp = self._sendQueryCommand("AC", axis=axis)
        return int(resp)

    def SetAccel(self, axis, val):
        """
        Write the acceleration
        axis (1<=int<=4): axis number
        val (1<=int<=200000): the acceleration in step/s²
        """
        if not 1 <= val <= 200000:
            raise ValueError("Acceleration outside of the range 0->200000")
        self._sendOrderCommand("AC", "%d" % (val,), axis)

    def MotorCheck(self):
        """
        Run the motor check command, that automatically configure the right
        values based on the type of motors connected.
        """
        self._sendOrderCommand("MC")

    def MoveAbs(self, axis, pos):
        """
        Requests a move to an absolute position. This is non-blocking.
        axis (1<=int<=4): axis number
        pos (-2**31 <= int 2*31-1): position in step
        """
        self._sendOrderCommand("PA", "%d" % (pos,), axis)

    def GetTarget(self, axis):
        """
        Read the target position for the given axis
        axis (1<=int<=4): axis number
        return (int): the position in steps
        """
        # Note, it's not clear what's the difference with PR?
        resp = self._sendQueryCommand("PA", axis=axis)
        return int(resp)

    def MoveRel(self, axis, offset):
        """
        Requests a move to a relative position. This is non-blocking.
        axis (1<=int<=4): axis number
        offset (-2**31 <= int 2*31-1): offset in step
        """
        self._sendOrderCommand("PR", "%d" % (offset,), axis)

    def GetPosition(self, axis):
        """
        Read the actual position for the given axis
        axis (1<=int<=4): axis number
        return (int): the position in steps
        """
        resp = self._sendQueryCommand("TP", axis=axis)
        return int(resp)

    def IsMotionDone(self, axis):
        """
        Check whether the axis is in motion 
        axis (1<=int<=4): axis number
        return (bool): False if in motion, True if motion is finished
        """
        resp = self._sendQueryCommand("MD", axis=axis)
        if resp == "0": # motion in progress
            return False
        elif resp == "1": # no motion
            return True
        else:
            raise IOError("Failed to decode answer about motion '%s'" %
                          resp.encode('string_escape'))

    def AbortMotion(self):
        """
        Stop immediately the motion on all the axes
        """
        self._sendOrderCommand("AB")

    def StopMotion(self, axis):
        """
        Stop nicely the motion (using accel/decel values)
        axis (1<=int<=4): axis number
        """
        self._sendOrderCommand("ST", axis=axis)

    def GetError(self):
        """
        Read the oldest error in memory.
        The error buffer is FIFO with 10 elements, so it might not be the 
        latest error if multiple errors have happened since the last time this
        function was called.
        return (None or (int, str)): the error number and message
        """
        # Note: there is another one "TE" which only returns the number, and so
        # is faster, but then there is no way to get the message
        resp = self._sendQueryCommand("TB")
        # returns something like "108, MOTOR NOT CONNECTED"
        try:
            m = re.match("(?P<no>\d+), (?P<msg>.+)", resp)
            no, msg = int(m.group("no")), m.group("msg")
        except Exception:
            raise IOError("Failed to decode error info '%s'" %
                          resp.encode('string_escape'))

        if no == 0:
            return None
        else:
            return no, msg

    def _checkError(self):
        """
        Check if an error happened and convert to a python exception
        return None
        raise NewFocusError if an error happened
        """
        err = self.GetError()
        if err:
            errno, msg = err
            raise NewFocusError(errno, msg)

    def _resynchonise(self):
        """
        Ensures the device communication is "synchronised"
        """
        self._accesser.flushInput()

        # drop all the errors
        while self.GetError():
            pass

    # high-level methods (interface)
    def _updatePosition(self, axes=None):
        """
        update the position VA
        axes (set of str): names of the axes to update or None if all should be
          updated
        """
        # uses the current values (converted to internal representation)
        pos = self._applyInversion(self.position.value)

        for n, i in self._name_to_axis.items():
            if axes is None or n in axes:
                pos[n] = self.GetPosition(i) * self._stepsize[i - 1]

        pos = self._applyInversion(pos)

        # it's read-only, so we change it via _value
        self.position._value = pos
        self.position.notify(self.position.value)

    def _speedToMS(self, axis, sps):
        """
        Convert speed in step/s to m/s
        axis (1<=int<=4): axis number
        sps (int): steps/s
        return (float): m/s
        """
        return sps * self._stepsize[axis - 1]

    def _setSpeed(self, value):
        """
        value (dict string-> float): speed for each axis
        returns (dict string-> float): the new value
        """
        if set(value.keys()) != set(self._axes.keys()):
            raise ValueError("Requested speed %s doesn't specify all axes %s" %
                             (value, self._axes.keys()))
        for axis, v in value.items():
            rng = self._axes[axis].speed
            if not rng[0] < v <= rng[1]:
                raise ValueError("Requested speed of %f for axis %s not within %f->%f" %
                                 (v, axis, rng[0], rng[1]))

            i = self._name_to_axis[axis]
            sps = max(1, int(round(v / self._stepsize[i - 1])))
            self.SetVelocity(i, sps)

        return value

    def _createFuture(self):
        """
        Return (CancellableFuture): a future that can be used to manage a move
        """
        f = CancellableFuture()
        f._moving_lock = threading.Lock() # taken while moving
        f._must_stop = threading.Event() # cancel of the current future requested
        f._was_stopped = False # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

    @isasync
    def moveRel(self, shift):
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)

        # Check if the distance is big enough to make sense
        for an, v in shift.items():
            aid = self._name_to_axis[an]
            if abs(v) < self._stepsize[aid - 1]:
                # TODO: store and accumulate all the small moves instead of dropping them?
                del shift[an]
                logging.info("Dropped too small move of %g m < %g m",
                             abs(v), self._stepsize[aid - 1])

        if not shift:
            return model.InstantaneousFuture()

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveRel, f, shift)
        return f

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveAbs, f, pos)
        return f
    moveAbs.__doc__ = model.Actuator.moveAbs.__doc__

    def stop(self, axes=None):
        self._executor.cancel()

    def _doMoveRel(self, future, pos):
        """
        Blocking and cancellable relative move
        future (Future): the future it handles
        pos (dict str -> float): axis name -> relative target position
        """
        with future._moving_lock:
            end = 0 # expected end
            moving_axes = set()
            for an, v in pos.items():
                aid = self._name_to_axis[an]
                moving_axes.add(aid)
                steps = int(round(v / self._stepsize[aid - 1]))
                self.MoveRel(aid, steps)
                # compute expected end
                dur = abs(steps) * self._stepsize[aid - 1] / self.speed.value[an]
                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        logging.debug("move successfully completed")

    def _doMoveAbs(self, future, pos):
        """
        Blocking and cancellable absolute move
        future (Future): the future it handles
        pos (dict str -> float): axis name -> absolute target position
        """
        with future._moving_lock:
            end = 0 # expected end
            old_pos = self._applyInversion(self.position.value)
            moving_axes = set()
            for an, v in pos.items():
                aid = self._name_to_axis[an]
                moving_axes.add(aid)
                steps = int(round(v / self._stepsize[aid - 1]))
                self.MoveAbs(aid, steps)
                # compute expected end
                dur = abs(v - old_pos[an]) / self.speed.value[an]
                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        logging.debug("move successfully completed")

    def _waitEndMove(self, future, axes, end=0):
        """
        Wait until all the given axes are finished moving, or a request to
        stop has been received.
        future (Future): the future it handles
        axes (set of int): the axes IDs to check
        end (float): expected end time
        raise:
            CancelledError: if cancelled before the end of the move
        """
        moving_axes = set(axes)

        last_upd = time.time()
        last_axes = moving_axes.copy()
        try:
            while not future._must_stop.is_set():
                for aid in moving_axes.copy(): # need copy to remove during iteration
                    if self.IsMotionDone(aid):
                        moving_axes.discard(aid)
                if not moving_axes:
                    # no more axes to wait for
                    break

                # Update the position from time to time (10 Hz)
                if time.time() - last_upd > 0.1 or last_axes != moving_axes:
                    last_names = set(n for n, i in self._name_to_axis.items() if i in last_axes)
                    self._updatePosition(last_names)
                    last_upd = time.time()
                    last_axes = moving_axes.copy()

                # Wait half of the time left (maximum 0.1 s)
                left = end - time.time()
                sleept = max(0.001, min(left / 2, 0.1))
                future._must_stop.wait(sleept)

                # TODO: timeout if really too long
            else:
                logging.debug("Move of axes %s cancelled before the end", axes)
                # stop all axes still moving them
                for i in moving_axes:
                    self.StopMotion(i)
                future._was_stopped = True
                raise CancelledError()
        except Exception:
            raise
        else:
            # Did everything really finished fine?
            self._checkError()
        finally:
            self._updatePosition() # update (all axes) with final position

    def _cancelCurrentMove(self, future):
        """
        Cancels the current move (both absolute or relative). Non-blocking.
        future (Future): the future to stop. Unused, only one future must be 
         running at a time.
        return (bool): True if it successfully cancelled (stopped) the move.
        """
        # The difficulty is to synchronise correctly when:
        #  * the task is just starting (not finished requesting axes to move)
        #  * the task is finishing (about to say that it finished successfully)
        logging.debug("Cancelling current move")

        future._must_stop.set() # tell the thread taking care of the move it's over
        with future._moving_lock:
            if not future._was_stopped:
                logging.debug("Cancelling failed")
            return future._was_stopped

    @classmethod
    def scan(cls):
        """
        returns (list of (str, dict)): name, kwargs
        Note: it's obviously not advised to call this function if a device is already under use
        """
        logging.info("Scanning for New Focus controllers in progress...")
        found = []  # (list of 2-tuple): name, kwargs
        try:
            conts = cls._scanOverIP()
        except IOError as exp:
            logging.exception("Failed to scan for New Focus controllers: %s", exp)

        for hn, host, port in conts:
            try:
                logging.debug("Trying controller at %s", host)
                dev = cls(None, None, address=host, axes=["a"], stepsize=[1e-6])
                modl, fw, sn = dev.GetIdentification()

                # find out about the axes
                dev.MotorCheck()
                axes = []
                stepsize = []
                for i in range(1, 5):
                    mt = dev.GetMotorType(i)
                    n = chr(ord('a') + i - 1)
                    # No idea about the stepsize, but make it different to allow
                    # distinguishing between motor types
                    if mt == MT_STANDARD:
                        ss = 1e-6
                    elif mt == MT_TINY:
                        ss = 0.1e-6
                    else:
                        n = ""
                        ss = 0
                    axes.append(n)
                    stepsize.append(ss)
            except IOError:
                # not possible to use this port? next one!
                continue
            except Exception:
                logging.exception("Error while communicating with controller %s @ %s:%s",
                                  hn, host, port)
                continue

            found.append(("NF-%s-%s" % (modl, sn),
                          {"address": host,
                           "axes": axes,
                           "stepsize": stepsize,
                           "sn": sn})
                        )

        return found

    @classmethod
    def _openConnection(cls, address, sn=None):
        """
        return (Accesser)
        """
        if address == "fake":
            host, port = "fake", 23
        elif address == "autoip":
            conts = cls._scanOverIP()
            if sn is not None:
                for hn, host, port in conts:
                    # Open connection to each controller and ask for their SN
                    dev = cls(None, None, address=host, axes=["a"], stepsize=[1e-6])
                    _, _, devsn = dev.GetIdentification()
                    if sn == devsn:
                        break
                else:
                    raise HwError("Failed to find New Focus controller %s over the "
                                  "network. Ensure it is turned on and connected to "
                                  "the network." % (sn))
            else:
                # just pick the first one
                # TODO: only pick the ones of model 8742
                try:
                    hn, host, port = conts[0]
                    logging.info("Connecting to New Focus %s", hn)
                except IndexError:
                    raise HwError("Failed to find New Focus controller over the "
                                  "network. Ensure it is turned on and connected to "
                                  "the network.")

        else:
            # split the (IP) port, separated by a :
            if ":" in address:
                host, ipport_str = port.split(":")
                port = int(ipport_str)
            else:
                host = address
                port = 23 # default

        return IPAccesser(host, port)

    @staticmethod
    def _scanOverIP():
        """
        Scan the network for all the responding new focus controllers
        Note: it actually calls a separate executable because it relies on opening
          a network port which needs special privileges.
        return (list of (str, str, int)): hostname, ip address, and port number
        """
        # Run the separate program via authbind
        try:
            exc = os.path.join(os.path.dirname(__file__), "nfpm_netscan.py")
            out = subprocess.check_output(["authbind", "python", exc])
        except CalledProcessError as exp:
            # and handle all the possible errors:
            # - no authbind (127)
            # - cannot find the separate program (2)
            # - no authorisation (13)
            ret = exp.returncode
            if ret == 127:
                raise IOError("Failed to find authbind")
            elif ret == 2:
                raise IOError("Failed to find %s" % exc)
            elif ret == 13:
                raise IOError("No permission to open network port 23")

        # or decode the output
        # hostname \t host \t port
        ret = []
        for l in out.split("\n"):
            if not l:
                continue
            try:
                hn, host, port = l.split("\t")
            except Exception:
                logging.exception("Failed to decode scanner line '%s'", l)
            ret.append((hn, host, port))

        return ret
Ejemplo n.º 32
0
class Focus(model.Actuator):
    """
    This is an extension of the model.Actuator class. It provides functions for
    moving the SEM focus (as it's considered an axis in Odemis)
    """

    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

        self.parent = parent

        axes_def = {
            # Ranges are from the documentation
            "z": model.Axis(unit="m", range=(FOCUS_RANGE[0] * 1e-3, FOCUS_RANGE[1] * 1e-3)),
        }

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Focus position polling")
        self._pos_poll.start()

    def _updatePosition(self):
        """
        update the position VA
        """
        z = self.parent.GetFocus() * 1e-3
        self.position._set_value({"z": z}, force_write=True)

    def _refreshPosition(self):
        """
        Called regularly to update the current position
        """
        # We don't use the VA setters, to avoid sending back to the hardware a
        # set request
        logging.debug("Updating SEM focus position")
        try:
            self._updatePosition()
        except Exception:
            logging.exception("Unexpected failure when updating focus position")

    def _doMoveRel(self, foc):
        """
        move by foc
        foc (float): relative change in mm
        """
        try:
            foc += self.parent.GetFocus()  # mm
            self.parent.SetFocus(foc)
        finally:
            # Update the position, even if the move didn't entirely succeed
            self._updatePosition()

    def _doMoveAbs(self, foc):
        """
        move to pos
        foc (float): unit mm
        """
        try:
            self.parent.SetFocus(foc)
        finally:
            # Update the position, even if the move didn't entirely succeed
            self._updatePosition()

    @isasync
    def moveRel(self, shift):
        """
        shift (dict): shift in m
        """
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)

        foc = shift["z"] * 1e3
        f = self._executor.submit(self._doMoveRel, foc)
        return f

    @isasync
    def moveAbs(self, pos):
        """
        pos (dict): pos in m
        """
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)

        foc = pos["z"] * 1e3
        f = self._executor.submit(self._doMoveAbs, foc)
        return f

    def stop(self, axes=None):
        """
        Stop the last command
        """
        # Empty the queue (and already stop the stage if a future is running)
        self._executor.cancel()
        logging.debug("Stopping all axes: %s", ", ".join(self.axes))

        try:
            self._updatePosition()
        except Exception:
            logging.exception("Unexpected failure when updating position")
Ejemplo n.º 33
0
class Stage(model.Actuator):
    """
    This is an extension of the model.Actuator class. It provides functions for
    moving the Zeiss stage and updating the position.
    """

    def __init__(self, name, role, parent, rng=None, **kwargs):
        """
        inverted (set of str): names of the axes which are inverted
        rng (dict str -> (float,float)): axis name -> min/max of the position on
          this axis. Note: if the axis is inverted, the range passed will be
          inverted. Also, if the hardware reports position outside of the range,
          move might fail, as it is considered outside of the range.
        """

        if rng is None:
            rng = {}

        if "x" not in rng:
            rng["x"] = (5e-3, 152e-3)
        if "y" not in rng:
            rng["y"] = (5e-3, 152e-3)
        if "z" not in rng:
            rng["z"] = (5e-3, 40e-3)

        axes_def = {
            # Ranges are from the documentation
            "x": model.Axis(unit="m", range=(rng["x"][0], rng["x"][1])),
            "y": model.Axis(unit="m", range=(rng["y"][0], rng["y"][1])),
            "z": model.Axis(unit="m", range=(rng["z"][0], rng["z"][1])),
        }

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Position polling")
        self._pos_poll.start()

    def _updatePosition(self, raw_pos=None):
        """
        update the position VA
        raw_pos (dict str -> float): the position in mm (as received from the SEM)
        """
        if raw_pos is None:
            x, y, z, _ = self.parent.GetStagePosition()
        else:
            x, y, z = raw_pos["x"], raw_pos["y"], raw_pos["z"]

        pos = {"x": x * 1e-3,
               "y": y * 1e-3,
               "z": z * 1e-3,
        }
        self.position._set_value(self._applyInversion(pos), force_write=True)

    def _refreshPosition(self):
        """
        Called regularly to update the current position
        """
        # We don't use the VA setters, to avoid sending back to the hardware a
        # set request
        logging.debug("Updating SEM stage position")
        try:
            self._updatePosition()
        except Exception:
            logging.exception("Unexpected failure when updating position")

    def _doMoveRel(self, future, shift):
        """
        move by the shift
        shift (float): unit m
        """
        x, y, z, _ = self.parent.GetStagePosition()
        if "x" in shift:
            x += shift["x"] * 1e3
        if "y" in shift:
            y += shift["y"] * 1e3
        if "z" in shift:
            z += shift["z"] * 1e3

        target_pos = self._applyInversion({"x": x * 1e-3, "y": y * 1e-3, "z": z * 1e-3})
        # Check range (for the axes we are moving)
        for an in shift.keys():
            rng = self.axes[an].range
            p = target_pos[an]
            if not rng[0] <= p <= rng[1]:
                raise ValueError("Relative move would cause axis %s out of bound (%g m)" % (an, p))

        self._moveTo(future, x, y, z)

    def _doMoveAbs(self, future, pos):
        """
        move to position pos
        pos (float): unit m
        """

        # Don't change position for unspecified coordinates
        x, y, z, _ = self.parent.GetStagePosition()
        # Convert to mm
        if "x" in pos:
            x = pos["x"] * 1e3
        if "y" in pos:
            y = pos["y"] * 1e3
        if "z" in pos:
            z = pos["z"] * 1e3

        self._moveTo(future, x, y, z)

    def _moveTo(self, future, x, y, z, timeout=60):
        with future._moving_lock:
            try:
                if future._must_stop.is_set():
                    raise CancelledError()
                logging.debug("Moving to position (%s, %s, %s)", x, y, z)
                self.parent.MoveStage(x, y, z)
                # documentation suggests to wait 1s before calling
                # GetStagePosition() after MoveStage()
                time.sleep(1)

                # Wait until the move is over
                # Don't check for future._must_stop because anyway the stage will
                # stop moving, and so it's nice to wait until we know the stage is
                # not moving.
                moving = True
                tstart = time.time()
                while moving:
                    x, y, z, moving = self.parent.GetStagePosition()
                    # Take the opportunity to update .position
                    self._updatePosition({"x": x, "y": y, "z": z})

                    if time.time() > tstart + timeout:
                        self.parent.Abort()
                        logging.error("Timeout after submitting stage move. Aborting move.")
                        break

                    # 50 ms is about the time it takes to read the stage status
                    time.sleep(50e-3)

                # If it was cancelled, Abort() has stopped the stage before, and
                # we still have waited until the stage stopped moving. Now let
                # know the user that the move is not complete.
                if future._must_stop.is_set():
                    raise CancelledError()
            except RemconError:
                if future._must_stop.is_set():
                    raise CancelledError()
                raise
            finally:
                future._was_stopped = True
                # Update the position, even if the move didn't entirely succeed
                self._updatePosition()

    def _cancelCurrentMove(self, future):
        """
        Cancels the current move (both absolute or relative). Non-blocking.
        future (Future): the future to stop. Unused, only one future must be
         running at a time.
        return (bool): True if it successfully cancelled (stopped) the move.
        """
        # The difficulty is to synchronise correctly when:
        #  * the task is just starting (not finished requesting axes to move)
        #  * the task is finishing (about to say that it finished successfully)
        logging.debug("Cancelling current move")
        future._must_stop.set()  # tell the thread taking care of the move it's over
        self.parent.Abort()

        with future._moving_lock:
            if not future._was_stopped:
                logging.debug("Cancelling failed")
            return future._was_stopped

    def _createFuture(self):
        """
        Return (CancellableFuture): a future that can be used to manage a move
        """
        f = CancellableFuture()
        f._moving_lock = threading.Lock()  # taken while moving
        f._must_stop = threading.Event()  # cancel of the current future requested
        f._was_stopped = False  # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

    @isasync
    def moveRel(self, shift):
        """
        shift (dict): shift in m
        """
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveRel, f, shift)
        return f

    @isasync
    def moveAbs(self, pos):
        """
        pos (dict): position in m
        """
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveAbs, f, pos)
        return f

    def stop(self, axes=None):
        # Empty the queue (and already stop the stage if a future is running)
        self._executor.cancel()

        # Try to stop the stage, even if no future is running, for safety
        logging.warning("Stopping all axes: %s", ", ".join(self.axes))
        self.parent.Abort()

        try:
            self._updatePosition()
        except Exception:
            logging.exception("Unexpected failure when updating position")
Ejemplo n.º 34
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()
Ejemplo n.º 35
0
class Stage(model.Actuator):
    """
    This is an extension of the model.Actuator class. It provides functions for
    moving the Tescan stage and updating the position. 
    """
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """
        axes_def = {}
        self._position = {}

        rng = [-0.5, 0.5]
        axes_def["x"] = model.Axis(unit="m", range=rng)
        axes_def["y"] = model.Axis(unit="m", range=rng)
        axes_def["z"] = model.Axis(unit="m", range=rng)

        # Demand calibrated stage
        if parent._device.StgIsCalibrated() !=1:
            logging.warning("Stage was not calibrated. We are performing calibration now.")
            parent._device.StgCalibrate()

        #Wait for stage to be stable after calibration
        while parent._device.StgIsBusy() != 0:
            # If the stage is busy (movement is in progress), current position is
            # updated approximately every 500 ms
            time.sleep(0.5)
            
        x, y, z, rot, tilt = parent._device.StgGetPosition()
        self._position["x"] = -x * 1e-3
        self._position["y"] = -y * 1e-3
        self._position["z"] = -z * 1e-3

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

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

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    self._applyInversionAbs(self._position),
                                    unit="m", readonly=True)

    def _updatePosition(self):
        """
        update the position VA
        """
        # it's read-only, so we change it via _value
        self.position._value = self._applyInversionAbs(self._position)
        self.position.notify(self.position.value)

    def _doMove(self, pos):
        """
        move to the position 
        """
        # Perform move through Tescan API
        # Position from m to mm and inverted
        self.parent._device.StgMoveTo(-pos["x"] * 1e3,
                                    - pos["y"] * 1e3,
                                    - pos["z"] * 1e3)

        # Obtain the finally reached position after move is performed.
        # This is mainly in order to keep the correct position in case the
        # move we tried to perform was greater than the maximum possible
        # one.
        with self.parent._acquisition_init_lock:
            x, y, z, rot, tilt = self.parent._device.StgGetPosition()
            self._position["x"] = -x * 1e-3
            self._position["y"] = -y * 1e-3
            self._position["z"] = -z * 1e-3

        self._updatePosition()

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)

        shift = self._applyInversionRel(shift)

        for axis, change in shift.items():
            self._position[axis] += change

        pos = self._position
        return self._executor.submit(self._doMove, pos)

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversionAbs(pos)

        for axis, new_pos in pos.items():
            self._position[axis] = new_pos

        pos = self._position
        return self._executor.submit(self._doMove, pos)

    def stop(self, axes=None):
        # Empty the queue for the given axes
        self._executor.cancel()
        logging.warning("Stopping all axes: %s", ", ".join(self.axes))

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None
Ejemplo n.º 36
0
class MultiplexActuator(model.Actuator):
    """
    An object representing an actuator made of several (real actuators)
     = a set of axes that can be moved and optionally report their position.
    """

    def __init__(self, name, role, children, axes_map, ref_on_init=None, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis
        axes_map (dict str -> str): axis name in this actuator -> axis name in the child actuator
        ref_on_init (list): axes to be referenced during initialization
        """
        if not children:
            raise ValueError("MultiplexActuator needs children")

        if set(children.keys()) != set(axes_map.keys()):
            raise ValueError("MultiplexActuator needs the same keys in children and axes_map")

        ref_on_init = ref_on_init or []
        self._axis_to_child = {} # axis name => (Actuator, axis name)
        self._position = {}
        self._speed = {}
        self._referenced = {}
        axes = {}

        for axis, child in children.items():
            caxis = axes_map[axis]
            self._axis_to_child[axis] = (child, caxis)

            # Ducktyping (useful to support also testing with MockComponent)
            # At least, it has .axes
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % (child,))
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % child.name)
            axes[axis] = copy.deepcopy(child.axes[caxis])
            self._position[axis] = child.position.value[axes_map[axis]]
            if model.hasVA(child, "speed") and caxis in child.speed.value:
                self._speed[axis] = child.speed.value[caxis]
            if model.hasVA(child, "referenced") and caxis in child.referenced.value:
                self._referenced[axis] = child.referenced.value[caxis]

        # this set ._axes and ._children
        model.Actuator.__init__(self, name, role, axes=axes,
                                children=children, **kwargs)

        if len(self.children.value) > 1:
            # will take care of executing axis move asynchronously
            self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
            # TODO: make use of the 'Cancellable' part (for now cancelling a running future doesn't work)
        else:  # Only one child => optimize by passing all requests directly
            self._executor = None

        # keep a reference to the subscribers so that they are not
        # automatically garbage collected
        self._subfun = []

        children_axes = {} # dict actuator -> set of string (our axes)
        for axis, (child, ca) in self._axis_to_child.items():
            logging.debug("adding axis %s to child %s", axis, child.name)
            if child in children_axes:
                children_axes[child].add(axis)
            else:
                children_axes[child] = set([axis])

        # position & speed: special VAs combining multiple VAs
        self.position = model.VigilantAttribute(self._position, readonly=True)
        for c, ax in children_axes.items():
            def update_position_per_child(value, ax=ax, c=c):
                logging.debug("updating position of child %s", c.name)
                for a in ax:
                    try:
                        self._position[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error("Child %s is not reporting position of axis %s", c.name, a)
                self._updatePosition()
            c.position.subscribe(update_position_per_child)
            self._subfun.append(update_position_per_child)

        # TODO: change the speed range to a dict of speed ranges
        self.speed = model.MultiSpeedVA(self._speed, [0., 10.], setter=self._setSpeed)
        for axis in self._speed.keys():
            c, ca = self._axis_to_child[axis]
            def update_speed_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._speed[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting speed of axis %s (%s): %s", cname, a, ca, value)
                self._updateSpeed()
            c.speed.subscribe(update_speed_per_child)
            self._subfun.append(update_speed_per_child)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True)

        for axis in self._referenced.keys():
            c, ca = self._axis_to_child[axis]
            def update_ref_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._referenced[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting reference of axis %s (%s)", cname, a, ca)
                self._updateReferenced()
            c.referenced.subscribe(update_ref_per_child)
            self._subfun.append(update_ref_per_child)

        self._axes_referencing = []
        for axis in ref_on_init:
            # If the axis can be referenced => do it now (and move to a known position)
            if not self._referenced.get(axis, True):
                # The initialisation will not fail if the referencing fails
                f = self.reference({axis})
                self._axes_referencing.append(axis)
                f.add_done_callback(self._on_referenced)

    def _on_referenced(self, future):
        try:
            future.result()
        except Exception as e:
            for ax in self._axes_referencing:
                c, ca = self._axis_to_child[ax]
                c.stop({ca})  # prevent any move queued
            self.state._set_value(e, force_write=True)
            logging.exception(e)

    def _updatePosition(self):
        """
        update the position VA
        """
        # it's read-only, so we change it via _value
        pos = self._applyInversion(self._position)
        logging.debug("reporting position %s", pos)
        self.position._set_value(pos, force_write=True)

    def _updateSpeed(self):
        """
        update the speed VA
        """
        # we must not call the setter, so write directly the raw value
        self.speed._value = self._speed
        self.speed.notify(self._speed)

    def _updateReferenced(self):
        """
        update the referenced VA
        """
        # .referenced is copied to detect changes to it on next update
        self.referenced._set_value(self._referenced.copy(), force_write=True)

    def _setSpeed(self, value):
        """
        value (dict string-> float): speed for each axis
        returns (dict string-> float): the new value
        """
        # FIXME the problem with this implementation is that the subscribers
        # will receive multiple notifications for each set:
        # * one for each axis (via _updateSpeed from each child)
        # * the actual one (but it's probably dropped as it's the same value)
        final_value = value.copy()  # copy
        for axis, v in value.items():
            child, ma = self._axis_to_child[axis]
            new_speed = child.speed.value.copy()  # copy
            new_speed[ma] = v
            child.speed.value = new_speed
            final_value[axis] = child.speed.value[ma]
        return final_value

    def _moveToChildMove(self, mv):
        child_to_move = collections.defaultdict(dict)  # child -> moveRel argument
        for axis, distance in mv.items():
            child, child_axis = self._axis_to_child[axis]
            child_to_move[child].update({child_axis: distance})
            logging.debug("Moving axis %s (-> %s) by %g", axis, child_axis, distance)

        return child_to_move

    def _axesToChildAxes(self, axes):
        child_to_axes = collections.defaultdict(set)  # child -> set(str): axes
        for axis in axes:
            child, child_axis = self._axis_to_child[axis]
            child_to_axes[child].add(child_axis)
            logging.debug("Interpreting axis %s (-> %s)", axis, child_to_axes)

        return child_to_axes

    @isasync
    def moveRel(self, shift, **kwargs):
        """
        Move the stage the defined values in m for each axis given.
        shift dict(string-> float): name of the axis and shift in m
        **kwargs: Mostly there to support "update" argument (but currently works
          only if there is only one child)
        """
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)

        if self._executor:
            f = self._executor.submit(self._doMoveRel, shift, **kwargs)
        else:
            cmv = self._moveToChildMove(shift)
            child, move = cmv.popitem()
            assert not cmv
            f = child.moveRel(move, **kwargs)

        return f

    def _doMoveRel(self, shift, **kwargs):
        # TODO: updates don't work because we still wait for the end of the
        # move before we get to the next one => multi-threaded queue? Still need
        # to ensure the order (ie, X>AB>X can be executed as X/AB>X or X>AB/X but
        # XA>AB>X must be in the order XA>AB/X
        futures = []
        for child, move in self._moveToChildMove(shift).items():
            f = child.moveRel(move, **kwargs)
            futures.append(f)

        # just wait for all futures to finish
        for f in futures:
            f.result()

    @isasync
    def moveAbs(self, pos, **kwargs):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        if self._executor:
            f = self._executor.submit(self._doMoveAbs, pos, **kwargs)
        else:
            cmv = self._moveToChildMove(pos)
            child, move = cmv.popitem()
            assert not cmv
            f = child.moveAbs(move, **kwargs)

        return f

    def _doMoveAbs(self, pos, **kwargs):
        futures = []
        for child, move in self._moveToChildMove(pos).items():
            f = child.moveAbs(move, **kwargs)
            futures.append(f)

        # just wait for all futures to finish
        for f in futures:
            f.result()

    @isasync
    def reference(self, axes):
        if not axes:
            return model.InstantaneousFuture()
        self._checkReference(axes)
        if self._executor:
            f = self._executor.submit(self._doReference, axes)
        else:
            cmv = self._axesToChildAxes(axes)
            child, a = cmv.popitem()
            assert not cmv
            f = child.reference(a)

        return f
    reference.__doc__ = model.Actuator.reference.__doc__

    def _doReference(self, axes):
        child_to_axes = self._axesToChildAxes(axes)
        futures = []
        for child, a in child_to_axes.items():
            f = child.reference(a)
            futures.append(f)

        # just wait for all futures to finish
        for f in futures:
            f.result()

    def stop(self, axes=None):
        """
        stops the motion
        axes (iterable or None): list of axes to stop, or None if all should be stopped
        """
        # Empty the queue for the given axes
        if self._executor:
            self._executor.cancel()

        all_axes = set(self.axes.keys())
        axes = axes or all_axes
        unknown_axes = axes - all_axes
        if unknown_axes:
            logging.error("Attempting to stop unknown axes: %s", ", ".join(unknown_axes))
            axes &= all_axes

        threads = []
        for child, a in self._axesToChildAxes(axes).items():
            # it's synchronous, but we want to stop all of them as soon as possible
            thread = threading.Thread(name="Stopping axis", target=child.stop, args=(a,))
            thread.start()
            threads.append(thread)

        # wait for completion
        for thread in threads:
            thread.join(1)
            if thread.is_alive():
                logging.warning("Stopping child actuator of '%s' is taking more than 1s", self.name)

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None
Ejemplo n.º 37
0
    def __init__(self, name, role, children, axes_map, ref_on_init=None, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis
        axes_map (dict str -> str): axis name in this actuator -> axis name in the child actuator
        ref_on_init (list): axes to be referenced during initialization
        """
        if not children:
            raise ValueError("MultiplexActuator needs children")

        if set(children.keys()) != set(axes_map.keys()):
            raise ValueError("MultiplexActuator needs the same keys in children and axes_map")

        ref_on_init = ref_on_init or []
        self._axis_to_child = {} # axis name => (Actuator, axis name)
        self._position = {}
        self._speed = {}
        self._referenced = {}
        axes = {}

        for axis, child in children.items():
            caxis = axes_map[axis]
            self._axis_to_child[axis] = (child, caxis)

            # Ducktyping (useful to support also testing with MockComponent)
            # At least, it has .axes
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % (child,))
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % child.name)
            axes[axis] = copy.deepcopy(child.axes[caxis])
            self._position[axis] = child.position.value[axes_map[axis]]
            if model.hasVA(child, "speed") and caxis in child.speed.value:
                self._speed[axis] = child.speed.value[caxis]
            if model.hasVA(child, "referenced") and caxis in child.referenced.value:
                self._referenced[axis] = child.referenced.value[caxis]

        # this set ._axes and ._children
        model.Actuator.__init__(self, name, role, axes=axes,
                                children=children, **kwargs)

        if len(self.children.value) > 1:
            # will take care of executing axis move asynchronously
            self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time
            # TODO: make use of the 'Cancellable' part (for now cancelling a running future doesn't work)
        else:  # Only one child => optimize by passing all requests directly
            self._executor = None

        # keep a reference to the subscribers so that they are not
        # automatically garbage collected
        self._subfun = []

        children_axes = {} # dict actuator -> set of string (our axes)
        for axis, (child, ca) in self._axis_to_child.items():
            logging.debug("adding axis %s to child %s", axis, child.name)
            if child in children_axes:
                children_axes[child].add(axis)
            else:
                children_axes[child] = set([axis])

        # position & speed: special VAs combining multiple VAs
        self.position = model.VigilantAttribute(self._position, readonly=True)
        for c, ax in children_axes.items():
            def update_position_per_child(value, ax=ax, c=c):
                logging.debug("updating position of child %s", c.name)
                for a in ax:
                    try:
                        self._position[a] = value[axes_map[a]]
                    except KeyError:
                        logging.error("Child %s is not reporting position of axis %s", c.name, a)
                self._updatePosition()
            c.position.subscribe(update_position_per_child)
            self._subfun.append(update_position_per_child)

        # TODO: change the speed range to a dict of speed ranges
        self.speed = model.MultiSpeedVA(self._speed, [0., 10.], setter=self._setSpeed)
        for axis in self._speed.keys():
            c, ca = self._axis_to_child[axis]
            def update_speed_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._speed[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting speed of axis %s (%s): %s", cname, a, ca, value)
                self._updateSpeed()
            c.speed.subscribe(update_speed_per_child)
            self._subfun.append(update_speed_per_child)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True)

        for axis in self._referenced.keys():
            c, ca = self._axis_to_child[axis]
            def update_ref_per_child(value, a=axis, ca=ca, cname=c.name):
                try:
                    self._referenced[a] = value[ca]
                except KeyError:
                    logging.error("Child %s is not reporting reference of axis %s (%s)", cname, a, ca)
                self._updateReferenced()
            c.referenced.subscribe(update_ref_per_child)
            self._subfun.append(update_ref_per_child)

        self._axes_referencing = []
        for axis in ref_on_init:
            # If the axis can be referenced => do it now (and move to a known position)
            if not self._referenced.get(axis, True):
                # The initialisation will not fail if the referencing fails
                f = self.reference({axis})
                self._axes_referencing.append(axis)
                f.add_done_callback(self._on_referenced)
Ejemplo n.º 38
0
class PMTControl(model.PowerSupplier):
    '''
    This represents the PMT control unit.
    At start up the following is set:
     * protection is on (=> gain is forced to 0)
     * gain = 0
     * power up
    '''
    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()

    def terminate(self):
        if self._executor:
            self._executor.cancel()
            self._executor.shutdown()
            self._executor = None
        with self._ser_access:
            if self._serial:
                self._serial.close()
                self._serial = None

    @isasync
    def supply(self, sup):
        if not sup:
            return model.InstantaneousFuture()
        self._checkSupply(sup)

        return self._executor.submit(self._doSupply, sup)

    def _doSupply(self, sup):
        """
        supply power
        """
        value = list(sup.values())[0]  # only care about the value
        self.setRelay(value)
        self._updateSupplied()

    def _updateSupplied(self):
        """
        update the supplied VA
        """
        # update all components since they are all connected to the same switch
        value = self.getRelay()
        for comp in self.powered:
            self._supplied[comp] = value

        # it's read-only, so we change it via _value
        self.supplied._value = self._supplied
        self.supplied.notify(self.supplied.value)

    def _getIdentification(self):
        return self._sendCommand(b"*IDN?").decode('latin1')

    def _setGain(self, value):
        self._sendCommand(b"VOLT %f" % (value, ))

        return self._getGain()

    def _setProtectionCurrent(self, value):
        self._sendCommand(b"PCURR %f" % (value * 1e6, ))  # in µA

    def _setProtectionTime(self, value):
        self._sendCommand(b"PTIME %f" % (value, ))

    def _getGain(self):
        ans = self._sendCommand(b"VOLT?")
        try:
            value = float(ans)
        except ValueError:
            raise IOError("Gain value cannot be converted to float.")

        return value

    def _setPowerSupply(self, value):
        if value:
            self._sendCommand(b"PWR 1")
        else:
            self._sendCommand(b"PWR 0")

        return value

    def _getPowerSupply(self):
        ans = self._sendCommand(b"PWR?")
        return ans == b"1"

    def _setProtection(self, value):
        if value:
            self._sendCommand(b"SWITCH 0")
        else:
            self._sendCommand(b"SWITCH 1")

        return value

    def _getProtection(self):
        ans = self._sendCommand(b"SWITCH?")
        return ans == b"0"

    # These two methods are strictly used for the SPARC system in Monash. Use
    # them to send a high/low signal via the PMT Control Unit to the relay, thus
    # to pull/push the relay contact and control the power supply from the power
    # board to the flippers and filter wheel.
    def setRelay(self, value):
        # When True, the relay contact is connected
        if value:
            self._sendCommand(b"RELAY 1")
        else:
            self._sendCommand(b"RELAY 0")

        return value

    def getRelay(self):
        ans = self._sendCommand(b"RELAY?")
        if ans == b"1":
            status = True
        else:
            status = False

        return status

    def _sendCommand(self, cmd):
        """
        cmd (byte str): command to be sent to PMT Control unit.
        returns (byte str): answer received from the PMT Control unit
        raises:
            IOError: if an ERROR is returned by the PMT Control firmware.
        """
        cmd = cmd + b"\n"
        with self._ser_access:
            logging.debug("Sending command %s", to_str_escape(cmd))
            self._serial.write(cmd)

            ans = b''
            char = None
            while char != b'\n':
                char = self._serial.read()
                if not char:
                    logging.error("Timeout after receiving %s",
                                  to_str_escape(ans))
                    # TODO: See how you should handle a timeout before you raise
                    # an HWError
                    raise HwError(
                        "PMT Control Unit connection timeout. "
                        "Please turn off and on the power to the box.")
                # Handle ERROR coming from PMT control unit firmware
                ans += char

            logging.debug("Received answer %s", to_str_escape(ans))
            if ans.startswith(b"ERROR"):
                raise PMTControlError(ans.split(b' ', 1)[1])

            return ans.rstrip()

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for a PMT control device.
        port (string): the name of the serial port (e.g., /dev/ttyACM0)
        return (serial): the opened serial port
        """
        ser = serial.Serial(
            port=port,
            timeout=1  # s
        )

        # Purge
        ser.flush()
        ser.flushInput()

        # Try to read until timeout to be extra safe that we properly flushed
        while True:
            char = ser.read()
            if char == b'':
                break
        logging.debug(
            "Nothing left to read, PMT Control Unit can safely initialize.")

        ser.timeout = 5  # Sometimes the software-based USB can have some hiccups
        return ser

    def _findDevice(self, ports):
        """
        Look for a compatible device
        ports (str): pattern for the port name
        return (str): the name of the port used
        It also sets ._serial and ._idn to contain the opened serial port, and
        the identification string.
        raises:
            IOError: if no device are found
        """
        # For debugging purpose
        if ports == "/dev/fake":
            self._serial = PMTControlSimulator(timeout=1)
            return ports

        if os.name == "nt":
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(ports)

        for n in names:
            try:
                self._serial = self._openSerialPort(n)
                # If the device has just been inserted, odemis-relay will block
                # it for 10s while reseting the relay, so be patient
                try:
                    fcntl.flock(self._serial.fileno(),
                                fcntl.LOCK_EX | fcntl.LOCK_NB)
                except IOError:
                    logging.info("Port %s is busy, will wait and retry", n)
                    time.sleep(11)
                    fcntl.flock(self._serial.fileno(),
                                fcntl.LOCK_EX | fcntl.LOCK_NB)

                try:
                    idn = self._getIdentification()
                except PMTControlError:
                    # Can happen if the device has received some weird characters
                    # => try again (now that it's flushed)
                    logging.info("Device answered by an error, will try again")
                    idn = self._getIdentification()
                # Check that we connect to the right device
                if not idn.startswith("Delmic Analog PMT"):
                    logging.info("Connected to wrong device on %s, skipping.",
                                 n)
                    continue
                return n
            except (IOError, PMTControlError):
                # not possible to use this port? next one!
                continue
        else:
            raise HwError(
                "Failed to find a PMT Control device on ports '%s'. "
                "Check that the device is turned on and connected to "
                "the computer." % (ports, ))

    @classmethod
    def scan(cls):
        """
        returns (list of 2-tuple): name, args (sn)
        Note: it's obviously not advised to call this function if a device is already under use
        """
        logging.info(
            "Serial ports scanning for PMT control device in progress...")
        found = []  # (list of 2-tuple): name, kwargs

        if sys.platform.startswith('linux'):
            # Look for each ACM device, if the IDN is the expected one
            acm_paths = glob.glob('/dev/ttyACM?')
            for port in acm_paths:
                # open and try to communicate
                try:
                    dev = cls(name="test", role="test", port=port)
                    idn = dev._getIdentification()
                    if idn.startswith("Delmic Analog PMT"):
                        found.append({"port": port})
                except Exception:
                    pass
        else:
            # TODO: Windows version
            raise NotImplementedError("OS not yet supported")

        return found
Ejemplo n.º 39
0
class SpectraPro(model.Actuator):
    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()

    def _readCalibVal(self, rawv):
        """
        rawv (str or number)
        return (float)
        """
        if isinstance(rawv, basestring):
            if rawv.startswith("hex:"):
                rawv = rawv[4:]
            return hextof(rawv)
        elif isinstance(rawv, numbers.Real):
            return rawv
        else:
            raise ValueError("Cannot convert %s to a number" % (rawv,))

    # Low-level methods: to access the hardware (should be called with the lock acquired)

    def _sendOrder(self, *args, **kwargs):
        """
        Send a command which does not expect any report back (just OK)
        com (str): command to send (non including the \r)
        raise
            SPError: if the command doesn't answer the expected OK.
            IOError: in case of timeout
        """
        # same as a query but nothing to do with the response
        self._sendQuery(*args, **kwargs)

    def _sendQuery(self, com, timeout=1):
        """
        Send a command which expects a report back (in addition to the OK)
        com (str): command to send (non including the \r)
        timeout (0<float): maximum read timeout for the response
        return (str): the response received (without the ok)
        raises:
            SPError: if the command doesn't answer the expected OK.
            IOError: in case of timeout
        """
        # All commands or strings of commands must be terminated with a carriage
        # return (0D hex). The monochromator responds to a command when the
        # command has been completed by returning the characters " ok" followed by
        # carriage return and line feed (hex ASCII sequence 20 6F 6B 0D 0A).
        # Examples of error answers:
        # MODEL\r
        # \x00X\xf0~\x00X\xf0~MODEL ? \r\n
        # ?\r
        # \r\nAddress Error \r\nA=3F4F4445 PC=81444

        assert(len(com) > 1 and len(com) <= 100) # commands cannot be long
        com += "\r"

        logging.debug("Sending: %s", com.encode('string_escape'))
        # send command until it succeeds
        while True:
            try:
                self._serial.write(com)
                break
            except IOError:
                if self._try_recover:
                    self._tryRecover()
                else:
                    raise

        # read response until timeout or known end of response
        response = ""
        timeend = time.time() + timeout
        while ((time.time() <= timeend) and
               not (response.endswith(" ok\r\n") or response.endswith("? \r\n"))):
            self._serial.timeout = max(0.1, timeend - time.time())
            char = self._serial.read()
            if not char: # timeout
                break
            response += char

        logging.debug("Received: %s", response.encode('string_escape'))
        if response.endswith(" ok\r\n"):
            return response[:-5]
        else:
            # if the device hasn't answered anything, it might have been disconnected
            if len(response) == 0:
                if self._try_recover:
                    self._tryRecover()
                else:
                    raise IOError("Device timeout after receiving '%s'." % response.encode('string_escape'))
            else: # just non understood command
                # empty the serial port
                self._serial.timeout = 0.1
                garbage = self._serial.read(100)
                if len(garbage) == 100:
                    raise IOError("Device keeps sending data")
                response += garbage
                raise SPError("Sent '%s' and received error: '%s'" %
                              (com.encode('string_escape'), response.encode('string_escape')))

    def _tryRecover(self):
        # no other access to the serial port should be done
        # so _ser_access should already be acquired

        # Retry to open the serial port (in case it was unplugged)
        while True:
            try:
                self._serial.close()
                self._serial = None
            except:
                pass
            try:
                logging.debug("retrying to open port %s", self._port)
                self._serial = self.openSerialPort(self._port)
            except IOError:
                time.sleep(2)
            except Exception:
                logging.exception("Unexpected error while trying to recover device")
                raise
            else:
                break

        self._try_recover = False # to avoid recursion
        self._initDevice()
        self._try_recover = True

    def _initDevice(self):
        # If no echo is desired, the command NO-ECHO will suppress the echo. The
        # command ECHO will return the SP-2150i to the default echo state.
        #
        # If is connected via the real serial port (not USB), it is in echo
        # mode, so we first need to disable it, while allowing echo of the
        # command we've just sent.

        try:
            r = self._sendOrder("no-echo")
        except SPError:
            logging.info("Failed to disable echo, hopping the device has not echo anyway")

        # empty the serial port
        self._serial.timeout = 0.1
        garbage = self._serial.read(100)
        if len(garbage) == 100:
            raise IOError("Device keeps sending data")

    def GetTurret(self):
        """
        returns (1 <= int <= 3): the current turret number
        """
        # ?TURRET Returns the correctly installed turret numbered 1 - 3
        res = self._sendQuery("?turret")
        val = int(res)
        if val < 1 or val > 3:
            raise SPError("Unexpected turret number '%s'" % res)
        return val

    def SetTurret(self, t):
        """
        Set the number of the current turret (for correct settings by the hardware)
        t (1 <= int <= 3): the turret number
        Raise:
            ValueError if the turret has no grating configured
        """
        # TURRET  Specifies the presently installed turret or the turret to be installed.
        # Doesn't change the hardware, just which gratings are available

        assert(1 <= t and t <= 3)
        # TODO check that there is grating configured for this turret (using GetGratingChoices)
        self._sendOrder("%d turret" % t)

    # regex to read the gratings
    RE_NOTINSTALLED = re.compile("\D*(\d+)\s+Not Installed")
    RE_INSTALLED = re.compile("\D*(\d+)\s+(\d+)\s*g/mm BLZ=\s*([0-9][.0-9]*)\s*(nm|NM|um|UM)")
    RE_GRATING = re.compile("\D*(\d+)\s+(.+\S)\s*\r")
    def GetGratingChoices(self):
        """
        return (dict int -> string): grating number to description
        """
        # ?GRATINGS Returns the list of installed gratings with position groove density and blaze. The
        #  present grating is specified with an arrow.
        # Example output:
        #  \r\n 1  300 g/mm BLZ=  500NM \r\n\x1a2  300 g/mm BLZ=  750NM \r\n 3  Not Installed     \r\n 4  Not Installed     \r\n 5  Not Installed     \r\n 6  Not Installed     \r\n 7  Not Installed     \r\n 8  Not Installed     \r\n ok\r\n
        #  \r\n\x1a1  600 g/mm BLZ=  1.6UM \r\n 2  150 g/mm BLZ=    2UM \r\n 3  Not Installed     \r\n 4  Not Installed     \r\n 5  Not Installed     \r\n 6  Not Installed     \r\n 7  Not Installed     \r\n 8  Not Installed     \r\n 9  Not Installed     \r\n ok\r\n

        # From the spectrapro_300i_ll.c of fsc2, it seems the format is:
        # non-digit*,digits=grating number,spaces,"Not Installed"\r\n
        # non-digit*,digits=grating number,space+,digit+:g/mm,space*,"g/mm BLZ=", space*,digit+:blaze wl in nm,space*,"nm"\r\n

        res = self._sendQuery("?gratings")
        gratings = {}
        for line in res[:-1].split("\n"): # avoid the last \n to not make an empty last line
            m = self.RE_NOTINSTALLED.search(line)
            if m:
                logging.debug("Decoded grating %s as not installed, skipping.", m.group(1))
                continue
            m = self.RE_GRATING.search(line)
            if not m:
                logging.debug("Failed to decode grating description '%s'", line)
                continue
            num = int(m.group(1))
            desc = m.group(2)
            # TODO: provide a nicer description, using RE_INSTALLED?
            gratings[num] = desc

        return gratings

    RE_GDENSITY = re.compile("(\d+)\s*g/mm")
    def _getGrooveDensity(self, gid):
        """
        Returns the groove density of the given grating
        gid (int): index of the grating
        returns (float): groove density in lines/meter
        raise
           LookupError if the grating is not installed
           ValueError: if the groove density cannot be found out
        """
        gstring = self.axes["grating"].choices[gid]
        m = self.RE_GDENSITY.search(gstring)
        if not m:
            raise ValueError("Failed to find groove density in '%s'" % gstring)
        density = float(m.group(1)) * 1e3 # l/m
        return density

    RE_BLZ = re.compile("BLZ=\s+(?P<blz>[0-9.]+)\s*(?P<unit>[NU]M)")
    def _getBlaze(self, gid):
        """
        Returns the blaze (=optimal center wavelength) of the given grating
        gid (int): index of the grating
        returns (float): blaze (in m)
        raise
           LookupError if the grating is not installed
           ValueError: if the groove density cannot be found out
        """
        gstring = self.axes["grating"].choices[gid]
        m = self.RE_BLZ.search(gstring)
        if not m:
            raise ValueError("Failed to find blaze in '%s'" % gstring)
        blaze, unit = float(m.group("blz")), m.group("unit").upper()
        blaze *= {"UM": 1e-6, "NM": 1e-9}[unit] # m
        return blaze

    def GetGrating(self):
        """
        Retuns the current grating in use
        returns (1<=int<=9) the grating in use
        """
        # ?GRATING Returns the number of gratings presently being used numbered 1 - 9.
        # On the SP-2150i, it's only up to 6

        res = self._sendQuery("?grating")
        val = int(res)
        if not 1 <= val <= 9:
            raise SPError("Unexpected grating number '%s'" % res)
        return val

    def SetGrating(self, g):
        """
        Change the current grating (the turret turns).
        g (1<=int<=9): the grating number to change to
        The method is synchronous, it returns once the grating is selected. It
          might take up to 20 s.
        Note: the grating is dependent on turret number (and the self.max_gratting)!
        Note: after changing the grating, the wavelength, might have changed
        """
        # GRATING Places specified grating in position to the [current] wavelength
        # Note: it always reports ok, and doesn't change the grating if not
        # installed or wrong value

        assert(1 <= g <= (3 * self.max_gratings))
        # TODO check that the grating is configured

        self._sendOrder("%d grating" % g, timeout=20)

    def GetWavelength(self):
        """
        Return (0<=float): the current wavelength at the center (in m)
        """
        # ?NM Returns present wavelength in nm to 0.01nm resolution with units
        #  nm appended.
        # Note: For the SP-2150i, it seems there is no unit appended
        # ?NM 300.00 nm

        res = self._sendQuery("?nm")
        m = re.search("\s*(\d+.\d+)( nm)?", res)
        wl = float(m.group(1)) * 1e-9
        if wl > 1e-3:
            raise SPError("Unexpected wavelength of '%s'" % res)
        return wl

    def SetWavelength(self, wl):
        """
        Change the wavelength at the center
        wl (0<=float<=10e-6): wavelength in meter
        returns when the move is complete
        The method is synchronous, it returns once the grating is selected. It
          might take up to 20 s.
        """
        # GOTO: Goes to a destination wavelength at maximum motor speed. Accepts
        #  destination wavelength in nm as a floating point number with up to 3
        #  digits after the decimal point or whole number wavelength with no
        #  decimal point.
        # 345.65 GOTO
        # Note: NM goes to the wavelength slowly (in order to perform a scan).
        #  It shouldn't be needed for spectrometer
        # Out of bound values are silently ignored by going to the min or max.

        assert(0 <= wl <= 10e-6)
        # TODO: check that the value fit the grating configuration?
        self._sendOrder("%.3f goto" % (wl * 1e9), timeout=20)

    def GetModel(self):
        """
        Return (str): the model name
        """
        # MODEL Returns model number of the Acton SP series monochromator.
        # returns something like ' SP-2-150i '
        res = self._sendQuery("model")
        return res.strip()

    def GetSerialNumber(self):
        """
        Return the serial number or None if it cannot be determined
        """
        try:
            res = self._sendQuery("serial")
        except SPError:
            logging.exception("Device doesn't support serial number query")
            return None
        return res.strip()

    # TODO diverter (mirror) functions: no diverter on SP-2??0i anyway.

    def _getCalibratedWavelength(self):
        """
        Read the center wavelength, and adapt it based on the calibration (if
         it is available for the current grating)
        return (float): wavelength in m
        """
        gid = self.GetGrating()
        rawwl = self.GetWavelength()
        # Do we already now the answer?
        if (gid, rawwl) == self._wl[0:2]:
            return self._wl[2]

        ca, sa, fl, ia, da = self._calib[gid]

        # It's pretty hard to reverse the formula, so we approximate a8 using
        # rawwl (instead of wl), which usually doesn't bring error > 0.01 nm
        gl = self._getGrooveDensity(gid)
        psz = self._ccd.pixelSize.value[0] # m/px
        a8 = (rawwl * gl / 2) / math.cos(ia / 2)
        ga = math.asin(a8) # rad
        dispersion = math.cos(ia / 2 + ga) / (gl * fl) # m/m
        pixbw = psz * dispersion
        wl = (rawwl - ca * pixbw) / (sa + 1)
        wl = max(0, wl)
        return wl

    def _setCalibratedWavelength(self, wl):
        """
        wl (float): center wavelength in m
        """
        gid = self.GetGrating()
        ca, sa, fl, ia, da = self._calib[gid]

        # This is approximately what Winspec does, but it seems not exactly,
        # because the values differ ± 0.1nm
        gl = self._getGrooveDensity(gid)
        psz = self._ccd.pixelSize.value[0] # m/px
        a8 = (wl * gl / 2) / math.cos(ia / 2)
        ga = math.asin(a8) # rad
        dispersion = math.cos(ia / 2 + ga) / (gl * fl) # m/m
        pixbw = psz * dispersion
        offset = ca * pixbw + sa * wl
        if abs(offset) > 50e-9:
            # we normally don't expect offset more than 10 nm
            logging.warning("Center wavelength offset computed of %g nm", offset * 1e9)
        else:
            logging.debug("Center wavelength offset computed of %g nm", offset * 1e9)
        rawwl = max(0, wl + offset)
        self.SetWavelength(rawwl)

        # store the corresponding official wl value as it's hard to inverse the
        # conversion (for displaying in .position)
        self._wl = (gid, self.GetWavelength(), wl)

    # high-level methods (interface)
    def _updatePosition(self):
        """
        update the position VA
        Note: it should not be called while holding _ser_access
        """
        with self._ser_access:
            pos = {"wavelength": self._getCalibratedWavelength(),
                   "grating": self.GetGrating()
                  }

        # it's read-only, so we change it via _value
        self.position._value = pos
        self.position.notify(self.position.value)

    @isasync
    def moveRel(self, shift):
        """
        Move the stage the defined values in m for each axis given.
        shift dict(string-> float): name of the axis and shift in m
        returns (Future): future that control the asynchronous move
        """
        self._checkMoveRel(shift)

        for axis in shift:
            if axis == "wavelength":
                # cannot convert it directly to an absolute move, because
                # several in a row must mean they accumulate. So we queue a
                # special task. That also means the range check is delayed until
                # the actual position is known.
                return self._executor.submit(self._doSetWavelengthRel, shift[axis])

    @isasync
    def moveAbs(self, pos):
        """
        Move the stage the defined values in m for each axis given.
        pos dict(string-> float): name of the axis and new position in m
        returns (Future): future that control the asynchronous move
        """
        self._checkMoveAbs(pos)

        # If grating needs to be changed, change it first, then the wavelength
        if "grating" in pos:
            g = pos["grating"]
            wl = pos.get("wavelength")
            return self._executor.submit(self._doSetGrating, g, wl)
        elif "wavelength" in pos:
            wl = pos["wavelength"]
            return self._executor.submit(self._doSetWavelengthAbs, wl)
        else: # nothing to do
            return model.InstantaneousFuture()

    def _doSetWavelengthRel(self, shift):
        """
        Change the wavelength by a value
        """
        with self._ser_access:
            pos = self.position.value["wavelength"] + shift
            # it's only now that we can check the absolute position is wrong
            minp, maxp = self.axes["wavelength"].range
            if not minp <= pos <= maxp:
                raise ValueError("Position %f of axis '%s' not within range %f→%f" %
                                 (pos, "wavelength", minp, maxp))
            self._setCalibratedWavelength(pos)
        self._updatePosition()

    def _doSetWavelengthAbs(self, pos):
        """
        Change the wavelength to a value
        """
        with self._ser_access:
            self._setCalibratedWavelength(pos)
        self._updatePosition()

    def _doSetGrating(self, g, wl=None):
        """
        Setter for the grating VA.
        g (1<=int<=3): the new grating
        wl (None or float): wavelength to set afterwards. If None, will put the
          same wavelength as before the change of grating.
        returns the actual new grating
        Warning: synchronous until the grating is finished (up to 20s)
        """
        try:
            with self._ser_access:
                if wl is None:
                    wl = self.position.value["wavelength"]
                self.SetGrating(g)
                self._setCalibratedWavelength(wl)
        except Exception:
            logging.exception("Failed to change grating to %d", g)
            raise

        self._updatePosition()

    def stop(self, axes=None):
        """
        stops the motion
        Warning: Only not yet-executed moves can be cancelled, this hardware
          doesn't support stopping while a move is going on.
        """
        self._executor.cancel()

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

        if self._serial:
            self._serial.close()
            self._serial = None

    def getPixelToWavelength(self, npixels, pxs):
        """
        Return the lookup table pixel number of the CCD -> wavelength observed.
        npixels (1 <= int): number of pixels on the CCD (horizontally), after
          binning.
        pxs (0 < float): pixel size in m (after binning)
        return (list of floats): pixel number -> wavelength in m
        """
        centerpixel = (npixels - 1) / 2
        cw = self.position.value["wavelength"] # m
        gid = self.position.value["grating"]
        gl = self._getGrooveDensity(gid)
        ca, sa, fl, ia, da = self._calib[gid]

        # Formula based on the Winspec documentation:
        # "Equations used in WinSpec Wavelength Calibration", p. 257 of the manual
        # ftp://ftp.piacton.com/Public/Manuals/Princeton%20Instruments/WinSpec%202.6%20Spectroscopy%20Software%20User%20Manual.pdf
        # Converted to code by Benjamin Brenny (from AMOLF)
        G = math.asin(cw / (math.cos(ia / 2) * 2 / gl))

        wllist = []
        for i in range(npixels):
            pxd = pxs * (i - centerpixel)  # distance of pixel to sensor centre
            E = math.atan((pxd * math.cos(da)) / (fl + pxd * math.sin(da)))
            wl = (math.sin(G - ia / 2) + math.sin(G + ia / 2 + E)) / gl
            wllist.append(wl)

        return wllist

#     def getPolyToWavelength(self):
#         """
#         Compute the right polynomial to convert from a position on the sensor to the
#           wavelength detected. It depends on the current grating, center
#           wavelength (and focal length of the spectrometer).
#         Note: It will always return some not-too-stupid values, but the only way
#           to get precise values is to have provided a calibration data file.
#           Without it, it will just base the calculations on the theoretical
#           perfect spectrometer.
#         returns (list of float): polynomial coefficients to apply to get the current
#           wavelength corresponding to a given distance from the center:
#           w = p[0] + p[1] * x + p[2] * x²...
#           where w is the wavelength (in m), x is the position from the center
#           (in m, negative are to the left), and p is the polynomial (in m, m^0, m^-1...).
#         """
#         # FIXME: shall we report the error on the polynomial? At least say if it's
#         # using calibration or not.
#         # TODO: have a calibration procedure, a file format, and load it at init
#         # See fsc2, their calibration is like this for each grating:
#         # INCLUSION_ANGLE_1  =   30.3
#         # FOCAL_LENGTH_1     =   301.2 mm
#         # DETECTOR_ANGLE_1   =   0.324871
#         # TODO: use detector angle
#         fl = self._focal_length # m
#         ia = self._inclusion_angle # rad
#         cw = self.position.value["wavelength"] # m
#         if not fl:
#             # "very very bad" calibration
#             return [cw]
#
#         # When no calibration available, fallback to theoretical computation
#         # based on http://www.roperscientific.de/gratingcalcmaster.html
#         gl = self._getGrooveDensity(self.position.value["grating"]) # g/m
#         # fL = focal length (mm)
#         # wE = inclusion angle (°) = the angle between the incident and the reflected beam for the center wavelength of the grating
#         # gL = grating lines (l/mm)
#         # cW = center wavelength (nm)
#         #   Grating angle
#         # A8 = (cW/1000*gL/2000)/Math.cos(wE* Math.PI/180);
#         # E8 = Math.asin(A8)*180/Math.PI;
#         try:
#             a8 = (cw * gl/2) / math.cos(ia)
#             ga = math.asin(a8) # radians
#         except (ValueError, ZeroDivisionError):
#             logging.exception("Failed to compute polynomial for wavelength conversion")
#             return [cw]
#         # if (document.forms[0].E8.value == "NaN deg." || E8 > 40){document.forms[0].E8.value = "> 40 deg."; document.forms[0].E8.style.colour="red";
#         if 0.5 > math.degrees(ga) or math.degrees(ga) > 40:
#             logging.warning("Failed to compute polynomial for wavelength "
#                             "conversion, got grating angle = %g°", math.degrees(ga))
#             return [cw]
#
#         # dispersion: wavelength(m)/distance(m)
#         # F8a = Math.cos(Math.PI/180*(wE*1 + E8))*(1000000)/(gL*fL); // nm/mm
#         # to convert from nm/mm -> m/m : *1e-6
#         dispersion = math.cos(ia + ga) / (gl*fl) # m/m
#         if 0 > dispersion or dispersion > 0.5e-3: # < 500 nm/mm
#             logging.warning("Computed dispersion is not within expected bounds: %f nm/mm",
#                             dispersion * 1e6)
#             return [cw]
#
#         # polynomial is cw + dispersion * x
#         return [cw, dispersion]

    def selfTest(self):
        """
        check as much as possible that it works without actually moving the motor
        return (boolean): False if it detects any problem
        """
        try:
            with self._ser_access:
                modl = self.GetModel()
                if not modl.startswith("SP-"):
                    # accept it anyway
                    logging.warning("Device reports unexpected model '%s'", modl)

                turret = self.GetTurret()
                if turret not in (1, 2, 3):
                    return False
                return True
        except Exception:
            logging.exception("Selftest failed")

        return False

    @staticmethod
    def scan(port=None):
        """
        port (string): name of the serial port. If None, all the serial ports are tried
        returns (list of 2-tuple): name, args (port)
        Note: it's obviously not advised to call this function if a device is already under use
        """
        if port:
            ports = [port]
        else:
            if os.name == "nt":
                ports = ["COM" + str(n) for n in range(8)]
            else:
                ports = glob.glob('/dev/ttyS?*') + glob.glob('/dev/ttyUSB?*')

        logging.info("Serial ports scanning for Acton SpectraPro spectrograph in progress...")
        found = []  # (list of 2-tuple): name, kwargs
        for p in ports:
            try:
                logging.debug("Trying port %s", p)
                dev = SpectraPro(None, None, p, _noinit=True)
            except (serial.SerialException, HwError):
                # not possible to use this port? next one!
                continue

            # Try to connect and get back some answer.
            try:
                model = dev.GetModel()
                if model.startswith("SP-"):
                    found.append((model, {"port": p}))
                else:
                    logging.info("Device on port '%s' responded correctly, but with unexpected model name '%s'.", p, model)
            except:
                continue

        return found

    @staticmethod
    def openSerialPort(port):
        """
        Opens the given serial port the right way for the SpectraPro.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        return (serial): the opened serial port
        """
        # according to doc:
        # "port set-up is 9600 baud, 8 data bits, 1 stop bit and no parity"
        ser = serial.Serial(
            port=port,
            baudrate=9600,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=2 # s
        )

        return ser
Ejemplo n.º 40
0
class PowerControlUnit(model.PowerSupplier):
    '''
    Implements the PowerSupplier class to regulate the power supply of the
    components connected to the Power Control Unit board. It also takes care of
    communication with the PCU firmware.
    '''
    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, ))

    @isasync
    def supply(self, sup):
        """
        Change the power supply to the defined state for each component given.
        This is an asynchronous method.
        sup dict(string-> boolean): name of the component and new state
        returns (Future): object to control the supply request
        """
        if not sup:
            return model.InstantaneousFuture()
        self._checkSupply(sup)

        return self._executor.submit(self._doSupply, sup)

    def _doSupply(self, sup, apply_delay=True):
        """
        supply power
        apply_delay (bool): If true, wait the amount of time requested in delay
          after turning on the power
        """
        for comp, val in sup.items():
            # find pin and values corresponding to component
            pin = self._pin_map[comp]
            # should always be able to get the value, default values just to be
            # on the safe side
            if apply_delay:
                delay = self._delay.get(comp, 0)
            else:
                # We still wait a little, to avoid starting all components
                # _exactly_ at the same time, which could cause a power peak.
                delay = 1

            if val:
                self._sendCommand("PWR " + str(pin) + " 1")
                state = self.supplied.value[comp]
                if state:
                    # Already on, wait the time remaining
                    remaining = (self._last_start[comp] + delay) - time.time()
                    time.sleep(max(0, remaining))
                else:
                    # wait full time
                    self._last_start[comp] = time.time()
                    time.sleep(delay)

                # Check it really worked
                ans = self._sendCommand("PWR? " + str(pin))
                if ans != "1":
                    logging.warning("Failed to turn on component %s", comp)
            else:
                self._sendCommand("PWR " + str(pin) + " 0")

        self._updateSupplied()

    def _updateSupplied(self):
        """
        update the supplied VA
        """
        pins_updated = set(self._pin_map.values()
                           )  # to avoid asking for the same pin multiple times
        for pin in pins_updated:
            ans = self._sendCommand("PWR? " + str(pin))
            # Update all components that are connected to the same pin
            to_update = [c for c in self.powered if pin == self._pin_map[c]]
            for c_update in to_update:
                self._supplied[c_update] = (ans == "1")

        # it's read-only, so we change it via _value
        self.supplied._value = self._supplied
        self.supplied.notify(self.supplied.value)

    def terminate(self):
        if self._executor:
            self._executor.cancel()
            self._executor.shutdown()
            self._executor = None

        # Power components on/off according to ._termination
        # If nothing is specified, leave it as-is.
        logging.debug("Changing power supply on termination: %s" %
                      self._termination)
        self._doSupply(self._termination)

        if self._serial:
            with self._ser_access:
                self._serial.close()
                self._serial = None

        if self._file:
            self._file.close()
            self._file = None

    def _getIdentification(self):
        return self._sendCommand("*IDN?")

    def writeMemory(self, id, address, data):
        """
        Write data to EEPROM.
        id (str): EEPROM registration number #hex (little-endian format)
        address (str): starting address #hex
        data (str): data to be written #hex (little-endian format)
        """
        self._sendCommand("WMEM %s %s %s" % (id, address, data))

    def readMemory(self, id, address, length):
        """
        Read data from EEPROM.
        id (str): EEPROM registration number #hex (little-endian format)
        address (str): starting address #hex
        length (int): number of bytes to be read
        returns (str): data read back #hex (little-endian format)
        """
        ans = self._sendCommand("RMEM %s %s %s" % (id, address, length))
        return ans

    def readEEPROM(self, id):
        """
        We use this method to get a dict that contains all the data written in
        the EEPROM with the given id.
        id (str): EEPROM registration number #hex (little-endian format)
        """
        if id not in self.memoryIDs.value:
            raise KeyError("There was no EEPROM with the given id found")
        mem_cont = self.readMemory(id, "00", EEPROM_CAPACITY)
        mem_yaml = ""
        while mem_cont != "":
            if mem_cont[:2] != "00":
                mem_yaml += chr(int(mem_cont[:2], 16))
            mem_cont = mem_cont[2:]
        dct = yaml.load(mem_yaml)
        return dct

    def _getIdentities(self):
        """
        Return the ids of connected EEPROMs
        """
        try:
            ans = self._sendCommand("SID")
        except PowerControlError as e:
            # means there is no power provided
            raise HwError(
                "There is no power provided to the Power Control Unit. "
                "Please make sure the board is turned on.")
        return [x for x in ans.split(',') if x != '']

    def _sendCommand(self, cmd):
        """
        cmd (str): command to be sent to Power Control unit.
        returns (str): answer received from the Power Control unit
        raises:
            IOError: if an ERROR is returned by the Power Control firmware.
        """
        cmd = (cmd + "\n").encode('latin1')
        with self._ser_access:
            logging.debug("Sending command %s" % to_str_escape(cmd))
            self._serial.write(cmd)

            ans = b''
            char = None
            while char != b'\n':
                char = self._serial.read()
                if not char:
                    logging.error("Timeout after receiving %s",
                                  to_str_escape(ans))
                    # TODO: See how you should handle a timeout before you raise
                    # an HWError
                    raise HwError(
                        "Power Control Unit connection timeout. "
                        "Please turn off and on the power to the box.")
                # Handle ERROR coming from Power control unit firmware
                ans += char

            logging.debug("Received answer %s", to_str_escape(ans))
            ans = ans.decode('latin1')
            if ans.startswith("ERROR"):
                raise PowerControlError(ans.split(' ', 1)[1])

            return ans.rstrip()

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for a Power control device.
        port (string): the name of the serial port (e.g., /dev/ttyACM0)
        return (serial): the opened serial port
        """
        ser = serial.Serial(
            port=port,
            timeout=1  # s
        )

        # Purge
        ser.flush()
        ser.flushInput()

        # Try to read until timeout to be extra safe that we properly flushed
        while True:
            char = ser.read()
            if char == b'':
                break
        logging.debug(
            "Nothing left to read, Power Control Unit can safely initialize.")

        ser.timeout = 5  # Sometimes the software-based USB can have some hiccups
        return ser

    def _findDevice(self, ports):
        """
        Look for a compatible device
        ports (str): pattern for the port name
        return (str): the name of the port used
        It also sets ._serial and ._idn to contain the opened serial port, and
        the identification string.
        raises:
            IOError: if no device are found
        """
        # For debugging purpose
        if ports == "/dev/fake":
            self._serial = PowerControlSimulator(timeout=1)
            return ports

        if os.name == "nt":
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(ports)

        for n in names:
            try:
                self._file = open(n)  # Open in RO, just to check for lock
                try:
                    fcntl.flock(self._file.fileno(),
                                fcntl.LOCK_EX | fcntl.LOCK_NB)
                except IOError:
                    logging.info("Port %s is busy, will wait and retry", n)
                    time.sleep(11)
                    fcntl.flock(self._file.fileno(),
                                fcntl.LOCK_EX | fcntl.LOCK_NB)
                self._serial = self._openSerialPort(n)

                try:
                    idn = self._getIdentification()
                except PowerControlError:
                    # Can happen if the device has received some weird characters
                    # => try again (now that it's flushed)
                    logging.info("Device answered by an error, will try again")
                    idn = self._getIdentification()
                # Check that we connect to the right device
                if not idn.startswith("Delmic Analog Power"):
                    logging.info("Connected to wrong device on %s, skipping.",
                                 n)
                    continue
                return n
            except (IOError, PowerControlError):
                # not possible to use this port? next one!
                logging.debug(
                    "Skipping port %s which doesn't seem the right device", n)
                continue
        else:
            raise HwError(
                "Failed to find a Power Control device on ports '%s'. "
                "Check that the device is turned on and connected to "
                "the computer." % (ports, ))

    @classmethod
    def scan(cls):
        """
        returns (list of 2-tuple): name, args (sn)
        Note: it's obviously not advised to call this function if a device is already under use
        """
        logging.info(
            "Serial ports scanning for Power control device in progress...")
        found = []  # (list of 2-tuple): name, kwargs

        if sys.platform.startswith('linux'):
            # Look for each ACM device, if the IDN is the expected one
            acm_paths = glob.glob('/dev/ttyACM?')
            for port in acm_paths:
                # open and try to communicate
                try:
                    dev = cls(name="test", role="test", port=port)
                    idn = dev._getIdentification()
                    if idn.startswith("Delmic Analog Power"):
                        found.append({"port": port})
                except Exception:
                    pass
        else:
            # TODO: Windows version
            raise NotImplementedError("OS not yet supported")

        return found
Ejemplo n.º 41
0
    def __init__(self, name, role, children, axis_name, positions, cycle=None, **kwargs):
        """
        name (string)
        role (string)
        children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis
        axis_name (str): axis name in the child actuator
        positions (set or dict value -> str): positions where the actuator is allowed to move
        cycle (float): if not None, it means the actuator does a cyclic move and this value represents a full cycle
        """
        # TODO: forbid inverted
        if len(children) != 1:
            raise ValueError("FixedPositionsActuator needs precisely one child")

        self._cycle = cycle
        self._move_sum = 0
        self._position = {}
        self._referenced = {}
        axis, child = children.items()[0]
        self._axis = axis
        self._child = child
        self._caxis = axis_name
        self._positions = positions
        # Executor used to reference and move to nearest position
        self._executor = CancellableThreadPoolExecutor(max_workers=1)  # one task at a time

        if not isinstance(child, model.ComponentBase):
            raise ValueError("Child %s is not a component." % (child,))
        if not hasattr(child, "axes") or not isinstance(child.axes, dict):
            raise ValueError("Child %s is not an actuator." % child.name)

        if cycle is not None:
            # just an offset to reference switch position
            self._offset = self._cycle / len(self._positions)
            if not all(0 <= p < cycle for p in positions.keys()):
                raise ValueError("Positions must be between 0 and %s (non inclusive)" % (cycle,))

        ac = child.axes[axis_name]
        axes = {axis: model.Axis(choices=positions, unit=ac.unit)}  # TODO: allow the user to override the unit?

        model.Actuator.__init__(self, name, role, axes=axes, children=children, **kwargs)

        self._position = {}
        self.position = model.VigilantAttribute({}, readonly=True)

        logging.debug("Subscribing to position of child %s", child.name)
        child.position.subscribe(self._update_child_position, init=True)

        if model.hasVA(child, "referenced") and axis_name in child.referenced.value:
            self._referenced[axis] = child.referenced.value[axis_name]
            self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True)
            child.referenced.subscribe(self._update_child_ref)

        # If the axis can be referenced => do it now (and move to a known position)
        # In case of cyclic move always reference
        if not self._referenced.get(axis, True) or (self._cycle and axis in self._referenced):
            # The initialisation will not fail if the referencing fails
            f = self.reference({axis})
            f.add_done_callback(self._on_referenced)
        else:
            # If not at a known position => move to the closest known position
            nearest = util.find_closest(self._child.position.value[self._caxis], self._positions.keys())
            self.moveAbs({self._axis: nearest}).result()
Ejemplo n.º 42
0
class PM8742(model.Actuator):
    """
    Represents one New Focus picomotor controller 8742.
    """
    def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs):
        """
        address (str): ip address (use "autoip" to automatically scan and find the
          controller, "fake" for a simulator)
        axes (list of str): names of the axes, from the 1st to the 4th, if present.
          if an axis is not connected, put a "".
        stepsize (list of float): size of a step in m (the smaller, the
          bigger will be a move for a given distance in m)
        sn (str or None): serial number of the device (eg, "11500"). If None, the
          driver will use whichever controller is first found.
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if not 1 <= len(axes) <= 4:
            raise ValueError(
                "Axes must be a list of 1 to 4 axis names (got %s)" % (axes, ))
        if len(axes) != len(stepsize):
            raise ValueError("Expecting %d stepsize (got %s)" %
                             (len(axes), stepsize))
        self._name_to_axis = {}  # str -> int: name -> axis number
        for i, n in enumerate(axes):
            if n == "":  # skip this non-connected axis
                continue
            self._name_to_axis[n] = i + 1

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

        self._address = address
        self._sn = sn
        self._accesser = self._openConnection(address, sn)
        self._recover = False

        self._resynchonise()

        if name is None and role is None:  # For scan only
            return

        # Seems to really be the device, so handle connection errors fully
        self._recover = True

        modl, fw, sn = self.GetIdentification()
        if modl != "8742":
            logging.warning("Controller %s is not supported, will try anyway",
                            modl)

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

        # Let the controller check the actuators are connected
        self.MotorCheck()

        axes_def = {}
        speed = {}
        for n, i in self._name_to_axis.items():
            sz = self._stepsize[i - 1]
            # TODO: allow to pass the range in m in the arguments
            # Position supports ±2³¹, probably not that much in reality, but
            # there is no info.
            rng = [(-2**31) * sz, (2**31 - 1) * sz]

            # Check the actuator is connected
            mt = self.GetMotorType(i)
            if mt in {MT_NONE, MT_UNKNOWN}:
                raise HwError(
                    "Controller failed to detect motor %d, check the "
                    "actuator is connected to the controller" % (i, ))
            max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt]
            srng = (0, self._speedToMS(i, max_stp_s))
            speed[n] = self._speedToMS(i, self.GetVelocity(i))

            axes_def[n] = model.Axis(range=rng, speed=srng, unit="m")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        self._swVersion = "%s (IP connection)" % (odemis.__version__, )
        self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn)

        # Note that the "0" position is just the position at which the
        # controller turned on
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed,
                                        range=(0, max_speed),
                                        unit="m/s",
                                        setter=self._setSpeed)

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown(wait=True)
            self._executor = None

        if self._accesser:
            self._accesser.terminate()
            self._accesser = None

    def _sendOrderCommand(self, cmd, val="", axis=None):
        return self._accesser.sendOrderCommand(cmd, val, axis)

    def _sendQueryCommand(self, cmd, val="", axis=None):
        """
        Same as accesser's sendQueryCommand, but with error recovery
        """
        trials = 0
        while True:
            try:
                return self._accesser.sendQueryCommand(cmd, val, axis)
            except IOError:  # Typically due to timeout
                trials += 1
                if not self._recover and trials < 5:
                    raise
                self._recover = False
                try:
                    # can also happen just due to error
                    # => first read error and see if that explains anything
                    self._checkError()
                except IOError:
                    # Sometimes the hardware seems to lose connection
                    # => try to reconnect
                    logging.warning(
                        "Device seems disconnected, will try to reconnect")
                    # Sometimes the device gets confused and answers are shifted.
                    # Reset helps, but it also reset the current position, which
                    # is not handy.
                    # self._accesser.sendOrderCommand("RS")
                    self._accesser.terminate()
                    time.sleep(0.5)
                    self._accesser = self._openConnection(
                        self._address, self._sn)
                    self._checkError()
                    logging.info("Recovered lost connection to device %s",
                                 self.name)
                finally:
                    self._recover = True

    # Low level functions
    def GetIdentification(self):
        """
        return (str, str, str):
             Model name
             Firmware version (and date)
             serial number
        """
        resp = self._sendQueryCommand("*IDN")
        # expects something like this:
        # New_Focus 8742 v2.2 08/01/13 11511
        try:
            m = re.match("\w+ (?P<model>\w+) (?P<fw>v\S+ \S+) (?P<sn>\d+)",
                         resp)
            modl, fw, sn = m.groups()
        except Exception:
            raise IOError("Failed to decode firmware answer '%s'" %
                          resp.encode('string_escape'))

        return modl, fw, sn

    def GetMotorType(self, axis):
        """
        Read the motor type.
        The motor check action must have been performed before to get correct
          values.
        axis (1<=int<=4): axis number
        return (0<=int<=3): the motor type
        """
        resp = self._sendQueryCommand("QM", axis=axis)
        return int(resp)

    def GetVelocity(self, axis):
        """
        Read the max speed
        axis (1<=int<=4): axis number
        return (0<=int<=2000): the speed in step/s
        """
        resp = self._sendQueryCommand("VA", axis=axis)
        return int(resp)

    def SetVelocity(self, axis, val):
        """
        Write the max speed
        axis (1<=int<=4): axis number
        val (1<=int<=2000): the speed in step/s
        """
        if not 1 <= val <= 2000:
            raise ValueError("Velocity outside of the range 0->2000")
        self._sendOrderCommand("VA", "%d" % (val, ), axis)

    def GetAccel(self, axis):
        """
        Read the acceleration
        axis (1<=int<=4): axis number
        return (0<=int): the acceleration in step/s²
        """
        resp = self._sendQueryCommand("AC", axis=axis)
        return int(resp)

    def SetAccel(self, axis, val):
        """
        Write the acceleration
        axis (1<=int<=4): axis number
        val (1<=int<=200000): the acceleration in step/s²
        """
        if not 1 <= val <= 200000:
            raise ValueError("Acceleration outside of the range 0->200000")
        self._sendOrderCommand("AC", "%d" % (val, ), axis)

    def MotorCheck(self):
        """
        Run the motor check command, that automatically configure the right
        values based on the type of motors connected.
        """
        self._sendOrderCommand("MC")

    def MoveAbs(self, axis, pos):
        """
        Requests a move to an absolute position. This is non-blocking.
        axis (1<=int<=4): axis number
        pos (-2**31 <= int 2*31-1): position in step
        """
        self._sendOrderCommand("PA", "%d" % (pos, ), axis)

    def GetTarget(self, axis):
        """
        Read the target position for the given axis
        axis (1<=int<=4): axis number
        return (int): the position in steps
        """
        # Note, it's not clear what's the difference with PR?
        resp = self._sendQueryCommand("PA", axis=axis)
        return int(resp)

    def MoveRel(self, axis, offset):
        """
        Requests a move to a relative position. This is non-blocking.
        axis (1<=int<=4): axis number
        offset (-2**31 <= int 2*31-1): offset in step
        """
        self._sendOrderCommand("PR", "%d" % (offset, ), axis)

    def GetPosition(self, axis):
        """
        Read the actual position for the given axis
        axis (1<=int<=4): axis number
        return (int): the position in steps
        """
        resp = self._sendQueryCommand("TP", axis=axis)
        return int(resp)

    def IsMotionDone(self, axis):
        """
        Check whether the axis is in motion 
        axis (1<=int<=4): axis number
        return (bool): False if in motion, True if motion is finished
        """
        resp = self._sendQueryCommand("MD", axis=axis)
        if resp == "0":  # motion in progress
            return False
        elif resp == "1":  # no motion
            return True
        else:
            raise IOError("Failed to decode answer about motion '%s'" %
                          resp.encode('string_escape'))

    def AbortMotion(self):
        """
        Stop immediately the motion on all the axes
        """
        self._sendOrderCommand("AB")

    def StopMotion(self, axis):
        """
        Stop nicely the motion (using accel/decel values)
        axis (1<=int<=4): axis number
        """
        self._sendOrderCommand("ST", axis=axis)

    def GetError(self):
        """
        Read the oldest error in memory.
        The error buffer is FIFO with 10 elements, so it might not be the 
        latest error if multiple errors have happened since the last time this
        function was called.
        return (None or (int, str)): the error number and message
        """
        # Note: there is another one "TE" which only returns the number, and so
        # is faster, but then there is no way to get the message
        resp = self._sendQueryCommand("TB")
        # returns something like "108, MOTOR NOT CONNECTED"
        try:
            m = re.match("(?P<no>\d+), (?P<msg>.+)", resp)
            no, msg = int(m.group("no")), m.group("msg")
        except Exception:
            raise IOError("Failed to decode error info '%s'" %
                          resp.encode('string_escape'))

        if no == 0:
            return None
        else:
            return no, msg

    def _checkError(self):
        """
        Check if an error happened and convert to a python exception
        return None
        raise NewFocusError if an error happened
        """
        err = self.GetError()
        if err:
            errno, msg = err
            raise NewFocusError(errno, msg)

    def _resynchonise(self):
        """
        Ensures the device communication is "synchronised"
        """
        self._accesser.flushInput()

        # drop all the errors
        while self.GetError():
            pass

    # high-level methods (interface)
    def _updatePosition(self, axes=None):
        """
        update the position VA
        axes (set of str): names of the axes to update or None if all should be
          updated
        """
        # uses the current values (converted to internal representation)
        pos = self._applyInversion(self.position.value)

        for n, i in self._name_to_axis.items():
            if axes is None or n in axes:
                pos[n] = self.GetPosition(i) * self._stepsize[i - 1]

        pos = self._applyInversion(pos)

        # it's read-only, so we change it via _value
        self.position._value = pos
        self.position.notify(self.position.value)

    def _speedToMS(self, axis, sps):
        """
        Convert speed in step/s to m/s
        axis (1<=int<=4): axis number
        sps (int): steps/s
        return (float): m/s
        """
        return sps * self._stepsize[axis - 1]

    def _setSpeed(self, value):
        """
        value (dict string-> float): speed for each axis
        returns (dict string-> float): the new value
        """
        if set(value.keys()) != set(self._axes.keys()):
            raise ValueError("Requested speed %s doesn't specify all axes %s" %
                             (value, self._axes.keys()))
        for axis, v in value.items():
            rng = self._axes[axis].speed
            if not rng[0] < v <= rng[1]:
                raise ValueError(
                    "Requested speed of %f for axis %s not within %f->%f" %
                    (v, axis, rng[0], rng[1]))

            i = self._name_to_axis[axis]
            sps = max(1, int(round(v / self._stepsize[i - 1])))
            self.SetVelocity(i, sps)

        return value

    def _createFuture(self):
        """
        Return (CancellableFuture): a future that can be used to manage a move
        """
        f = CancellableFuture()
        f._moving_lock = threading.Lock()  # taken while moving
        f._must_stop = threading.Event(
        )  # cancel of the current future requested
        f._was_stopped = False  # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

    @isasync
    def moveRel(self, shift):
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)

        # Check if the distance is big enough to make sense
        for an, v in shift.items():
            aid = self._name_to_axis[an]
            if abs(v) < self._stepsize[aid - 1]:
                # TODO: store and accumulate all the small moves instead of dropping them?
                del shift[an]
                logging.info("Dropped too small move of %g m < %g m", abs(v),
                             self._stepsize[aid - 1])

        if not shift:
            return model.InstantaneousFuture()

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveRel, f, shift)
        return f

    moveRel.__doc__ = model.Actuator.moveRel.__doc__

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        f = self._createFuture()
        f = self._executor.submitf(f, self._doMoveAbs, f, pos)
        return f

    moveAbs.__doc__ = model.Actuator.moveAbs.__doc__

    def stop(self, axes=None):
        self._executor.cancel()

    def _doMoveRel(self, future, pos):
        """
        Blocking and cancellable relative move
        future (Future): the future it handles
        pos (dict str -> float): axis name -> relative target position
        """
        with future._moving_lock:
            end = 0  # expected end
            moving_axes = set()
            for an, v in pos.items():
                aid = self._name_to_axis[an]
                moving_axes.add(aid)
                steps = int(round(v / self._stepsize[aid - 1]))
                self.MoveRel(aid, steps)
                # compute expected end
                dur = abs(steps) * self._stepsize[aid -
                                                  1] / self.speed.value[an]
                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        logging.debug("move successfully completed")

    def _doMoveAbs(self, future, pos):
        """
        Blocking and cancellable absolute move
        future (Future): the future it handles
        pos (dict str -> float): axis name -> absolute target position
        """
        with future._moving_lock:
            end = 0  # expected end
            old_pos = self._applyInversion(self.position.value)
            moving_axes = set()
            for an, v in pos.items():
                aid = self._name_to_axis[an]
                moving_axes.add(aid)
                steps = int(round(v / self._stepsize[aid - 1]))
                self.MoveAbs(aid, steps)
                # compute expected end
                dur = abs(v - old_pos[an]) / self.speed.value[an]
                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        logging.debug("move successfully completed")

    def _waitEndMove(self, future, axes, end=0):
        """
        Wait until all the given axes are finished moving, or a request to
        stop has been received.
        future (Future): the future it handles
        axes (set of int): the axes IDs to check
        end (float): expected end time
        raise:
            CancelledError: if cancelled before the end of the move
        """
        moving_axes = set(axes)

        last_upd = time.time()
        last_axes = moving_axes.copy()
        try:
            while not future._must_stop.is_set():
                for aid in moving_axes.copy(
                ):  # need copy to remove during iteration
                    if self.IsMotionDone(aid):
                        moving_axes.discard(aid)
                if not moving_axes:
                    # no more axes to wait for
                    break

                # Update the position from time to time (10 Hz)
                if time.time() - last_upd > 0.1 or last_axes != moving_axes:
                    last_names = set(n for n, i in self._name_to_axis.items()
                                     if i in last_axes)
                    self._updatePosition(last_names)
                    last_upd = time.time()
                    last_axes = moving_axes.copy()

                # Wait half of the time left (maximum 0.1 s)
                left = end - time.time()
                sleept = max(0.001, min(left / 2, 0.1))
                future._must_stop.wait(sleept)

                # TODO: timeout if really too long
            else:
                logging.debug("Move of axes %s cancelled before the end", axes)
                # stop all axes still moving them
                for i in moving_axes:
                    self.StopMotion(i)
                future._was_stopped = True
                raise CancelledError()
        except Exception:
            raise
        else:
            # Did everything really finished fine?
            self._checkError()
        finally:
            self._updatePosition()  # update (all axes) with final position

    def _cancelCurrentMove(self, future):
        """
        Cancels the current move (both absolute or relative). Non-blocking.
        future (Future): the future to stop. Unused, only one future must be 
         running at a time.
        return (bool): True if it successfully cancelled (stopped) the move.
        """
        # The difficulty is to synchronise correctly when:
        #  * the task is just starting (not finished requesting axes to move)
        #  * the task is finishing (about to say that it finished successfully)
        logging.debug("Cancelling current move")

        future._must_stop.set(
        )  # tell the thread taking care of the move it's over
        with future._moving_lock:
            if not future._was_stopped:
                logging.debug("Cancelling failed")
            return future._was_stopped

    @classmethod
    def scan(cls):
        """
        returns (list of (str, dict)): name, kwargs
        Note: it's obviously not advised to call this function if a device is already under use
        """
        logging.info("Scanning for New Focus controllers in progress...")
        found = []  # (list of 2-tuple): name, kwargs
        try:
            conts = cls._scanOverIP()
        except IOError as exp:
            logging.exception("Failed to scan for New Focus controllers: %s",
                              exp)

        for hn, host, port in conts:
            try:
                logging.debug("Trying controller at %s", host)
                dev = cls(None,
                          None,
                          address=host,
                          axes=["a"],
                          stepsize=[1e-6])
                modl, fw, sn = dev.GetIdentification()

                # find out about the axes
                dev.MotorCheck()
                axes = []
                stepsize = []
                for i in range(1, 5):
                    mt = dev.GetMotorType(i)
                    n = chr(ord('a') + i - 1)
                    # No idea about the stepsize, but make it different to allow
                    # distinguishing between motor types
                    if mt == MT_STANDARD:
                        ss = 1e-6
                    elif mt == MT_TINY:
                        ss = 0.1e-6
                    else:
                        n = ""
                        ss = 0
                    axes.append(n)
                    stepsize.append(ss)
            except IOError:
                # not possible to use this port? next one!
                continue
            except Exception:
                logging.exception(
                    "Error while communicating with controller %s @ %s:%s", hn,
                    host, port)
                continue

            found.append(("NF-%s-%s" % (modl, sn), {
                "address": host,
                "axes": axes,
                "stepsize": stepsize,
                "sn": sn
            }))

        return found

    @classmethod
    def _openConnection(cls, address, sn=None):
        """
        return (Accesser)
        """
        if address == "fake":
            host, port = "fake", 23
        elif address == "autoip":
            conts = cls._scanOverIP()
            if sn is not None:
                for hn, host, port in conts:
                    # Open connection to each controller and ask for their SN
                    dev = cls(None,
                              None,
                              address=host,
                              axes=["a"],
                              stepsize=[1e-6])
                    _, _, devsn = dev.GetIdentification()
                    if sn == devsn:
                        break
                else:
                    raise HwError(
                        "Failed to find New Focus controller %s over the "
                        "network. Ensure it is turned on and connected to "
                        "the network." % (sn, ))
            else:
                # just pick the first one
                # TODO: only pick the ones of model 8742
                try:
                    hn, host, port = conts[0]
                    logging.info("Connecting to New Focus %s", hn)
                except IndexError:
                    raise HwError(
                        "Failed to find New Focus controller over the "
                        "network. Ensure it is turned on and connected to "
                        "the network.")

        else:
            # split the (IP) port, separated by a :
            if ":" in address:
                host, ipport_str = address.split(":")
                port = int(ipport_str)
            else:
                host = address
                port = 23  # default

        return IPAccesser(host, port)

    @staticmethod
    def _scanOverIP():
        """
        Scan the network for all the responding new focus controllers
        Note: it actually calls a separate executable because it relies on opening
          a network port which needs special privileges.
        return (list of (str, str, int)): hostname, ip address, and port number
        """
        # Run the separate program via authbind
        try:
            exc = os.path.join(os.path.dirname(__file__), "nfpm_netscan.py")
            out = subprocess.check_output(["authbind", "python", exc])
        except CalledProcessError as exp:
            # and handle all the possible errors:
            # - no authbind (127)
            # - cannot find the separate program (2)
            # - no authorisation (13)
            ret = exp.returncode
            if ret == 127:
                raise IOError("Failed to find authbind")
            elif ret == 2:
                raise IOError("Failed to find %s" % exc)
            elif ret == 13:
                raise IOError("No permission to open network port 23")

        # or decode the output
        # hostname \t host \t port
        ret = []
        for l in out.split("\n"):
            if not l:
                continue
            try:
                hn, host, port = l.split("\t")
            except Exception:
                logging.exception("Failed to decode scanner line '%s'", l)
            ret.append((hn, host, port))

        return ret
Ejemplo n.º 43
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()
Ejemplo n.º 44
0
class ESP(model.Actuator):
    def __init__(self, name, role, port, axes=None, **kwargs):
        """
        A driver for a Newport ESP 301 Stage Actuator.
        This driver supports a serial connection. Note that as of the Linux
        kernel 4.13, the USB connection is known to _not_ work, as the TI 3410
        chipset apparently behind is not handled properly. Use a of the
        RS-232 port is required (via a USB adapter if necessary).

        name: (str)
        role: (str)
        port: (str) port name. Can be a pattern, in which case all the ports
          fitting the pattern will be tried.
          Use /dev/fake for a simulator
        axes: dict str (axis name) -> dict (axis parameters)
            axis parameters: {
                number (1 <= int <= 3): axis number on the hardware
                range: [float, float], default is -1 -> 1
                unit (str): the external unit of the axis (internal is mm),
                   default is "m".
                conv_factor (float): a conversion factor that converts to the
                   device internal unit (mm), default is 1000.
            }

        inverted: (bool) defines if the axes are inverted

        The offset can be specified by setting MD_POS_COR as a coordinate dictionary
        """

        if len(axes) == 0:
            raise ValueError("Needs at least 1 axis.")

        # Connect to serial port
        self._ser_access = threading.Lock()
        self._serial = None
        self._file = None
        self._port, self._version = self._findDevice(
            port)  # sets ._serial and ._file
        logging.info("Found Newport ESP301 device on port %s, Ver: %s",
                     self._port, self._version)

        self.LockKeypad(
            KEYPAD_LOCK_EXCEPT_STOP)  # lock user input for the controller

        # Clear errors at start
        try:
            self.checkError()
        except ESPError:
            pass

        self._offset = {}
        self._axis_conv_factor = {}

        # Not to be mistaken with axes which is a simple public view
        self._axis_map = {}  # axis name -> axis number used by controller
        axes_def = {}  # axis name -> Axis object
        speed = {}
        accel = {}
        decel = {}
        self._id = {}

        for axis_name, axis_par in axes.items():
            # Unpack axis parameters from the definitions in the YAML
            try:
                axis_num = axis_par['number']
            except KeyError:
                raise ValueError(
                    "Axis %s must have a number to identify it. " %
                    (axis_name, ))

            try:
                axis_range = axis_par['range']
            except KeyError:
                logging.info("Axis %s has no range. Assuming (-1, 1)",
                             axis_name)
                axis_range = (-1, 1)

            try:
                axis_unit = axis_par['unit']
            except KeyError:
                logging.info("Axis %s has no unit. Assuming m", axis_name)
                axis_unit = "m"

            try:
                conv_factor = float(axis_par['conv_factor'])
            except KeyError:
                logging.info(
                    "Axis %s has no conversion factor. Assuming 1000 (m to mm)",
                    axis_name)
                conv_factor = 1000.0

            self._axis_map[axis_name] = axis_num
            self._axis_conv_factor[axis_num] = conv_factor
            self._id[axis_num] = self.GetIdentification(axis_num)
            speed[axis_name] = self.GetSpeed(axis_num)
            accel[axis_name] = self.GetAcceleration(axis_num)
            decel[axis_name] = self.GetDeceleration(axis_num)

            # Force millimetres for consistency as the internal unit.
            self.SetAxisUnit(axis_num, "mm")
            # initialize each motor
            self.MotorOn(axis_num)

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

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute({a: False
                                                   for a in axes},
                                                  readonly=True)

        self._hwVersion = str(self._id)
        self._swVersion = self._version

        # Get the position in object coord with the offset applied.

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()

        self._speed = speed
        self._accel = accel
        self._decel = decel

        # set offset due to mounting of components (float)
        self._metadata[model.MD_POS_COR] = {}

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

        # Check the error state
        self.checkError()

    def terminate(self):
        if self._serial.isOpen():
            self.LockKeypad(
                KEYPAD_UNLOCK)  # unlock user input for the controller

        with self._ser_access:
            self._serial.close()
        model.Actuator.terminate(self)

    def updateMetadata(self, md):
        super(ESP, self).updateMetadata(md)
        try:
            value = md[model.MD_POS_COR]
        except KeyError:
            # there is no offset set.
            return

        if not isinstance(value, dict):
            raise ValueError(
                "Invalid metadata, should be a coordinate dictionary but got %s."
                % (value, ))

        # update all axes
        for n in self._axis_map.keys():
            if n in value:
                self._offset[n] = value[n]
        logging.debug("Updating offset to %s.", value)
        self._updatePosition()

    # Connection methods

    @staticmethod
    def _openSerialPort(port, baudrate):
        """
        Opens the given serial port the right way for a Power control device.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        baudrate (int)
        return (serial): the opened serial port
        """
        ser = serial.Serial(
            port=port,
            baudrate=baudrate,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            rtscts=True,
            timeout=1  # s
        )

        # Purge
        ser.flush()
        ser.flushInput()

        # Try to read until timeout to be extra safe that we properly flushed
        ser.timeout = 0
        while True:
            char = ser.read()
            if char == b'':
                break
        ser.timeout = 1

        return ser

    def _findDevice(self, ports, baudrate=19200):
        """
        Look for a compatible device
        ports (str): pattern for the port name
        baudrate (0<int)
        return:
           (str): the name of the port used
           (str): the hardware version string
           Note: will also update ._file and ._serial
        raises:
            IOError: if no devices are found
        """
        # For debugging purpose
        if ports == "/dev/fake":
            self._serial = ESPSimulator(timeout=1)
            self._file = None
            ve = self.GetVersion()
            return ports, ve

        if os.name == "nt":
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(ports)

        for n in names:
            try:
                # Ensure no one will talk to it simultaneously, and we don't talk to devices already in use
                self._file = open(n)  # Open in RO, just to check for lock
                try:
                    fcntl.flock(
                        self._file.fileno(), fcntl.LOCK_EX
                        | fcntl.LOCK_NB)  # Raises IOError if cannot lock
                except IOError:
                    logging.info("Port %s is busy, will not use", n)
                    continue

                self._serial = self._openSerialPort(n, baudrate)

                try:
                    ve = self.GetVersion()
                    if not "ESP301" in ve.upper():
                        raise IOError(
                            "Device at %s is not an ESP301 controller. Reported version string: %s"
                            % (ports, ve))

                except ESPError as e:
                    # Can happen if the device has received some weird characters
                    # => try again (now that it's flushed)
                    logging.info(
                        "Device answered by an error %s, will try again", e)
                    ve = self.GetVersion()
                return n, ve
            except (IOError, ESPError) as e:
                logging.debug(e)
                logging.info(
                    "Skipping device on port %s, which didn't seem to be compatible",
                    n)
                # not possible to use this port? next one!
                continue
        else:
            raise HwError(
                "Failed to find a device on ports '%s'. "
                "Check that the device is turned on and connected to "
                "the computer." % (ports, ))

    def _sendOrder(self, cmd):
        """
        cmd (byte str): command to be sent to device (without the CR)
        """
        cmd = cmd + b"\r"
        with self._ser_access:
            logging.debug("Sending command %s", to_str_escape(cmd))
            self._serial.write(cmd)

    def _sendQuery(self, cmd):
        """
        cmd (byte str): command to be sent to device (without the CR, but with the ?)
        returns (str): answer received from the device (without \n or \r)
        raise:
            IOError if no answer is returned in time
        """
        cmd = cmd + b"\r"
        with self._ser_access:
            logging.debug("Sending command %s", to_str_escape(cmd))
            self._serial.write(cmd)

            self._serial.timeout = 1
            ans = b''
            while ans[-1:] != b'\r':
                char = self._serial.read()
                if not char:
                    raise IOError("Timeout after receiving %s" %
                                  to_str_escape(ans))
                ans += char

            logging.debug("Received answer %s", to_str_escape(ans))

            return ans.strip().decode("latin1")

    # Low level serial commands.
    # Note: These all convert to internal units of the controller

    def GetErrorCode(self):
        # Checks the device error register
        return int(self._sendQuery(b"TE?"))

    def checkError(self):
        # Checks if an error occurred and raises an exception accordingly.
        err_q = []

        # Get all of the errors in the error FIFO stack
        while True:
            errcode = self.GetErrorCode()

            if errcode == 0:  # No error
                break
            else:
                err_q.append(errcode)

        # After errors are collected
        if len(err_q) > 0:
            for err in err_q[:-1]:
                logging.warning("Discarding error %d", err)
            raise ESPError("Error code %d" % (err_q[-1], ), err_q[-1])

    def SetAxisUnit(self, axis_num, unit):
        # Set the internal unit used by the controller
        if not unit in UNIT_DEF:
            raise ValueError("Unknown unit name %s" % (unit, ))
        self._sendOrder(b"%d SN %d" % (axis_num, UNIT_DEF[unit]))

    def MoveLimit(self, aid, limit):
        """
        Requests a move to the positive or negative limit.
        limit (str): either '+' or '-', defining positive or negative limit
        """
        if not limit in ("+", "-"):
            raise ValueError(
                "Asked to move %d to %s limit. Only + or - allowed." % (
                    aid,
                    limit,
                ))
        self._sendOrder(b"%d MV %s" % (aid, limit))

    def LockKeypad(self, lock_type):
        """
        Lock keypad on device from preventing bad user input
        lock_type (KEYPAD_*)
        """
        self._sendOrder(b"LC %d" % (lock_type, ))

    def HomeSearch(self, aid, search_type):
        """
        Searches for home using a search type (int 0,1,2,3,4,5,6) as per manual
        """
        self._sendOrder(b"%d OR %d" % (aid, search_type))

    def SetHome(self, aid, value):
        """
        Set the position value to use at the origin (home)
        """
        self._sendOrder(b"%d SH %f" % (aid, value))

    def SaveMemory(self):
        """
        Save configuration to non - volatile memory
        """
        self._sendOrder(b"SM")

    def MoveAbsPos(self, axis_num, pos):
        """
        Requests a move to an absolute position. This is non-blocking.
        Converts to internal unit of the controller
        """
        self._sendOrder(b"%d PA %f" % (axis_num, pos))

    def MoveRelPos(self, axis_num, rel):
        """
        Requests a move to a relative position. This is non-blocking.
        """
        self._sendOrder(b"%d PR %f" % (axis_num, rel))  # 0 = absolute

    def GetDesiredPos(self, axis_num):
        # Get the target position programmed into the controller
        return float(self._sendQuery(b"%d DP?" % (axis_num, )))

    def StopMotion(self, axis):
        # Stop the motion on the specified axis
        self._sendOrder(b"%d ST" % (axis, ))

    def MotorOn(self, axis):
        # Start the motor
        self._sendOrder(b"%d MO" % (axis, ))

    def MotorOff(self, axis):
        # Stop the motor
        self._sendOrder(b"%d MF" % (axis, ))

    def GetMotionDone(self, axis_n):
        # Return true or false based on if the axis is still moving.
        done = int(self._sendQuery(b"%d MD?" % axis_n))
        logging.debug("Motion done: %d", done)
        return bool(done)

    def GetPosition(self, axis_n):
        # Get the position of the axis
        return float(self._sendQuery(b"%d TP?" % axis_n))

    def GetSpeed(self, axis_n):
        # Get the speed of the axis
        return float(self._sendQuery(b"%d VA?" % axis_n))

    def SetSpeed(self, axis_n, speed):
        # Set the axis speed
        self._sendOrder(b"%d VA %f" % (
            axis_n,
            speed,
        ))

    def GetAcceleration(self, axis_n):
        # Get axis accel
        return float(self._sendQuery(b"%d AC?" % axis_n))

    def SetAcceleration(self, axis_n, ac):
        # Set axis accel
        self._sendOrder(b"%d AC %f" % (
            axis_n,
            ac,
        ))

    def GetDeceleration(self, axis_n):
        return float(self._sendQuery(b"%d AG?" % axis_n))

    def SetDeceleration(self, axis_n, dc):
        self._sendOrder(b"%d AG %f" % (
            axis_n,
            dc,
        ))

    def GetIdentification(self, axis):
        """
        return (str): the identification string as-is for the first axis
        """
        return self._sendQuery(b"%d ID?" % (axis, ))

    def GetVersion(self):
        """
        return (str): the version string as-is
        """
        return self._sendQuery(b"VE?")

    def SaveMem(self):
        """
        Instruct the controller to save the current settings to non-volatile memory
        """
        self._sendOrder(b"SM")

    """
    High level commands (ie, Odemis Actuator API)
    """

    def _applyOffset(self, pos):
        """
        Apply the offset to the position and return it
        """
        ret = dict(pos)
        for axis in self._offset:
            if axis in ret:
                ret[axis] -= self._offset[axis]
        return ret

    def _removeOffset(self, pos):
        """
        Remove the offset from the position and return it
        """
        ret = dict(pos)
        for axis in self._offset:
            if axis in ret:
                ret[axis] += self._offset[axis]
        return ret

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        pos = self._removeOffset(pos)  # Get the position in controller coord.
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doMoveAbs, f, pos)
        return f

    @isasync
    def moveRel(self, shift):
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)
        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doMoveRel, f, shift)
        return f

    def _doMoveRel(self, future, pos):
        """
        Blocking and cancellable relative move
        future (Future): the future it handles
        _pos (dict str -> float): axis name -> relative target position
        raise:
            ValueError: if the target position is
            TMCLError: if the controller reported an error
            CancelledError: if cancelled before the end of the move
        """
        with future._moving_lock:
            end = 0  # expected end
            moving_axes = set()
            for an, v in pos.items():
                aid = self._axis_map[an]
                moving_axes.add(aid)
                self.MoveRelPos(aid, v * self._axis_conv_factor[aid])
                # compute expected end
                # convert to mm units
                dur = driver.estimateMoveDuration(
                    abs(v) * self._axis_conv_factor[aid], self._speed[an],
                    self._accel[an])

                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        self.checkError()
        logging.debug("move successfully completed")

    def _doMoveAbs(self, future, pos):
        """
        Blocking and cancellable absolute move
        future (Future): the future it handles
        _pos (dict str -> float): axis name -> absolute target position
        raise:
            TMCLError: if the controller reported an error
            CancelledError: if cancelled before the end of the move
        """
        with future._moving_lock:
            end = 0  # expected end
            old_pos = self._applyInversion(self.position.value)
            moving_axes = set()
            for an, v in pos.items():
                aid = self._axis_map[an]
                moving_axes.add(aid)
                self.MoveAbsPos(aid, v * self._axis_conv_factor[aid])
                d = abs(v - old_pos[an])
                # convert displacement unit to mm
                dur = driver.estimateMoveDuration(
                    d * self._axis_conv_factor[aid], self._speed[an],
                    self._accel[an])
                end = max(time.time() + dur, end)

            self._waitEndMove(future, moving_axes, end)
        self.checkError()
        logging.debug("move successfully completed")

    def _waitEndMove(self, future, axes, end=0):
        """
        Wait until all the given axes are finished moving, or a request to
        stop has been received.
        future (Future): the future it handles
        axes (set of int): the axes IDs to check
        end (float): expected end time
        raise:
            TimeoutError: if took too long to finish the move
            CancelledError: if cancelled before the end of the move
        """
        moving_axes = set(axes)

        last_upd = time.time()
        dur = max(0.01, min(end - last_upd, 60))
        max_dur = dur * 2 + 1
        logging.debug("Expecting a move of %g s, will wait up to %g s", dur,
                      max_dur)
        timeout = last_upd + max_dur
        last_axes = moving_axes.copy()
        try:
            while not future._must_stop.is_set():
                for aid in moving_axes.copy(
                ):  # need copy to remove during iteration
                    if self.GetMotionDone(aid):
                        moving_axes.discard(aid)
                if not moving_axes:
                    # no more axes to wait for
                    break

                now = time.time()
                if now > timeout:
                    logging.warning("Stopping move due to timeout after %g s.",
                                    max_dur)
                    for i in moving_axes:
                        self.StopMotion(i)
                    raise TimeoutError("Move is not over after %g s, while "
                                       "expected it takes only %g s" %
                                       (max_dur, dur))

                # Update the position from time to time (10 Hz)
                if now - last_upd > 0.1 or last_axes != moving_axes:
                    last_names = set(n for n, i in self._axis_map.items()
                                     if i in last_axes)
                    self._updatePosition(last_names)
                    last_upd = time.time()
                    last_axes = moving_axes.copy()

                # Wait half of the time left (maximum 0.1 s)
                left = end - time.time()
                sleept = max(0.001, min(left / 2, 0.1))
                future._must_stop.wait(sleept)
            else:
                logging.debug("Move of axes %s cancelled before the end", axes)
                # stop all axes still moving them
                for i in moving_axes:
                    self.StopMotion(i)
                future._was_stopped = True
                raise CancelledError()
        finally:
            # TODO: check if the move succeded ? (= Not failed due to stallguard/limit switch)
            self._updatePosition()  # update (all axes) with final position

    # high-level methods (interface)
    def _updatePosition(self, axes=None):
        """
        update the position VA
        axes (set of str): names of the axes to update or None if all should be
          updated
        """
        # uses the current values (converted to internal representation)
        pos = self._applyInversion(self.position.value)

        for n, i in self._axis_map.items():
            if axes is None or n in axes:
                pos[n] = self.GetPosition(i) / self._axis_conv_factor[i]

        pos = self._applyInversion(pos)
        pos = self._applyOffset(pos)  # Appy the offset back for display

        logging.debug("Updated position to %s", pos)

        self.position._set_value(pos, force_write=True)

    def _cancelCurrentMove(self, future):
        """
        Cancels the current move (both absolute or relative). Non-blocking.
        future (Future): the future to stop. Unused, only one future must be
         running at a time.
        return (bool): True if it successfully cancelled (stopped) the move.
        """
        # The difficulty is to synchronise correctly when:
        #  * the task is just starting (not finished requesting axes to move)
        #  * the task is finishing (about to say that it finished successfully)
        logging.debug("Cancelling current move")

        future._must_stop.set(
        )  # tell the thread taking care of the move it's over
        with future._moving_lock:
            if not future._was_stopped:
                logging.debug("Cancelling failed")
            return future._was_stopped

    def stop(self, axes=None):
        self._executor.cancel()
        # For safety, just force stop every axis
        for an, aid in self._axis_map.items():
            if axes is None or an in axes:
                self.StopMotion(aid)
                try:
                    self.checkError()
                except ESPError as e:
                    logging.warning("Cancellation error %d", e.code)

                # Should now turn the motor back on
                self.MotorOn(aid)

    def _createMoveFuture(self):
        """
        Return (CancellableFuture): a future that can be used to manage a move
        """
        f = CancellableFuture()
        f._moving_lock = threading.Lock()  # taken while moving
        f._must_stop = threading.Event(
        )  # cancel of the current future requested
        f._was_stopped = False  # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

    @isasync
    def reference(self, axes):
        if not axes:
            return model.InstantaneousFuture()
        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doReference, f, axes)
        return f

    def _doReference(self, future, axes):
        """
        Actually runs the referencing code
        axes (set of str)
        raise:
            IOError: if referencing failed due to hardware
            CancelledError if was cancelled
        """
        # Reset reference so that if it fails, it states the axes are not
        # referenced (anymore)
        with future._moving_lock:
            try:
                # do the referencing for each axis sequentially
                # (because each referencing is synchronous)
                for a in axes:
                    if future._must_stop.is_set():
                        raise CancelledError()
                    aid = self._axis_map[a]
                    self.referenced._value[a] = False
                    self.HomeSearch(
                        aid, REF_NEGATIVE_LIMIT
                    )  # search for the negative limit signal to set an origin
                    self._waitEndMove(future, (aid, ),
                                      time.time() +
                                      100)  # block until it's over
                    self.SetHome(aid, 0.0)  # set negative limit as origin
                    self.referenced._value[a] = True
            except CancelledError:
                # FIXME: if the referencing is stopped, the device refuses to
                # move until referencing is run (and successful).
                # => Need to put back the device into a mode where at least
                # relative moves work.
                logging.warning(
                    "Referencing cancelled, device will not move until another referencing"
                )
                future._was_stopped = True
                raise
            except Exception:
                logging.exception("Referencing failure")
                raise
            finally:
                # We only notify after updating the position so that when a listener
                # receives updates both values are already updated.
                self._updatePosition(
                    axes)  # all the referenced axes should be back to 0
                # read-only so manually notify
                self.referenced.notify(self.referenced.value)
Ejemplo n.º 45
0
class PMDSimulator(object):
    """
    Simulates beamshift controller with three axes on axes 1, 2 and 3.
    """
    def __init__(self, timeout=0.3):
        self.timeout = timeout
        self._input_buf = ""  # use str internally instead of bytes, makes indexing easier
        self._output_buf = ""

        self.waveform = {1: WAVEFORM_PARK, 2: WAVEFORM_PARK, 3: WAVEFORM_PARK}
        self.target_pos = {1: 0, 2: 0, 3: 0}
        self.current_pos = {1: 0, 2: 0, 3: 0}
        self.speed = 0
        self.is_moving = False
        self.status = "0000"
        self.indexing = True
        self.closed_loop = {1: False, 2: False, 3: False}

        self.executor = CancellableThreadPoolExecutor(1)

    def write(self, data):
        self._input_buf += data.decode('ascii')
        msg = ""
        while self._input_buf[:len(EOL)] != sEOL:
            msg += self._input_buf[0]
            self._input_buf = self._input_buf[1:]
        self._input_buf = self._input_buf[len(EOL):]  # remove EOL

        self._parseMessage(msg)

    def read(self, size=1):
        ret = self._output_buf[:size]
        self._output_buf = self._output_buf[len(ret):]

        if len(ret) < size:
            # simulate timeout
            time.sleep(self.timeout)
        return ret.encode('ascii')

    def flush(self):
        self._input_buf = ""

    def flushInput(self):
        self._output_buf = ""

    def close(self):
        # using read or write will fail after that
        del self._output_buf
        del self._input_buf

    def _parseMessage(self, msg):
        """
        :param msg (str): the message to parse
        :returns (None): self._output_buf is updated if necessary
        """
        # Message structure:
        # X<axis><cmd><EOL> or
        # X<cmd><EOL> or
        # X<axis><cmd><arg0>,...,<argN><EOL>
        # Axis can in principle have multiple digits, but we only care about 1-3, so let's keep
        # it simple
        logging.debug("Received message %s" % msg)

        if msg[0] != "X":
            self._output_buf += "_??_%s" % msg[1:]
            logging.error("Command %s doesn't start with 'X'.", msg)
            return
        try:  # first symbol is axis number
            axis = int(msg[1])
            cmd = msg[2]
            args = msg[3:].split(',')
        except ValueError:
            # msg[1] is not an int --> axis number 0 assumed
            axis = 0
            cmd = msg[1]
            args = msg[2:].split(',')
        args = [] if args == [''] else args

        # Message is always echoed back
        self._output_buf += msg

        try:
            if cmd == "M":
                if not args:
                    self._output_buf += ":%d" % self.waveform[axis]
                elif len(args) == 1:
                    self.waveform[axis] = int(args[0])
                else:
                    raise ValueError()
            elif cmd == "?":
                if not args:
                    self._output_buf += ":PMD401 V1"
                else:
                    raise ValueError()
            elif cmd == "T":
                # Absolute move
                self.closed_loop[axis] = True
                if not args:
                    self._output_buf += ":%d" % self.target_pos[axis]
                elif len(args) == 1:
                    steps = int(args[0]) - self.target_pos[axis]
                    self.target_pos[axis] = int(args[0])
                    steps = int(steps * DEFAULT_ENCODER_RESOLUTION /
                                DEFAULT_MOTORSTEP_RESOLUTION)
                    self.move(steps)
                elif len(args) == 2:
                    steps = int(args[0]) - self.target_pos[axis]
                    self.target_pos[axis] = int(args[0])
                    self.speed = int(args[1])
                    steps = int(steps * DEFAULT_ENCODER_RESOLUTION /
                                DEFAULT_MOTORSTEP_RESOLUTION)
                    self.move(steps)
                else:
                    raise ValueError()
            elif cmd == "S":  # stop axis
                self.is_moving = False
                self.closed_loop[axis] = False
            elif cmd == "C":
                # Relative move
                self.closed_loop[axis] = True
                if not args:
                    self._output_buf += ":%d" % self.target_pos[axis]
                elif len(args) == 1:
                    self.target_pos[axis] += int(args[0])
                    steps = int(
                        int(args[0]) * DEFAULT_ENCODER_RESOLUTION /
                        DEFAULT_MOTORSTEP_RESOLUTION)
                    self.move(steps)
                elif len(args) == 2:
                    self.target_pos[axis] += int(args[0])
                    self.speed = int(args[1])
                    steps = int(
                        int(args[0]) * DEFAULT_ENCODER_RESOLUTION /
                        DEFAULT_MOTORSTEP_RESOLUTION)
                    self.move(steps)
                else:
                    raise ValueError()
            elif cmd == "E":
                if not args:
                    self._output_buf += ":%d" % self.current_pos[axis]
                elif len(args) == 1:
                    self.target_pos[axis] += int(args[0])
                else:
                    raise ValueError()
            elif cmd == "J":
                if not args:
                    if self.is_moving:
                        self._output_buf += ":222"
                    else:
                        self._output_buf += ":0"
                elif len(args) == 1:
                    pass  # simulate move
                elif len(args) == 2:
                    pass  # simulate move
                elif len(args) == 3:
                    # simulate move
                    self.speed = int(args[1])
                else:
                    raise ValueError()
            elif cmd == "I":
                self.find_index()
            elif cmd == "N":
                if self.indexing:
                    self._output_buf += "1,132.,indexed"
                else:
                    self._output_buf += "1,132"
            elif cmd == "Y":
                if len(args) == 1:
                    if int(args[0]) == 42:  # serial number
                        self._output_buf += ":12345678"
                    elif int(args[0]) == 11:  # spc parameter
                        self._output_buf += ":70000"
            elif cmd == "U":
                if self.is_moving:
                    # "0020" means targetMode (closed loop mode) active
                    self.status = "0020" if self.closed_loop[axis] else "0000"
                else:
                    # "0030" means targetMode (closed loop mode) active and targetReached, "0010" means targetReached
                    self.status = "0030" if self.closed_loop[axis] else "0010"
                self._output_buf += ":%s" % self.status
            else:
                # Syntax error is indicated by inserting _??_ in the response
                self._output_buf = self._output_buf[:-len(msg)]
                self._output_buf += "X%s_??_%s" % (
                    axis, ''.join(args))  # args can be str or list of str
                logging.error("Unknown command %s" % cmd)
        except ValueError as ex:
            # Assume something is wrong with the arguments
            self._output_buf = self._output_buf[:-len(msg)]
            self._output_buf += "X%s%s_??_" % (axis, cmd)
            logging.error("Parsing %s failed with exception %s" % (msg, ex))

        self._output_buf += sEOL

    def move(self, steps):
        # simple move, same duration for every length, don't care about speed
        self.executor.submit(self._do_move, steps)

    def find_index(self):
        t = Thread(target=self._do_indexing)
        t.start()

    def _do_indexing(self):
        self.indexing = True
        time.sleep(1)
        self.indexing = False

    def _do_move(self, steps):
        self.is_moving = True
        startt = time.time()
        dur = abs(steps / self.speed)
        # be a bit faster than the real hardware because the real hardware can move multiple axes at the same time
        dur /= 2
        while time.time() < startt + dur:
            if not self.is_moving:  # stopped
                return
            else:
                time.sleep(0.1)
        self.current_pos = copy.deepcopy(self.target_pos)
        self.is_moving = False
Ejemplo n.º 46
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()
Ejemplo n.º 47
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()
Ejemplo n.º 48
0
class PMD401Bus(Actuator):
    """
    This represents the PMD401 motor controller bus for the PiezoMotor LEGS motors. It supports multiple controllers
    (each one handling one axis), connected in a daisy chain. Only the first
    controller is directly connected to the computer.
    The specification for the hardware interface can be found in the document
     "PiezoMotor_PMD401_Technical_Manual.pdf".
    This driver works equally well with the PMD301 controller.
    """
    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)

    def terminate(self):
        # terminate can be called several times, do nothing if ._serial is already None
        if self._serial is None:
            return
        self._serial.close()
        self._serial = None
        for axis in self._axis_map.values():
            self.setWaveform(axis, WAVEFORM_PARK)  # power off

        super(PMD401Bus, self).terminate()

    def stop(self, axes=None):
        self._executor.cancel()
        axes = axes or self._axis_map.keys()
        for ax in axes:
            self.stopAxis(self._axis_map[ax])

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)
        shift = self._applyInversion(shift)
        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doMoveRel, f, shift)
        return f

    @isasync
    def moveAbs(self, pos):
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)
        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doMoveAbs, f, pos)
        return f

    @isasync
    def reference(self, axes):
        self._checkReference(axes)
        f = self._createMoveFuture()
        f = self._executor.submitf(f, self._doReference, f, axes)
        return f

    def _doReference(self, f, axes):
        self._check_hw_error()
        # Request referencing on all axes
        # Referencing procedure: index signal is in the middle of the rod (when using encoder)
        #   * move to the limit switch in the negative direction (fixed end of the rod)
        #   * once we reach the limit switch, PMD error 6 will be raised
        #   * move back in the opposite direction until indexing signal is registered

        # In case there is no encoder, it is still possible to reference. The motor has an internal indexing
        # signal 8.9 µm from fixed end of the rod (the end of the rod should be the most negative position if
        # the axis is not inverted). By referencing, this position can be set to 0 and absolute moves are possible
        # (although with less accuracy). However, we do not currently support this type of referencing without
        # encoder. With our hardware attached to the motor, it is impossible to reach the 8.9 µm indexing position.

        for axname in axes:
            if f._must_stop.is_set():
                self.stopAxis(self._axis_map[axname])
                raise CancelledError()
            axis = self._axis_map[axname]
            self.referenced._value[axname] = False

            # First, search for the index in negative direction.
            idx_found = self._search_index(f, axname, direction=-1)

            # If it wasn't found, try again in positive direction.
            if not idx_found:
                logging.debug("Referencing axis %s in the positive direction",
                              axis)
                idx_found = self._search_index(f, axname, direction=1)

            # If it's still not found, something went wrong.
            if not idx_found:
                raise ValueError(
                    "Couldn't find index on axis %s (%s), referencing failed."
                    % (axis, axname))

            # Referencing complete
            logging.debug("Finished referencing axis %s." % axname)
            self.stopAxis(
                axis
            )  # the axis should already be stopped, make sure for safety
            self.referenced._value[axname] = True

        # read-only so manually notify
        self.referenced.notify(self.referenced.value)
        self._updatePosition()

    def _search_index(self, f, axname, direction):
        """
        :param f (Future)
        :param axname (str): axis name (as seen by the user)
        :param direction (-1 or 1): -1 for negative direction (beginning of the rod), 1 for positive direction
        returns (bool): True if index was found, false if limit was reached
        raises PMDError for all other errors except limit exceeded error
        IOError in case of timeout
        """
        axis = self._axis_map[axname]
        maxdist = self._axes[axname].range[1] - self._axes[axname].range[
            0]  # complete rodlength
        steps = int(maxdist * self._steps_per_meter[axname])
        maxdur = maxdist / self._speed[axname] + 1
        end_time = time.time() + 2 * maxdur

        logging.debug("Searching for index in direction %s.", direction)
        self.startIndexMode(axis)
        self.moveToIndex(axis, steps * direction)

        index_found = False
        while not index_found:
            if f._must_stop.is_set():
                self.stopAxis(axis)
                raise CancelledError()
            # Check for timeout
            if time.time() > end_time:
                self.stopAxis(axis)  # exit index mode
                raise IOError(
                    "Timeout while waiting for end of motion on axis %s" %
                    axis)

            # Check if limit is reached
            try:
                self._check_hw_error()
            except PMDError as ex:
                if ex.errno == 6:  # external limit reached
                    logging.debug("Axis %d limit reached during referencing",
                                  axis)
                    self.stopAxis(
                        axis
                    )  # that seems to be necessary after reaching the limit
                    break
                else:
                    raise

            # Get index status
            index_found = self.getIndexStatus(self._axis_map[axname])[-1]
            time.sleep(0.05)

        return index_found

    def _doMoveAbs(self, f, pos):
        self._check_hw_error()
        self._updatePosition()
        current_pos = self._applyInversion(self.position.value)

        shifts = {}
        if f._must_stop.is_set():
            raise CancelledError()
        for axname, val in pos.items():
            if self._closed_loop[axname]:
                shifts[axname] = val - current_pos[axname]
                encoder_cnts = round(val * self._counts_per_meter[axname])
                self.runAbsTargetMove(self._axis_map[axname], encoder_cnts,
                                      self._speed_steps[axname])
            else:
                # No absolute move for open-loop => convert to relative move
                shifts[axname] = val - current_pos[axname]
                steps_float = shifts[axname] * self._steps_per_meter[axname]
                steps = int(steps_float)
                usteps = int((steps_float - steps) * USTEPS_PER_STEP)
                self.runMotorJog(self._axis_map[axname], steps, usteps,
                                 self._speed_steps[axname])

        try:
            self._waitEndMotion(f, shifts)
        finally:
            # Leave target mode in case of closed-loop move
            for ax in pos:
                self.stopAxis(self._axis_map[ax])
            self._updatePosition()

    def _doMoveRel(self, f, shift):
        self._check_hw_error()

        shifts = {}
        if f._must_stop.is_set():
            raise CancelledError()

        for axname, val in shift.items():
            if self._closed_loop[axname]:
                shifts[axname] = val
                encoder_cnts = val * self._counts_per_meter[axname]
                self.runRelTargetMove(self._axis_map[axname], encoder_cnts,
                                      self._speed_steps[axname])
            else:
                shifts[axname] = val
                steps_float = val * self._steps_per_meter[axname]
                steps = int(steps_float)
                usteps = int((steps_float - steps) * USTEPS_PER_STEP)
                self.runMotorJog(self._axis_map[axname], steps, usteps,
                                 self._speed_steps[axname])

        try:
            self._waitEndMotion(f, shifts)
        finally:
            # Leave target mode in case of closed-loop move
            for ax in shift:
                self.stopAxis(self._axis_map[ax])
            self._updatePosition()

    def _waitEndMotion(self, f, shifts):
        """
        Wait until move is done.
        :param f: (CancellableFuture) move future
        :param shifts: (dict: str --> floats) relative move (in m) between current position and previous position
        """
        dur = 0
        for ax, shift in shifts.items():
            dur = max(abs(shift / self._speed[ax]), dur)

        max_dur = dur * 2 + 1
        logging.debug("Expecting a move of %g s, will wait up to %g s", dur,
                      max_dur)

        end_time = time.time() + max_dur
        moving_axes = set(shifts.keys())  # All axes (still) moving
        logging.debug(f"Axes {moving_axes} are moving.")
        while moving_axes:
            if f._must_stop.is_set():
                for axname in moving_axes:
                    self.stopAxis(self._axis_map[axname])
                    raise CancelledError()

            if time.time() > end_time:
                raise TimeoutError(
                    "Timeout while waiting for end of motion on axes %s for %g s"
                    % (moving_axes, max_dur))

            for axname in moving_axes.copy(
            ):  # Copy as the set can change during the iteration
                axis = self._axis_map[axname]
                if self._closed_loop[axname]:
                    moving = self.isMovingClosedLoop(axis)
                    logging.debug(f"Axis {axis} is moving closed loop."
                                  ) if moving else None
                else:
                    moving = self.isMovingOpenLoop(axis)
                    logging.debug(f"Axis {axis} is moving open loop."
                                  ) if moving else None
                if not moving:
                    logging.debug(f"Axis {axis} finished moving.")
                    moving_axes.discard(axname)

            self._check_hw_error()
            time.sleep(0.05)
        logging.debug("All axis finished moving")

    def _check_hw_error(self):
        """
        Read hardware status and raise exception if error is detected.
        """
        for ax, axnum in self._axis_map.items():
            status = self.getStatus(axnum)
            # Always log the status
            logging.debug("Device status: %s", status)
            if status[0] & 8:
                raise PMDError(
                    1,
                    "Communication Error on axis %s (wrong baudrate, data collision, "
                    "or buffer overflow)" % ax)
            elif status[0] & 4:
                raise PMDError(
                    2,
                    "Encoder error on axis %s (serial communication or reported error from "
                    "serial encoder)" % ax)
            elif status[0] & 2:
                raise PMDError(
                    3,
                    "Supply voltage or motor fault was detected on axis %s." %
                    ax)
            elif status[0] & 1:
                raise PMDError(
                    4,
                    "Command timeout occurred or a syntax error was detected on axis %s when "
                    "response was not allowed." % ax)
            elif status[1] & 8:
                # That's really not a big deal since everything is working fine after power-on, so don't raise an error.
                logging.debug(
                    "Power-on/reset has occurred and detected on axis %s." %
                    ax)
            elif status[1] & 4:
                raise PMDError(
                    6, "External limit reached, detected on axis %s." % ax)

    def _updatePosition(self):
        """
        Update the position VA.
        """
        pos = {}
        for axname, axis in self._axis_map.items():
            # TODO: if not in closed-loop, it's probably because there is no encoder, so we need a different way
            pos[axname] = self.getEncoderPosition(axis)
        logging.debug("Reporting new position at %s", pos)
        pos = self._applyInversion(pos)
        self.position._set_value(pos, force_write=True)

    def stopAxis(self, axis):
        self._sendCommand(b'X%dS' % axis)

    def getVersion(self, axis):
        """
        :param axis: (int) axis number
        :returns (str): controller type and firmware version, e.g. 'PMD401 V13'
        """
        return self._sendCommand(b'X%d?' % axis)

    def getSerialNumber(self, axis):
        """
        :param axis: (int) axis number
        :returns (str): serial number
        """
        return self._sendCommand(b'X%dY42' % axis)

    def initFromFlash(self, axis):
        """
        Initialize settings from values stored in flash.
        :param axis: (int) axis number
        """
        # 2 for init from flash, 3 for factory values
        self._sendCommand(b'X%dY1,2' % axis)

    def setLimitType(self, axis, limit_type):
        """
        :param axis: (int) axis number
        :param limit_type: (0 <= int <= 3) 0 no limit, 1 active high, 2 active low
        """
        self._sendCommand(b'X%dY2,%d' % (axis, limit_type))

    def getEncoderPosition(self, axis):
        """
        :param axis: (int) axis number
        :returns (float): current position of the axis as reported by encoders (in m)
        """
        axname = [name for name, num in self._axis_map.items()
                  if num == axis][0]  # get axis name from number
        return int(self._sendCommand(
            b'X%dE' % axis)) / self._counts_per_meter[axname]

    def runRelTargetMove(self, axis, encoder_cnts, speed):
        """
        Closed-loop relative move.
        :param axis: (int) axis number
        :param encoder_cnts: (int)
        :param speed: (int) speed in motor steps per s
        """
        # There are two possibilities: move relative to current position (XC) and move relative to
        # target position (XR). We are moving relative to the current position (might be more intuitive
        # if something goes wrong and we're stuck in the wrong position).
        self._sendCommand(b'X%dC%d,%d' % (axis, encoder_cnts, speed))

    def runMotorJog(self, axis, motor_steps, usteps, speed):
        """
        Open loop stepping.
        :param axis: (int) axis number
        :param motor_steps: (int) number of motor steps to move
        :param usteps: (int) number of microsteps (1 motor step = 8192 microsteps)
        :param speed: (int) speed in steps / m
        """
        self._sendCommand(b'X%dJ%d,%d,%d' % (axis, motor_steps, usteps, speed))

    def runAbsTargetMove(self, axis, encoder_cnts, speed):
        """
        Closed loop move.
        :param axis: (int) axis number
        :param encoder_cnts: (int)
        :param speed: speed in motor steps per s
        """
        self._sendCommand(b'X%dT%d,%d' % (axis, encoder_cnts, speed))

    def setWaveform(self, axis, wf):
        """
        :param axis: (int) axis number
        :param wf: (WAVEFORM_RHOMB, WAVEFORM_DELTA, WAVEFORM_PARK) waveform to set
        """
        if wf not in (WAVEFORM_DELTA, WAVEFORM_RHOMB, WAVEFORM_PARK):
            raise ValueError("wf %s not a valid waveform" % wf)

        self._sendCommand(b'X%dM%d' % (axis, wf))

    def getStatus(self, axis):
        """
        :returns (list of 4 int): 4-bit status code
        The most important values are the following
        First bit:
        8: communication error (wrong baudrate, data collision, or buffer overflow)
        4: encoder error (serial communication or reported error from serial encoder)
        2: voltage error (supply voltage or motor fault was detected)
        1: command error (command timeout occurred or a syntax error was detected when response was not allowed)

        Second bit:
        8: reset (power on/ reset occurred)
        4: set if the last motor movement was stopped by external limit switch
        1: index (index signal was detected since last report)

        For all codes, please refer to the PMD-401 manual.
        """
        return [int(i) for i in self._sendCommand(b'X%dU0' % axis)]

    def isMovingClosedLoop(self, axis):
        """
        :param axis: (int) axis number
        :returns: (bool) True if moving, False otherwise
        """
        _, d2, d3, _ = self.getStatus(axis)
        # Check d2 (second status value) bit 2 (external limit)
        if d2 & 0b100:
            logging.debug(
                f"External limit reached on axis {axis}, current position is {self.position.value}"
            )
            raise PMDError(6, f"External limit reached on axis {axis}.")

        # Check d3 (third status value) bit 2 (targetLimit: position limit reached) and bit 0 (targetReached)
        if not d3 & 0b010:  # closed loop not active, thus it is not moving in closed loop
            logging.debug(
                f"Closed loop not active, therefore not moving in closed loop on axis {axis}."
            )
            return False
        elif d3 & 0b101:
            logging.debug(
                f"Target reached or position limit reached on axis {axis}.")
            return False
        else:
            return True

    def isMovingOpenLoop(self, axis):
        """
        :param axis: (int) axis number
        :returns: (bool) True if moving, False otherwise
        """
        resp = self._sendCommand(
            b'X%dJ' % axis
        )  # will be 0 if finished, otherwise +/-222 (contrary to manual!)
        return int(resp) != 0

    def startIndexMode(self, axis):
        """
        Enters index mode.
        """
        self._sendCommand(b'X%dN4' % axis)

    def moveToIndex(self, axis, dist):
        """
        Move towards the index until it's found.
        """
        axname = [name for name, num in self._axis_map.items()
                  if num == axis][0]  # get axis name from number
        self._sendCommand(b'X%dI%d,0,%d' %
                          (axis, dist, self._speed_steps[axname]))

    def getIndexStatus(self, axis):
        """
        Returns a description of the index status.
        :returns (tuple of 4):
            mode (0 or 1): index mode (1 if position has been reset at index)
            position (float):
            logged (bool): position was logged since last report
            indexed (bool): position has been reset at index
        """
        # Check if referencing was successful
        # Response looks like this: "1,132.,indexed"
        ret = self._sendCommand(b'X%dN?' % axis).split(',')
        try:
            mode = ret[0]
            if mode == '1':
                mode = 1
            else:  # empty string means mode 0
                mode = 0
            if ret[1][-1] == '.':
                # . means position was logged since last report
                logged = True
                position = ret[1][:-1]
            else:
                logged = False
                position = ret[1]
            if len(ret) > 2 and 'indexed' in ret[2]:
                indexed = True
            else:
                indexed = False
            return mode, position, logged, indexed
        except Exception as ex:
            logging.exception("Failed to parse index status %s, ex: %s", ret,
                              ex)
            raise

    def setAxisAddress(self, current_address, new_address):
        """
        Set the address of the axis. The factory default is 0 for all boards. Don't use this
        command if multiple axes with the same number are connected.
        :param current_address: (int) current axis number
        :param new_address: (int) new axis number
        """
        self._sendCommand(b"X%dY40,%d" % (current_address, new_address))

    def runAutoConf(self, axis):
        """
        Runs automatic configuration for the encoder parameters.
        :param axis: (int) axis number
        """
        self._sendCommand(b"X%dY25,1" % axis)

    def writeParamsToFlash(self, axis):
        self._sendCommand(b"X%dY32" % axis)

    def setParam(self, axis, param, value):
        self._sendCommand(b"X%dY%d,%d" % (axis, param, value))

    def readParam(self, axis, param):
        """
        :returns (str): parameter value from device
        """
        return self._sendCommand(b"X%dY%d" % (axis, param))

    def _createMoveFuture(self):
        """
        :returns: (CancellableFuture) a future that can be used to manage a move
        """
        f = CancellableFuture()
        f._moving_lock = threading.Lock()  # taken while moving
        f._must_stop = threading.Event(
        )  # cancel of the current future requested
        f._was_stopped = False  # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

    def _cancelCurrentMove(self, future):
        """
        Cancels the current move (both absolute or relative). Non-blocking.
        future (Future): the future to stop. Unused, only one future must be
         running at a time.
        return (bool): True if it successfully cancelled (stopped) the move.
        """
        # The difficulty is to synchronise correctly when:
        #  * the task is just starting (not finished requesting axes to move)
        #  * the task is finishing (about to say that it finished successfully)
        logging.debug("Cancelling current move")

        future._must_stop.set(
        )  # tell the thread taking care of the move it's over
        with future._moving_lock:
            if not future._was_stopped:
                logging.debug("Cancelling failed")
            return future._was_stopped

    def _sendCommand(self, cmd):
        """
        :param cmd: (bytes) command to be sent to the hardware
        :returns: (str) response
        """
        cmd += EOL
        with self._ser_access:
            logging.debug("Sending command %s", to_str_escape(cmd))
            try:
                self._serial.write(cmd)
            # TODO: what kind of exception is raised? Needs to be more specific.
            except:
                logging.warning("Failed to read from PMT Control firmware, "
                                "trying to reconnect.")
                if self._recovering:
                    raise
                else:
                    self._tryRecover()
                    # don't send command again
                    raise IOError("Failed to read from PMT Control firmware, "
                                  "restarted serial connection.")

            resp = b""
            while resp[-len(EOL):] != EOL:
                try:
                    char = self._serial.read()
                # TODO: what kind of exception is raised? Needs to be more specific.
                except:
                    logging.warning(
                        "Failed to read from PMT Control firmware, "
                        "trying to reconnect.")
                    if self._recovering:
                        raise
                    else:
                        self._tryRecover()
                        # don't send command again
                        raise IOError(
                            "Failed to read from PMT Control firmware, "
                            "restarted serial connection.")
                if not char:
                    raise IOError("Timeout after receiving %s" %
                                  to_str_escape(resp))
                else:
                    resp += char
            logging.debug("Received response %s", to_str_escape(resp))

            # Check response (command should be echoed back)
            if not resp.startswith(cmd[:-len(EOL) - 1]):
                raise IOError("Response starts with %s != %s" %
                              (to_str_escape(resp[:len(cmd)]), cmd))
            if b"_??_" in resp:
                raise ValueError(
                    "Received response %s, command %s not understood." %
                    (to_str_escape(resp), cmd))
            if b"!" in resp:
                raise PMDError(0, to_str_escape(resp))
            # Format:
            #    * for query with response: <cmd>:<ret><EOL> (will return <ret>)
            #    * for set command without response: <cmd><EOL> (will return "")
            return resp[len(cmd) + 1 - len(EOL):-len(EOL)].decode("latin1")

    def _tryRecover(self):
        self._recovering = True
        self.state._set_value(HwError("Connection lost, reconnecting..."),
                              force_write=True)
        # Retry to open the serial port (in case it was unplugged)
        # _ser_access should already be acquired, but since it's an RLock it can be acquired
        # again in the same thread
        try:
            with self._ser_access:
                while True:
                    if self._serial:
                        self._serial.close()
                    self._serial = None
                    try:
                        logging.debug("Searching for the device on port %s",
                                      self._portpattern)
                        min_axis = min(self._axis_map.values())
                        self._port = self._findDevice(self._portpattern,
                                                      min_axis)
                    except IOError:
                        time.sleep(2)
                    except Exception:
                        logging.exception(
                            "Unexpected error while trying to recover device")
                        raise
                    else:
                        # We found it back!
                        break
            # it now should be accessible again
            self.state._set_value(model.ST_RUNNING, force_write=True)
            logging.info("Recovered device on port %s", self._port)
        finally:
            self._recovering = False

    def _findDevice(self, port, address=0):
        """
        Look for a compatible device
        port (str): pattern for the port name
        address (None or int): the address of the stage controller
        return (serial, int): the (opened) serial port used, and the actual address
        raises:
            IOError: if no device are found
        """
        if port.startswith("/dev/fake"):
            names = [port]
        elif os.name == "nt":
            raise NotImplementedError("Windows not supported")
        else:
            names = glob.glob(port)

        for n in names:
            try:
                ser = self._openSerialPort(n)
            except IOError as ex:
                # not possible to use this port? next one!
                logging.info("Skipping port %s, which is not available (%s)",
                             n, ex)
                continue

            # check whether it answers with the right address
            try:
                # If any garbage was previously received, make it discarded.
                self._serial = ser
                self._serial.flush()
                v = self.getVersion(address)
                # The driver was writte for PMD401, but PMD301 has the same API and is more compatible with our hardware
                if 'PMD401 ' in v or 'PMD301 ' in v:
                    self._port = n
                    return ser  # found it!
            except Exception as ex:
                logging.debug(
                    "Port %s doesn't seem to have a PMD device connected: %s",
                    n, ex)
            ser.close()  # make sure to close/unlock that port
        else:
            raise IOError(
                "Failed to find a PMD controller with adress %s on ports '%s'. "
                "Check that the device is turned on and "
                "connected to the computer." % (
                    address,
                    port,
                ))

    @staticmethod
    def _openSerialPort(port):
        """
        Opens the given serial port the right way for a PiezoMotor PMD device.
        port (string): the name of the serial port (e.g., /dev/ttyUSB0)
        return (serial): the opened serial port
        raise HwError: if the serial port cannot be opened (doesn't exist, or
          already opened)
        """
        # For debugging purpose
        if port == "/dev/fake":
            return PMDSimulator(timeout=0.1)

        try:
            ser = serial.Serial(
                port=port,
                baudrate=BAUDRATE,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=0.3,  # s
            )
        except IOError:
            raise HwError("Failed to find a PMD controller on port '%s'. "
                          "Check that the device is turned on and "
                          "connected to the computer." % (port, ))

        return ser

    @classmethod
    def scan(cls):
        """
        returns (list of 2-tuple): name, kwargs
        Note: it's obviously not advised to call this function if a device is already under use
        """
        ports = [p.device for p in serial.tools.list_ports.comports()]

        logging.info("Scanning for Piezomotor controllers in progress...")
        found = []  # (list of 2-tuple): name, kwargs
        for p in ports:
            axes = {}
            for axis in range(5):  # most of the time, it's a small axis number
                try:
                    logging.debug("Trying port %s, axis %s", p, axis)
                    dev = cls(None, None, p, axes={"x": {"axis_number": axis}})
                except IOError:
                    # not possible to use this port? next one!
                    continue
                except Exception:
                    logging.exception("Error while communicating with port %s",
                                      p)
                    continue
                axes["a%s" % axis] = {"axis_number": axis}
                try:
                    ver = dev.getVersion(axis)
                except Exception as ex:
                    ver = "Unknown"
                    logging.error(
                        "Could not get version number for device on axis %s, ex: %s",
                        axis, ex)

            if axes:
                found.append(("%s" % ver, {"port": p, "axes": axes}))

        return found

    @staticmethod
    def parse_tsv_config(f):
        """
        Parse a tab-separated value (TSV) file in the following format:
          axis    param   value    # comment
          axis 0->127 (axis: number)
          param is the parameter number (int)
          value is a number (int))
        f (File): opened file
        return:
          axis_params (dict (int, int) -> int): axis number/param number -> value
        """
        axis_params = {}  # (axis/add) -> val (int)

        # read the parameters "database" the file
        for l in f:
            # comment or empty line?
            mc = re.match(r"\s*(#|$)", l)
            if mc:
                logging.debug("Comment line skipped: '%s'", l.rstrip("\n\r"))
                continue
            m = re.match(
                r"(?P<num>[0-9]+)\t(?P<param>[0-9]+)\t(?P<value>[-+]?[0-9]+)\s*(#.*)?$",
                l)
            if not m:
                raise ValueError("Failed to parse line '%s'" %
                                 l.rstrip("\n\r"))
            num, add, val = int(m.group("num")), int(m.group("param")), int(
                m.group("value"))
            axis_params[(num, add)] = val

        return axis_params

    def apply_params(self, params):
        for (axis, param), val in params.items():
            self.setParam(axis, param, val)
Ejemplo n.º 49
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()
Ejemplo n.º 50
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)
Ejemplo n.º 51
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, ))
Ejemplo n.º 52
0
class Chamber(model.Actuator):
    """
    Simulated chamber component. Just pretends to be able to change pressure
    """
    def __init__(self, name, role, positions, has_pressure=True, **kwargs):
        """
        Initialises the component
        positions (list of str): each pressure positions supported by the
          component (among the allowed ones)
        has_pressure (boolean): if True, has a pressure VA with the current
         pressure.
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?

        chp = {}
        for p in positions:
            try:
                chp[PRESSURES[p]] = p
            except KeyError:
                raise ValueError("Pressure position %s is unknown" % (p,))
        axes = {"pressure": model.Axis(unit="Pa", choices=chp)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_VENTED # last official position
        self._goal = PRESSURE_VENTED
        self._time_goal = 0 # time the goal was/will be reached
        self._time_start = 0 # time the move started

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    {"pressure": self._position},
                                    unit="Pa", readonly=True)
        if has_pressure:
            # Almost the same as position, but gives the current position
            self.pressure = model.VigilantAttribute(self._position,
                                        unit="Pa", readonly=True)

            self._press_timer = util.RepeatingTimer(1, self._updatePressure,
                                             "Simulated pressure update")
            self._press_timer.start()
        else:
            self._press_timer = None

        # Indicates whether the chamber is opened or not
        # Just pretend it's always closed, and allow the user to change that
        # for instance via CLI.
        self.opened = model.BooleanVA(False)

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

    def terminate(self):
        if self._press_timer:
            self._press_timer.cancel()
            self._press_timer = None

        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

    def _updatePressure(self):
        """
        update the pressure VA (called regularly from a thread)
        """
        # Compute the current pressure
        now = time.time()
        if self._time_goal < now: # done
            # goal ±5%
            pos = self._goal * random.uniform(0.95, 1.05)
        else:
            # TODO make it logarithmic
            ratio = (now - self._time_start) / (self._time_goal - self._time_start)
            pos = self._position + (self._goal - self._position) * ratio

        # it's read-only, so we change it via _value
        self.pressure._value = pos
        self.pressure.notify(pos)

    def _updatePosition(self):
        """
        update the position VA
        """
        # .position contains the last known/valid position
        # it's read-only, so we change it via _value
        self.position._value = {"pressure": self._position}
        self.position.notify(self.position.value)

    @isasync
    def moveRel(self, shift):
        self._checkMoveRel(shift)

        # convert into an absolute move
        pos = {}
        for a, v in shift.items:
            pos[a] = self.position.value[a] + v

        return self.moveAbs(pos)

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)

        new_pres = pos["pressure"]
        est_start = time.time() + 0.1
        f = model.ProgressiveFuture(start=est_start,
                                    end=est_start + self._getDuration(new_pres))

        return self._executor.submitf(f, self._changePressure, f, new_pres)

    def _getDuration(self, pos):
        return abs(self._position - pos) / SPEED_PUMP

    def _changePressure(self, f, p):
        """
        Synchronous change of the pressure
        p (float): target pressure
        """
        # TODO: allow to cancel during the change
        now = time.time()
        duration = self._getDuration(p) # s
        self._time_start = now
        self._time_goal = now + duration # s
        self._goal = p

        time.sleep(duration / 2)
        # DEBUG: for testing wrong time estimation
        # f.set_progress(start=self._time_start, end=self._time_goal + 10)
        time.sleep(duration / 2)

        self._position = p
        self._updatePosition()

    def stop(self, axes=None):
        self._executor.cancel()
        logging.warning("Stopped pressure change")
Ejemplo n.º 53
0
    def __init__(self, name, role, address, axes, stepsize, sn=None, **kwargs):
        """
        address (str): ip address (use "autoip" to automatically scan and find the
          controller, "fake" for a simulator)
        axes (list of str): names of the axes, from the 1st to the 4th, if present.
          if an axis is not connected, put a "".
        stepsize (list of float): size of a step in m (the smaller, the
          bigger will be a move for a given distance in m)
        sn (str or None): serial number of the device (eg, "11500"). If None, the
          driver will use whichever controller is first found.
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if not 1 <= len(axes) <= 4:
            raise ValueError(
                "Axes must be a list of 1 to 4 axis names (got %s)" % (axes, ))
        if len(axes) != len(stepsize):
            raise ValueError("Expecting %d stepsize (got %s)" %
                             (len(axes), stepsize))
        self._name_to_axis = {}  # str -> int: name -> axis number
        for i, n in enumerate(axes):
            if n == "":  # skip this non-connected axis
                continue
            self._name_to_axis[n] = i + 1

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

        self._address = address
        self._sn = sn
        self._accesser = self._openConnection(address, sn)
        self._recover = False

        self._resynchonise()

        if name is None and role is None:  # For scan only
            return

        # Seems to really be the device, so handle connection errors fully
        self._recover = True

        modl, fw, sn = self.GetIdentification()
        if modl != "8742":
            logging.warning("Controller %s is not supported, will try anyway",
                            modl)

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

        # Let the controller check the actuators are connected
        self.MotorCheck()

        axes_def = {}
        speed = {}
        for n, i in self._name_to_axis.items():
            sz = self._stepsize[i - 1]
            # TODO: allow to pass the range in m in the arguments
            # Position supports ±2³¹, probably not that much in reality, but
            # there is no info.
            rng = [(-2**31) * sz, (2**31 - 1) * sz]

            # Check the actuator is connected
            mt = self.GetMotorType(i)
            if mt in {MT_NONE, MT_UNKNOWN}:
                raise HwError(
                    "Controller failed to detect motor %d, check the "
                    "actuator is connected to the controller" % (i, ))
            max_stp_s = {MT_STANDARD: 2000, MT_TINY: 1750}[mt]
            srng = (0, self._speedToMS(i, max_stp_s))
            speed[n] = self._speedToMS(i, self.GetVelocity(i))

            axes_def[n] = model.Axis(range=rng, speed=srng, unit="m")

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        self._swVersion = "%s (IP connection)" % (odemis.__version__, )
        self._hwVersion = "New Focus %s (firmware %s, S/N %s)" % (modl, fw, sn)

        # Note that the "0" position is just the position at which the
        # controller turned on
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed,
                                        range=(0, max_speed),
                                        unit="m/s",
                                        setter=self._setSpeed)
Ejemplo n.º 54
0
class CoupledStage(model.Actuator):
    """
    Wrapper stage that takes as children the SEM sample stage and the
    ConvertStage. For each move to be performed CoupledStage moves, at the same
    time, both stages.
    """
    def __init__(self, name, role, children, **kwargs):
        """
        children (dict str -> actuator): names to ConvertStage and SEM sample stage
        """
        # SEM stage
        self._master = None
        # Optical stage
        self._slave = None

        for crole, child in children.items():
            # Check if children are actuators
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % child)
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % child.name)
            if "x" not in child.axes or "y" not in child.axes:
                raise ValueError("Child %s doesn't have both x and y axes" % child.name)

            if crole == "slave":
                self._slave = child
            elif crole == "master":
                self._master = child
            else:
                raise ValueError("Child given to CoupledStage must be either 'master' or 'slave', but got %s." % crole)

        if self._master is None:
            raise ValueError("CoupledStage needs a master child")
        if self._slave is None:
            raise ValueError("CoupledStage needs a slave child")

        # TODO: limit the range to the minimum of master/slave?
        axes_def = {}
        for an in ("x", "y"):
            axes_def[an] = copy.deepcopy(self._master.axes[an])
            axes_def[an].canUpdate = False

        model.Actuator.__init__(self, name, role, axes=axes_def, children=children,
                                **kwargs)
        self._metadata[model.MD_HW_NAME] = "CoupledStage"

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

        self._position = {}
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()
        # TODO: listen to master position to update the position? => but
        # then it might get updated too early, before the slave has finished
        # moving.

        self.referenced = model.VigilantAttribute({}, readonly=True)
        # listen to changes from children
        for c in self.children.value:
            if model.hasVA(c, "referenced"):
                logging.debug("Subscribing to reference of child %s", c.name)
                c.referenced.subscribe(self._onChildReferenced)
        self._updateReferenced()

        self._stage_conv = None
        self._createConvertStage()

    def updateMetadata(self, md):
        self._metadata.update(md)
        # Re-initialize ConvertStage with the new transformation values
        # Called after every sample holder insertion
        self._createConvertStage()

    def _createConvertStage(self):
        """
        (Re)create the convert stage, based on the metadata
        """
        self._stage_conv = ConvertStage("converter-xy", "align",
                    children={"aligner": self._slave},
                    axes=["x", "y"],
                    scale=self._metadata.get(MD_PIXEL_SIZE_COR, (1, 1)),
                    rotation=self._metadata.get(MD_ROTATION_COR, 0),
                    translation=self._metadata.get(MD_POS_COR, (0, 0)))

#         if set(self._metadata.keys()) & {MD_PIXEL_SIZE_COR, MD_ROTATION_COR, MD_POS_COR}:
#             # Schedule a null relative move, just to ensure the stages are
#             # synchronised again (if some metadata is provided)
#             self._executor.submit(self._doMoveRel, {})

    def _updatePosition(self):
        """
        update the position VA
        """
        mode_pos = self._master.position.value
        self._position["x"] = mode_pos['x']
        self._position["y"] = mode_pos['y']

        pos = self._applyInversion(self._position)
        self.position._set_value(pos, force_write=True)

    def _onChildReferenced(self, ref):
        # ref can be from any child, so we don't use it
        self._updateReferenced()

    def _updateReferenced(self):
        """
        update the referenced VA
        """
        ref = {} # str (axes name) -> boolean (is referenced)
        # consider an axis referenced iff it's referenced in every referenceable children
        for c in self.children.value:
            if not model.hasVA(c, "referenced"):
                continue
            cref = c.referenced.value
            for a in (set(self.axes.keys()) & set(cref.keys())):
                ref[a] = ref.get(a, True) and cref[a]

        self.referenced._set_value(ref, force_write=True)

    def _doMoveAbs(self, pos):
        """
        move to the position
        """
        f = self._master.moveAbs(pos)
        try:
            f.result()
        finally:  # synchronise slave position even if move failed
            # TODO: Move simultaneously based on the expected position, and
            # only if the final master position is different, move again.
            mpos = self._master.position.value
            # Move objective lens
            f = self._stage_conv.moveAbs({"x": mpos["x"], "y": mpos["y"]})
            f.result()

        self._updatePosition()

    def _doMoveRel(self, shift):
        """
        move by the shift
        """
        f = self._master.moveRel(shift)
        try:
            f.result()
        finally:
            mpos = self._master.position.value
            # Move objective lens
            f = self._stage_conv.moveAbs({"x": mpos["x"], "y": mpos["y"]})
            f.result()

        self._updatePosition()

    @isasync
    def moveRel(self, shift):
        if not shift:
            shift = {"x": 0, "y": 0}
        self._checkMoveRel(shift)

        shift = self._applyInversion(shift)
        return self._executor.submit(self._doMoveRel, shift)

    @isasync
    def moveAbs(self, pos):
        if not pos:
            pos = self.position.value
        self._checkMoveAbs(pos)
        pos = self._applyInversion(pos)

        return self._executor.submit(self._doMoveAbs, pos)

    def stop(self, axes=None):
        # Empty the queue for the given axes
        self._executor.cancel()
        self._master.stop(axes)
        self._stage_conv.stop(axes)
        logging.warning("Stopping all axes: %s", ", ".join(axes or self.axes))

    def _doReference(self, axes):
        fs = []
        for c in self.children.value:
            # only do the referencing for the stages that support it
            if not model.hasVA(c, "referenced"):
                continue
            ax = axes & set(c.referenced.value.keys())
            fs.append(c.reference(ax))

        # wait for all referencing to be over
        for f in fs:
            f.result()

        # Re-synchronize the 2 stages by moving the slave where the master is
        mpos = self._master.position.value
        f = self._stage_conv.moveAbs({"x": mpos["x"], "y": mpos["y"]})
        f.result()

        self._updatePosition()

    @isasync
    def reference(self, axes):
        if not axes:
            return model.InstantaneousFuture()
        self._checkReference(axes)
        return self._executor.submit(self._doReference, axes)

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None
Ejemplo n.º 55
0
    def __init__(self, name, role, port, axes=None, **kwargs):
        """
        A driver for a Newport ESP 301 Stage Actuator.
        This driver supports a serial connection. Note that as of the Linux
        kernel 4.13, the USB connection is known to _not_ work, as the TI 3410
        chipset apparently behind is not handled properly. Use a of the
        RS-232 port is required (via a USB adapter if necessary).

        name: (str)
        role: (str)
        port: (str) port name. Can be a pattern, in which case all the ports
          fitting the pattern will be tried.
          Use /dev/fake for a simulator
        axes: dict str (axis name) -> dict (axis parameters)
            axis parameters: {
                number (1 <= int <= 3): axis number on the hardware
                range: [float, float], default is -1 -> 1
                unit (str): the external unit of the axis (internal is mm),
                   default is "m".
                conv_factor (float): a conversion factor that converts to the
                   device internal unit (mm), default is 1000.
            }

        inverted: (bool) defines if the axes are inverted

        The offset can be specified by setting MD_POS_COR as a coordinate dictionary
        """

        if len(axes) == 0:
            raise ValueError("Needs at least 1 axis.")

        # Connect to serial port
        self._ser_access = threading.Lock()
        self._serial = None
        self._file = None
        self._port, self._version = self._findDevice(
            port)  # sets ._serial and ._file
        logging.info("Found Newport ESP301 device on port %s, Ver: %s",
                     self._port, self._version)

        self.LockKeypad(
            KEYPAD_LOCK_EXCEPT_STOP)  # lock user input for the controller

        # Clear errors at start
        try:
            self.checkError()
        except ESPError:
            pass

        self._offset = {}
        self._axis_conv_factor = {}

        # Not to be mistaken with axes which is a simple public view
        self._axis_map = {}  # axis name -> axis number used by controller
        axes_def = {}  # axis name -> Axis object
        speed = {}
        accel = {}
        decel = {}
        self._id = {}

        for axis_name, axis_par in axes.items():
            # Unpack axis parameters from the definitions in the YAML
            try:
                axis_num = axis_par['number']
            except KeyError:
                raise ValueError(
                    "Axis %s must have a number to identify it. " %
                    (axis_name, ))

            try:
                axis_range = axis_par['range']
            except KeyError:
                logging.info("Axis %s has no range. Assuming (-1, 1)",
                             axis_name)
                axis_range = (-1, 1)

            try:
                axis_unit = axis_par['unit']
            except KeyError:
                logging.info("Axis %s has no unit. Assuming m", axis_name)
                axis_unit = "m"

            try:
                conv_factor = float(axis_par['conv_factor'])
            except KeyError:
                logging.info(
                    "Axis %s has no conversion factor. Assuming 1000 (m to mm)",
                    axis_name)
                conv_factor = 1000.0

            self._axis_map[axis_name] = axis_num
            self._axis_conv_factor[axis_num] = conv_factor
            self._id[axis_num] = self.GetIdentification(axis_num)
            speed[axis_name] = self.GetSpeed(axis_num)
            accel[axis_name] = self.GetAcceleration(axis_num)
            decel[axis_name] = self.GetDeceleration(axis_num)

            # Force millimetres for consistency as the internal unit.
            self.SetAxisUnit(axis_num, "mm")
            # initialize each motor
            self.MotorOn(axis_num)

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

        model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs)

        # whether the axes are referenced
        self.referenced = model.VigilantAttribute({a: False
                                                   for a in axes},
                                                  readonly=True)

        self._hwVersion = str(self._id)
        self._swVersion = self._version

        # Get the position in object coord with the offset applied.

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()

        self._speed = speed
        self._accel = accel
        self._decel = decel

        # set offset due to mounting of components (float)
        self._metadata[model.MD_POS_COR] = {}

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

        # Check the error state
        self.checkError()
Ejemplo n.º 56
0
    def __init__(self, name, role, children, **kwargs):
        """
        children (dict str -> actuator): names to ConvertStage and SEM sample stage
        """
        # SEM stage
        self._master = None
        # Optical stage
        self._slave = None

        for crole, child in children.items():
            # Check if children are actuators
            if not isinstance(child, model.ComponentBase):
                raise ValueError("Child %s is not a component." % child)
            if not hasattr(child, "axes") or not isinstance(child.axes, dict):
                raise ValueError("Child %s is not an actuator." % child.name)
            if "x" not in child.axes or "y" not in child.axes:
                raise ValueError("Child %s doesn't have both x and y axes" % child.name)

            if crole == "slave":
                self._slave = child
            elif crole == "master":
                self._master = child
            else:
                raise ValueError("Child given to CoupledStage must be either 'master' or 'slave', but got %s." % crole)

        if self._master is None:
            raise ValueError("CoupledStage needs a master child")
        if self._slave is None:
            raise ValueError("CoupledStage needs a slave child")

        # TODO: limit the range to the minimum of master/slave?
        axes_def = {}
        for an in ("x", "y"):
            axes_def[an] = copy.deepcopy(self._master.axes[an])
            axes_def[an].canUpdate = False

        model.Actuator.__init__(self, name, role, axes=axes_def, children=children,
                                **kwargs)
        self._metadata[model.MD_HW_NAME] = "CoupledStage"

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

        self._position = {}
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()
        # TODO: listen to master position to update the position? => but
        # then it might get updated too early, before the slave has finished
        # moving.

        self.referenced = model.VigilantAttribute({}, readonly=True)
        # listen to changes from children
        for c in self.children.value:
            if model.hasVA(c, "referenced"):
                logging.debug("Subscribing to reference of child %s", c.name)
                c.referenced.subscribe(self._onChildReferenced)
        self._updateReferenced()

        self._stage_conv = None
        self._createConvertStage()
Ejemplo n.º 57
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()
Ejemplo n.º 58
0
class EbeamFocus(model.Actuator):
    """
    This is an extension of the model.Actuator class. It provides functions for
    adjusting the ebeam focus by changing the working distance i.e. the distance 
    between the end of the objective and the surface of the observed specimen 
    """
    def __init__(self, name, role, parent, axes, ranges=None, **kwargs):
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}

        # Just z axis
        a = axes[0]
        # The maximum, obviously, is not 1 meter. We do not actually care
        # about the range since Tescan API will adjust the value set if the
        # required one is out of limits.
        rng = [0, 1]
        axes_def[a] = model.Axis(unit="m", range=rng)

        # start at the centre
        self._position[a] = parent._device.GetWD() * 1e-3

        model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def, **kwargs)

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    self._applyInversionAbs(self._position),
                                    unit="m", readonly=True)

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

    def _updatePosition(self):
        """
        update the position VA
        """
        # it's read-only, so we change it via _value
        self.position._value = self._applyInversionAbs(self._position)
        self.position.notify(self.position.value)

    def _doMove(self, pos):
        """
        move to the position 
        """
        # Perform move through Tescan API
        # Position from m to mm and inverted
        self.parent._device.SetWD(self._position["z"] * 1e03)

        # Obtain the finally reached position after move is performed.
        with self.parent._acquisition_init_lock:
            wd = self.parent._device.GetWD()
            self._position["z"] = wd * 1e-3

        # Changing WD results to change in fov
        self.parent._scanner.updateHorizontalFOV()

        self._updatePosition()

    @isasync
    def moveRel(self, shift):
        if not shift:
            return model.InstantaneousFuture()
        self._checkMoveRel(shift)

        shift = self._applyInversionRel(shift)

        for axis, change in shift.items():
            self._position[axis] += change

        pos = self._position
        return self._executor.submit(self._doMove, pos)

    @isasync
    def moveAbs(self, pos):
        if not pos:
            return model.InstantaneousFuture()
        self._checkMoveAbs(pos)
        pos = self._applyInversionAbs(pos)

        for axis, new_pos in pos.items():
            self._position[axis] = new_pos

        pos = self._position
        return self._executor.submit(self._doMove, pos)

    def stop(self, axes=None):
        # Empty the queue for the given axes
        self._executor.cancel()
        logging.warning("Stopping all axes: %s", ", ".join(self.axes))

    def terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None