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

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

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

        self._resynchonise()

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

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

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

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

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

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

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

        axes = {"band": model.Axis(choices=bands)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

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

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

        self._speed = self.GetSpeed()

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

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {
            model.MD_FILTER_NAME: name,
            model.MD_OUT_WL: self._axes["band"].choices[curpos]
        }
Exemple #4
0
    def __init__(self, name, role, **kwargs):
        """
        Initialises the component
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?
        # TODO: support multiple pressures (low vacuum, high vacuum)
        axes = {
            "pressure":
            model.Axis(unit="Pa",
                       choices={
                           PRESSURE_VENTED: "vented",
                           PRESSURE_PUMPED: "vacuum"
                       })
        }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_PUMPED  # last official position
        self._goal = PRESSURE_PUMPED
        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)
        # 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
Exemple #5
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
Exemple #6
0
    def __init__(self, name, role, **kwargs):
        """
        Initialises the component
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?
        # TODO: support multiple pressures (low vacuum, high vacuum)
        axes = {"pressure": model.Axis(unit="Pa", 
                                       choices={PRESSURE_VENTED: "vented",
                                                PRESSURE_PUMPED: "vacuum"})}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_PUMPED # last official position
        self._goal = PRESSURE_PUMPED
        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)
        # 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
Exemple #7
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
Exemple #8
0
    def __init__(self, name, role, sn=None, port=None, axis="rz", inverted=None, **kwargs):
        """
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis) 
        """
        if (sn is None and port is None) or (sn is not None and port is not None):
            raise ValueError("sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning("Serial number '%s' is unexpected for a MFF "
                                "device (should be 8 digits starting with %s).",
                                sn, SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
        else:
            self._port = port

        self._serial = self._openSerialPort(self._port)
        self._ser_access = threading.Lock()

        # Ensure we don't receive anything
        self.SendMessage(HW_STOP_UPDATEMSGS)
        self._serial.flushInput()

        # Documentation says it should be done first, though it doesn't seem
        # required
        self.SendMessage(HW_NO_FLASH_PROGRAMMING)

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

        # TODO: have the standard inverted Actuator functions work on enumerated
        # use a different format than the standard Actuator
        if inverted and axis in inverted:
            self._pos_to_jog = {POS_UP: 2,
                                POS_DOWN: 1}
            self._status_to_pos = {STA_RVS_HLS: POS_UP,
                                   STA_FWD_HLS: POS_DOWN}
        else:
            self._pos_to_jog = {POS_UP: 1,
                                POS_DOWN: 2}
            self._status_to_pos = {STA_FWD_HLS: POS_UP,
                                   STA_RVS_HLS: POS_DOWN}

        # TODO: add support for speed
        axes = {axis: model.Axis(unit="rad",
                                 choices=set(self._pos_to_jog.keys()))
                }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
Exemple #9
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)
Exemple #10
0
    def test_multiple_cancellable(self):
        """
        Try to cancel multiple running cancellable futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)
Exemple #11
0
    def __init__(self, name, role, port, bands, _scan=False, **kwargs):
        """
        port (string): name of the serial port to connect to. Can be a pattern,
         in which case, all the ports fitting the pattern will be tried, and the
         first one which looks like an FW102C will be used.
        bands (dict 1<=int<=12 -> 2-tuple of floats > 0 or str):
          filter position -> lower and higher bound of the wavelength (m) of the
          light which goes _through_. If it's a list, it implies that the filter
          is multi-band.
        _scan (bool): only for internal usage
        raise IOError if no device answering or not a compatible device
        """
        self._ser_access = threading.Lock()
        self._port = self._findDevice(port)
        logging.info("Found FW102C device on port %s", self._port)
        if _scan:
            return
        
        # check bands contains correct data
        self._maxpos = self.GetMaxPosition() 
        if not bands:
            raise ValueError("Argument bands must contain at least one band")
        try:
            for pos, band in bands.items():
                if not 1 <= pos <= self._maxpos:
                    raise ValueError("Filter position should be between 1 and "
                                     "%d, but got %d." % (self._maxpos, pos))
                # To support "weird" filter, we accept strings
                if isinstance(band, basestring):
                    if not band.strip():
                        raise ValueError("Name of filter %d is empty" % pos)
                else:
                    self._checkBand(band)
        except Exception:
            logging.exception("Failed to parse bands %s", bands)
            raise
        
        axes = {"band": model.Axis(choices=bands)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        
        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        self._hwVersion = self._idn

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

        self._speed = self.GetSpeed()

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

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {model.MD_FILTER_NAME: name,
                          model.MD_OUT_WL: self._axes["band"].choices[curpos]}
Exemple #12
0
    def test_multiple_simple(self):
        """
        Try to cancel multiple running simple futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = self.executor.submit(self._task, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        cancelled = 0
        for f in fs:
            if f.cancelled():
                cancelled += 1
                self.assertRaises(CancelledError, f.result)
            else:
                self.assertGreaterEqual(f.result(), 1)  # should be a number

        self.assertGreaterEqual(cancelled, 10)
Exemple #13
0
    def test_one_cancellable(self):
        """
        Test cancelling multiple cancellable futures running one at a time
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=1)

        self.called = 0
        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            f.add_done_callback(self._on_end_task)
            fs.append(f)

        time.sleep(0.1)
        self.executor.cancel()
        self.assertEquals(self.called, 20)
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)
Exemple #14
0
    def test_multiple_cancellable(self):
        """
        Try to cancel multiple running cancellable futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)
Exemple #15
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
Exemple #16
0
    def test_multiple_simple(self):
        """
        Try to cancel multiple running simple futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = self.executor.submit(self._task, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        cancelled = 0
        for f in fs:
            if f.cancelled():
                cancelled += 1
                self.assertRaises(CancelledError, f.result)
            else:
                self.assertGreaterEqual(f.result(), 1) # should be a number

        self.assertGreaterEqual(cancelled, 10)
Exemple #17
0
    def test_one_cancellable(self):
        """
        Test cancelling multiple cancellable futures running one at a time
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=1)

        self.called = 0
        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            f.add_done_callback(self._on_end_task)
            fs.append(f)

        time.sleep(0.1)
        self.executor.cancel()
        self.assertEquals(self.called, 20)
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)
Exemple #18
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)
Exemple #19
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
Exemple #20
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:
                    self._checkBand(band)
        except Exception:
            logging.exception("Failed to parse bands %s", bands)
            raise

        axes = {"band": model.Axis(choices=bands)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

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

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

        self._speed = self.GetSpeed()

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

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {
            model.MD_FILTER_NAME: name,
            model.MD_OUT_WL: self._axes["band"].choices[curpos]
        }

    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 _checkBand(self, band):
        """
        band (object): should be tuple of floats or list of tuple of floats
        raise ValueError: if the band doesn't follow the convention
        """
        if not isinstance(band, collections.Iterable) or len(band) == 0:
            raise ValueError("band must be a (list of a) list of 2 floats")
        # is it a list of list?
        if isinstance(band[0], collections.Iterable):
            # => set of 2-tuples
            for sb in band:
                if len(sb) != 2:
                    raise ValueError(
                        "Expected only 2 floats in band, found %d" % len(sb))
            band = tuple(band)
        else:
            # 2-tuple
            if len(band) != 2:
                raise ValueError("Expected only 2 floats in band, found %d" %
                                 len(band))
            band = (tuple(band), )

        # Check the values are min/max and in m: typically within nm (< µm!)
        max_val = 10e-6  # m
        for low, high in band:
            if low > high:
                raise ValueError("Min of band must be first in list")
            if low < 0:
                raise ValueError("Band must be 2 positive value in meters")
            if low > max_val or high > max_val:
                raise ValueError(
                    "Band contains very high values for light "
                    "wavelength, ensure the value is in meters: %r." % band)

        # no error found

    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 IOError("No device seems to be an FW102C for ports '%s'" %
                          (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=1  #s
        )

        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
            HwError: 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)

                # 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 HwError("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
            HwError: 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._metadata[model.MD_OUT_WL] = self._axes["band"].choices[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:
            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(0, 8)]
            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
Exemple #21
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:
                    self._checkBand(band)
        except Exception:
            logging.exception("Failed to parse bands %s", bands)
            raise
        
        axes = {"band": model.Axis(choices=bands)}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        
        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        self._hwVersion = self._idn

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

        self._speed = self.GetSpeed()

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

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {model.MD_FILTER_NAME: name,
                          model.MD_OUT_WL: self._axes["band"].choices[curpos]}

    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 _checkBand(self, band):
        """
        band (object): should be tuple of floats or list of tuple of floats
        raise ValueError: if the band doesn't follow the convention
        """
        if not isinstance(band, collections.Iterable) or len(band) == 0:
            raise ValueError("band must be a (list of a) list of 2 floats")
        # is it a list of list?
        if isinstance(band[0], collections.Iterable):
            # => set of 2-tuples
            for sb in band:
                if len(sb) != 2:
                    raise ValueError("Expected only 2 floats in band, found %d" % len(sb))
            band = tuple(band)
        else:
            # 2-tuple
            if len(band) != 2:
                raise ValueError("Expected only 2 floats in band, found %d" % len(band))
            band = (tuple(band),)

        # Check the values are min/max and in m: typically within nm (< µm!)
        max_val = 10e-6 # m
        for low, high in band:
            if low > high:
                raise ValueError("Min of band must be first in list")
            if low < 0:
                raise ValueError("Band must be 2 positive value in meters")
            if low > max_val or high > max_val:
                raise ValueError("Band contains very high values for light "
                     "wavelength, ensure the value is in meters: %r." % band)

        # no error found
        
    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 IOError("No device seems to be an FW102C for ports '%s'" % (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=1 #s
        )

        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
            HwError: 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)
    
                # 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 HwError("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
            HwError: 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._metadata[model.MD_OUT_WL] = self._axes["band"].choices[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:
            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 (0,8)]
            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
Exemple #22
0
    def __init__(self, name, role, port, prot_time=1e-3, prot_curr=50e-6,
                 relay_cycle=None, powered=None, **kwargs):
        '''
        port (str): port name
        prot_time (float): protection trip time (in s)
        prot_curr (float): protection current threshold (in Amperes)
        relay_cycle (None or 0<float): if not None, will power cycle the relay
          with the given delay (in s)
        Raise an exception if the device cannot be opened
        '''
        if powered is None:
            powered = []
        self.powered = powered

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

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

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

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

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

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

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

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

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

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

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

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

        # Reset if no powered provided
        if not powered:
            self.setRelay(True)
        else:
            self._supplied = {}
            self.supplied = model.VigilantAttribute(self._supplied, readonly=True)
            self._updateSupplied()
Exemple #23
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=50e-6,
                 relay_cycle=None, powered=None, **kwargs):
        '''
        port (str): port name
        prot_time (float): protection trip time (in s)
        prot_curr (float): protection current threshold (in Amperes)
        relay_cycle (None or 0<float): if not None, will power cycle the relay
          with the given delay (in s)
        Raise an exception if the device cannot be opened
        '''
        if powered is None:
            powered = []
        self.powered = powered

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

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

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

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

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

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

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

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

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

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

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

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

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

    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 = 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("*IDN?")

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

        return self._getGain()

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

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

    def _getGain(self):
        ans = self._sendCommand("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("PWR 1")
        else:
            self._sendCommand("PWR 0")

        return value

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

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

        return value

    def _getProtection(self):
        ans = self._sendCommand("SWITCH?")
        return (ans == "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("RELAY 1")
        else:
            self._sendCommand("RELAY 0")

        return value

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

        return status

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

            ans = ''
            char = None
            while char != '\n':
                char = self._serial.read()
                if not char:
                    logging.error("Timeout after receiving %s", ans.encode('string_escape'))
                    # 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", ans.encode('string_escape'))
            if ans.startswith("ERROR"):
                raise PMTControlError(ans.split(' ', 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=5  # 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
        logging.debug("Nothing left to read, PMT Control Unit can safely initialize.")

        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
Exemple #24
0
class MFF(model.Actuator):
    """
    Represents one Thorlabs Motorized Filter Flipper (ie: MFF101 or MFF102)
    """
    def __init__(self, name, role, sn=None, port=None, axis="rz", inverted=None, **kwargs):
        """
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis) 
        """
        if (sn is None and port is None) or (sn is not None and port is not None):
            raise ValueError("sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning("Serial number '%s' is unexpected for a MFF "
                                "device (should be 8 digits starting with %s).",
                                sn, SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
        else:
            self._port = port

        self._serial = self._openSerialPort(self._port)
        self._ser_access = threading.Lock()

        # Ensure we don't receive anything
        self.SendMessage(HW_STOP_UPDATEMSGS)
        self._serial.flushInput()

        # Documentation says it should be done first, though it doesn't seem
        # required
        self.SendMessage(HW_NO_FLASH_PROGRAMMING)

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

        # TODO: have the standard inverted Actuator functions work on enumerated
        # use a different format than the standard Actuator
        if inverted and axis in inverted:
            self._pos_to_jog = {POS_UP: 2,
                                POS_DOWN: 1}
            self._status_to_pos = {STA_RVS_HLS: POS_UP,
                                   STA_FWD_HLS: POS_DOWN}
        else:
            self._pos_to_jog = {POS_UP: 1,
                                POS_DOWN: 2}
            self._status_to_pos = {STA_FWD_HLS: POS_UP,
                                   STA_RVS_HLS: POS_DOWN}

        # TODO: add support for speed
        axes = {axis: model.Axis(unit="rad",
                                 choices=set(self._pos_to_jog.keys()))
                }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

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

        # It'd be nice to know when a move is over, but it seems the MFF10x
        # never 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
        # always off without doing anything (cf MOT_SET_AVMODES)

    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 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

        logging.debug("Sending: '%s'", ", ".join("%02X" % ord(c) for c in com))
        with self._ser_access:
            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)

    # 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 timeout, 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 timeout, 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:
                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 = 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)
        # 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._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:
                raise ValueError("No USB device with S/N %s" % 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")

    @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
Exemple #25
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
Exemple #26
0
class SpectraPro(model.Actuator):
    def __init__(self, name, role, port, turret=None, _noinit=False, **kwargs):
        """
        port (string): name of the serial port to connect to.
        turret (None or 1<=int<=3): turret number set-up. If None, consider that
          the current turret known by the device is correct.
        inverted (None): it is not allowed to invert the axes
        _noinit (boolean): for internal use only, don't try to initialise the device
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        # start with this opening the port: if it fails, we are done
        self._serial = self.openSerialPort(port)
        self._port = port

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

        self._try_recover = False
        if _noinit:
            return

        self._initDevice()
        self._try_recover = True

        # according to the model determine how many gratings per turret
        model_name = self.GetModel()
        self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3)

        if turret is not None:
            if turret < 1 or turret > self.max_gratings:
                raise ValueError("Turret number given is %s, while expected a value between 1 and %d" %
                                 (turret, self.max_gratings))
            self.SetTurret(turret)
            self._turret = turret
        else:
            self._turret = self.GetTurret()

        # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
        max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s
        self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s",
                                        readonly=True)

        gchoices = self.GetGratingChoices()
        # remove the choices which are not valid for the current turret
        for c in gchoices:
            t = 1 + (c - 1) // self.max_gratings
            if t != self._turret:
                del gchoices[c]

        # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?)
        # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?)
        # cf http://www.roperscientific.de/gratingcalcmaster.html

        # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?)
        # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !?
        axes = {"wavelength": model.Axis(unit="m", range=(0, 2400e-9),
                                         speed=(max_speed, max_speed)),
                "grating": model.Axis(choices=gchoices)
                }
        # provides a ._axes
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver.getSerialDriver(port))
        self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown"))

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

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

        # store focal length and inclusion angle for the polynomial computation
        try:
            self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name]
            self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])
        except KeyError:
            self._focal_length = None
            self._inclusion_angle = None

    # 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

    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 val < 1 or 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 and 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 and 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.


    # 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.GetWavelength(),
                   "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.GetWavelength() + 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.SetWavelength(pos)
        self._updatePosition()

    def _doSetWavelengthAbs(self, pos):
        """
        Change the wavelength to a value
        """
        with self._ser_access:
            self.SetWavelength(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.SetWavelength(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 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
        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:
                model = self.GetModel()
                if not model.startswith("SP-"):
                    # accept it anyway
                    logging.warning("Device reports unexpected model '%s'", model)

                turret = self.GetTurret()
                if not turret in (1,2,3):
                    return False
                return True
        except:
            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 (0,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:
                # 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
Exemple #27
0
class Chamber(model.Actuator):
    """
    Simulated chamber component. Just pretends to be able to change pressure
    """
    def __init__(self, name, role, **kwargs):
        """
        Initialises the component
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?
        # TODO: support multiple pressures (low vacuum, high vacuum)
        axes = {
            "pressure":
            model.Axis(unit="Pa",
                       choices={
                           PRESSURE_VENTED: "vented",
                           PRESSURE_PUMPED: "vacuum"
                       })
        }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_PUMPED  # last official position
        self._goal = PRESSURE_PUMPED
        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)
        # 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 terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

    def _updatePosition(self):
        """
        update the position VA and .pressure VA
        """
        # 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)

        # .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
        """
        # TODO: allow to cancel during the change
        now = time.time()
        duration = 5  # s
        self._time_start = now
        self._time_goal = now + duration  # s
        self._goal = p

        time.sleep(duration)
        self._position = p
        self._updatePosition()

    def stop(self, axes=None):
        self._executor.cancel()
        logging.warning("Stopped pressure change")
Exemple #28
0
    def __init__(self,
                 name,
                 role,
                 sn=None,
                 port=None,
                 axis="rz",
                 inverted=None,
                 **kwargs):
        """
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis) 
        """
        if (sn is None and port is None) or (sn is not None
                                             and port is not None):
            raise ValueError(
                "sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning(
                    "Serial number '%s' is unexpected for a MFF "
                    "device (should be 8 digits starting with %s).", sn,
                    SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
        else:
            self._port = port

        self._serial = self._openSerialPort(self._port)
        self._ser_access = threading.Lock()

        # Ensure we don't receive anything
        self.SendMessage(HW_STOP_UPDATEMSGS)
        self._serial.flushInput()

        # Documentation says it should be done first, though it doesn't seem
        # required
        self.SendMessage(HW_NO_FLASH_PROGRAMMING)

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

        # TODO: have the standard inverted Actuator functions work on enumerated
        # use a different format than the standard Actuator
        if inverted and axis in inverted:
            self._pos_to_jog = {POS_UP: 2, POS_DOWN: 1}
            self._status_to_pos = {STA_RVS_HLS: POS_UP, STA_FWD_HLS: POS_DOWN}
        else:
            self._pos_to_jog = {POS_UP: 1, POS_DOWN: 2}
            self._status_to_pos = {STA_FWD_HLS: POS_UP, STA_RVS_HLS: POS_DOWN}

        # TODO: add support for speed
        axes = {
            axis: model.Axis(unit="rad", choices=set(self._pos_to_jog.keys()))
        }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver_name)
        snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
Exemple #29
0
class MFF(model.Actuator):
    """
    Represents one Thorlabs Motorized Filter Flipper (ie: MFF101 or MFF102)
    """
    def __init__(self,
                 name,
                 role,
                 sn=None,
                 port=None,
                 axis="rz",
                 inverted=None,
                 **kwargs):
        """
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis) 
        """
        if (sn is None and port is None) or (sn is not None
                                             and port is not None):
            raise ValueError(
                "sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning(
                    "Serial number '%s' is unexpected for a MFF "
                    "device (should be 8 digits starting with %s).", sn,
                    SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
        else:
            self._port = port

        self._serial = self._openSerialPort(self._port)
        self._ser_access = threading.Lock()

        # Ensure we don't receive anything
        self.SendMessage(HW_STOP_UPDATEMSGS)
        self._serial.flushInput()

        # Documentation says it should be done first, though it doesn't seem
        # required
        self.SendMessage(HW_NO_FLASH_PROGRAMMING)

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

        # TODO: have the standard inverted Actuator functions work on enumerated
        # use a different format than the standard Actuator
        if inverted and axis in inverted:
            self._pos_to_jog = {POS_UP: 2, POS_DOWN: 1}
            self._status_to_pos = {STA_RVS_HLS: POS_UP, STA_FWD_HLS: POS_DOWN}
        else:
            self._pos_to_jog = {POS_UP: 1, POS_DOWN: 2}
            self._status_to_pos = {STA_FWD_HLS: POS_UP, STA_RVS_HLS: POS_DOWN}

        # TODO: add support for speed
        axes = {
            axis: model.Axis(unit="rad", choices=set(self._pos_to_jog.keys()))
        }
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver_name)
        snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

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

        # It'd be nice to know when a move is over, but it seems the MFF10x
        # never 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
        # always off without doing anything (cf MOT_SET_AVMODES)

    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 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

        logging.debug("Sending: '%s'", ", ".join("%02X" % ord(c) for c in com))
        with self._ser_access:
            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)

    # 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 timeout, 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 timeout, 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:
                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 = 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)
        # 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._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:
                raise ValueError("No USB device with S/N %s" % 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")

    @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
Exemple #30
0
    def __init__(self, name, role, port, turret=None, _noinit=False, **kwargs):
        """
        port (string): name of the serial port to connect to.
        turret (None or 1<=int<=3): turret number set-up. If None, consider that
          the current turret known by the device is correct.
        inverted (None): it is not allowed to invert the axes
        _noinit (boolean): for internal use only, don't try to initialise the device
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        # start with this opening the port: if it fails, we are done
        self._serial = self.openSerialPort(port)
        self._port = port

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

        self._try_recover = False
        if _noinit:
            return

        self._initDevice()
        self._try_recover = True

        # according to the model determine how many gratings per turret
        model_name = self.GetModel()
        self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3)

        if turret is not None:
            if turret < 1 or turret > self.max_gratings:
                raise ValueError("Turret number given is %s, while expected a value between 1 and %d" %
                                 (turret, self.max_gratings))
            self.SetTurret(turret)
            self._turret = turret
        else:
            self._turret = self.GetTurret()

        # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
        max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s
        self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s",
                                        readonly=True)

        gchoices = self.GetGratingChoices()
        # remove the choices which are not valid for the current turret
        for c in gchoices:
            t = 1 + (c - 1) // self.max_gratings
            if t != self._turret:
                del gchoices[c]

        # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?)
        # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?)
        # cf http://www.roperscientific.de/gratingcalcmaster.html

        # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?)
        # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !?
        axes = {"wavelength": model.Axis(unit="m", range=(0, 2400e-9),
                                         speed=(max_speed, max_speed)),
                "grating": model.Axis(choices=gchoices)
                }
        # provides a ._axes
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)

        # set HW and SW version
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver.getSerialDriver(port))
        self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown"))

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

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

        # store focal length and inclusion angle for the polynomial computation
        try:
            self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name]
            self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])
        except KeyError:
            self._focal_length = None
            self._inclusion_angle = None
Exemple #31
0
class TMCM3110(model.Actuator):
    """
    Represents one Trinamic TMCM-3110 controller.
    Note: it must be set to binary communication mode (that's the default).
    """
    def __init__(self, name, role, port, axes, ustepsize, **kwargs):
        """
        port (str): port name (only if sn is not specified)
        axes (list of str): names of the axes, from the 1st to the 3rd.
        ustepsize (list of float): size of a microstep in m  
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if len(axes) != 3:
            raise ValueError("Axes must be a list of 3 axis names (got %s)" %
                             (axes, ))
        self._axes_names = axes  # axes names in order

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

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

        self._resynchonise()

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

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

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

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

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

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

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

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

    # Communication functions

    @staticmethod
    def _instr_to_str(instr):
        """
        instr (buffer of 9 bytes)
        """
        target, n, typ, mot, val, chk = struct.unpack('>BBBBiB', instr)
        s = "%d, %d, %d, %d, %d (%d)" % (target, n, typ, mot, val, chk)
        return s

    @staticmethod
    def _reply_to_str(rep):
        """
        rep (buffer of 9 bytes)
        """
        ra, rt, status, rn, rval, chk = struct.unpack('>BBBBiB', rep)
        s = "%d, %d, %d, %d, %d (%d)" % (ra, rt, status, rn, rval, chk)
        return s

    def _resynchonise(self):
        """
        Ensures the device communication is "synchronised"
        """
        with self._ser_access:
            self._serial.flushInput()
            garbage = self._serial.read(1000)
            if garbage:
                logging.debug("Received unexpected bytes '%s'", garbage)
            if len(garbage) == 1000:
                # Probably a sign that it's not the device we are expecting
                logging.warning("Lots of garbage sent from device")

            # In case the device has received some data before, resynchronise by
            # sending one byte at a time until we receive a reply.
            # On Ubuntu, when plugging the device, udev automatically checks
            # whether this is a real modem, which messes up everything immediately.
            # As there is no command 0, either we will receive a "wrong command" or
            # a "wrong checksum", but it will never do anything more.
            for i in range(9):  # a message is 9 bytes
                self._serial.write(b"\x00")
                self._serial.flush()
                res = self._serial.read(9)
                if len(res) == 9:
                    break  # just got synchronised
                elif len(res) == 0:
                    continue
                else:
                    logging.error(
                        "Device not answering with a 9 bytes reply: %s", res)
            else:
                logging.error("Device not answering to a 9 bytes message")

    def SendInstruction(self, n, typ=0, mot=0, val=0):
        """
        Sends one instruction, and return the reply.
        n (0<=int<=255): instruction ID
        typ (0<=int<=255): instruction type
        mot (0<=int<=255): motor/bank number
        val (0<=int<2**32): value to send
        return (0<=int<2**32): value of the reply (if status is good)
        raises:
            IOError: if problem with sending/receiving data over the serial port
            TMCLError: if status if bad
        """
        msg = numpy.empty(9, dtype=numpy.uint8)
        struct.pack_into('>BBBBiB', msg, 0, self._target, n, typ, mot, val, 0)
        # compute the checksum (just the sum of all the bytes)
        msg[-1] = numpy.sum(msg[:-1], dtype=numpy.uint8)
        with self._ser_access:
            logging.debug("Sending %s", self._instr_to_str(msg))
            self._serial.write(msg)
            self._serial.flush()
            while True:
                res = self._serial.read(9)
                if len(res) < 9:  # TODO: TimeoutError?
                    raise IOError("Received only %d bytes after %s" %
                                  (len(res), self._instr_to_str(msg)))
                logging.debug("Received %s", self._reply_to_str(res))
                ra, rt, status, rn, rval, chk = struct.unpack('>BBBBiB', res)

                # Check it's a valid message
                if rt != self._target:
                    logging.warning(
                        "Received a message from %d while expected %d", rt,
                        self._target)
                if rn != n:
                    logging.info(
                        "Skipping a message about instruction %d (waiting for %d)",
                        rn, n)
                    continue
                npres = numpy.frombuffer(res, dtype=numpy.uint8)
                good_chk = numpy.sum(npres[:-1], dtype=numpy.uint8)
                if chk != good_chk:
                    logging.warning(
                        "Message checksum incorrect (%d), skipping it", chk)
                    continue
                if not status in TMCL_OK_STATUS:
                    raise TMCLError(status, rval, self._instr_to_str(msg))

                return rval

    # Low level functions
    def GetVersion(self):
        """
        return (int, int, int): 
             Controller ID: 3110 for the TMCM-3110
             Firmware major version number
             Firmware minor version number
        """
        val = self.SendInstruction(136, 1)  # Ask for binary reply
        cont = val >> 16
        vmaj, vmin = (val & 0xff00) >> 8, (val & 0xff)
        return cont, vmaj, vmin

    def GetAxisParam(self, axis, param):
        """
        Read the axis/parameter setting from the RAM
        axis (0<=int<=2): axis number
        param (0<=int<=255): parameter number
        return (0<=int): the value stored for the given axis/parameter
        """
        val = self.SendInstruction(6, param, axis)
        return val

    def MoveAbsPos(self, axis, pos):
        """
        Requests a move to an absolute position. This is non-blocking.
        axis (0<=int<=2): axis number
        pos (-2**31 <= int 2*31-1): position
        """
        self.SendInstruction(4, 0, axis, pos)  # 0 = absolute

    def MoveRelPos(self, axis, offset):
        """
        Requests a move to a relative position. This is non-blocking.
        axis (0<=int<=2): axis number
        offset (-2**31 <= int 2*31-1): relative postion
        """
        self.SendInstruction(4, 1, axis, offset)  # 1 = relative

    def MotorStop(self, axis):
        self.SendInstruction(3, mot=axis)

    def ReferenceSearch(self, axis):
        self.SendInstruction(13, 0, axis)  # 0 = start

    def _isOnTarget(self, axis):
        """
        return (bool): True if the target position is reached
        """
        reached = self.GetAxisParam(axis, 8)
        return (reached != 0)

    # high-level methods (interface)
    def _updatePosition(self):
        """
        update the position VA
        """
        # TODO: allow to specify which axes to update (and other axes keep the current position)
        pos = {}
        for i, n in enumerate(self._axes_names):
            # param 1 = current position
            pos[n] = self.GetAxisParam(i, 1) * self._ustepsize[i]

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

    def _updateSpeed(self):
        """
        Update the speed VA from the controller settings
        """
        speed = {}
        # As described in section 3.4.1:
        #       fCLK * velocity
        # usf = ------------------------
        #       2**pulse_div * 2048 * 32
        for i, n in enumerate(self._axes_names):
            velocity = self.GetAxisParam(i, 4)
            pulse_div = self.GetAxisParam(i, 154)
            # fCLK = 16 MHz
            usf = (16e6 * velocity) / (2**pulse_div * 2048 * 32)
            speed[n] = usf * self._ustepsize[i]  # m/s

        # it's read-only, so we change it via _value
        self.speed._value = speed
        self.speed.notify(self.speed.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 = threading.Event()  # if cancel was successful
        f.task_canceller = self._cancelCurrentMove
        return f

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

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

        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._applyInversionRel(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
        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._axes_names.index(an)
                moving_axes.add(aid)
                usteps = int(round(v / self._ustepsize[aid]))
                self.MoveRelPos(aid, usteps)
                # compute expected end
                dur = abs(usteps) * self._ustepsize[aid] / 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
        pos (dict str -> float): axis name -> absolute target position
        """
        with future._moving_lock:
            end = 0  # expected end
            old_pos = self.position.value
            moving_axes = set()
            for an, v in pos.items():
                aid = self._axes_names.index(an)
                moving_axes.add(aid)
                usteps = int(round(v / self._ustepsize[aid]))
                self.MoveAbsPos(aid, usteps)
                # 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.
        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()
        try:
            while not future._must_stop.is_set():
                for aid in moving_axes.copy(
                ):  # need copy to remove during iteration
                    if self._isOnTarget(aid):
                        moving_axes.discard(aid)
                if not moving_axes:
                    # no more axes to wait for
                    return

                # Update the position from time to time (10 Hz)
                if time.time() - last_upd > 0.1:
                    self._updatePosition(
                    )  # TODO: only update the axes which moved since last time
                    last_upd = time.time()

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

            logging.debug("Move of axes %s cancelled before the end", axes)
            # stop all axes still moving them
            for i in moving_axes:
                self.MotorStop(i)
            future._was_stopped.set()
            raise CancelledError()
        finally:
            self._updatePosition()  # update 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 future._was_stopped.is_set():
                return True
            else:
                logging.debug("Cancelling failed")
                return False

    @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 TMCM3110Simulator(timeout=0.1)

        ser = serial.Serial(
            port=port,
            baudrate=9600,  # TODO: can be changed by RS485 setting p.85?
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=0.1  # s
        )

        return ser

    @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
        """
        # TODO: use serial.tools.list_ports.comports() (but only availabe in pySerial 2.6)
        if os.name == "nt":
            ports = ["COM" + str(n) for n in range(0, 8)]
        else:
            ports = glob.glob('/dev/ttyACM?*')

        logging.info("Scanning for TMCM controllers in progress...")
        found = []  # (list of 2-tuple): name, args (port, axes(channel -> CL?)
        for p in ports:
            try:
                logging.debug("Trying port %s", p)
                dev = cls(None,
                          None,
                          p,
                          axes=["x", "y", "z"],
                          ustepsize=[1e-6, 1e-6, 1e-6])
                modl, vmaj, vmin = dev.GetVersion()
            except (serial.SerialException, IOError):
                # not possible to use this port? next one!
                continue
            except Exception:
                logging.exception("Error while communicating with port %s", p)
                continue

            found.append(("TMCM-%s" % modl, {
                "port": p,
                "axes": ["x", "y", "z"],
                "ustepsize": [1e-6, 1e-6, 1e-6]
            }))

        return found
Exemple #32
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")
Exemple #33
0
class Chamber(model.Actuator):
    """
    Simulated chamber component. Just pretends to be able to change pressure
    """
    def __init__(self, name, role, **kwargs):
        """
        Initialises the component
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?
        # TODO: support multiple pressures (low vacuum, high vacuum)
        axes = {"pressure": model.Axis(unit="Pa", 
                                       choices={PRESSURE_VENTED: "vented",
                                                PRESSURE_PUMPED: "vacuum"})}
        model.Actuator.__init__(self, name, role, axes=axes, **kwargs)
        # For simulating moves
        self._position = PRESSURE_PUMPED # last official position
        self._goal = PRESSURE_PUMPED
        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)
        # 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 terminate(self):
        if self._executor:
            self.stop()
            self._executor.shutdown()
            self._executor = None

    def _updatePosition(self):
        """
        update the position VA and .pressure VA
        """
        # 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)

        # .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
        """
        # TODO: allow to cancel during the change
        now = time.time()
        duration = 5 # s
        self._time_start = now
        self._time_goal = now + duration # s
        self._goal = p

        time.sleep(duration)
        self._position = p
        self._updatePosition()

    def stop(self, axes=None):
        self._executor.cancel()
        logging.warning("Stopped pressure change")
Exemple #34
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
Exemple #35
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
Exemple #36
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")
Exemple #37
0
class TestExecutor(unittest.TestCase):

    def setUp(self):
        self.executor = None

    def tearDown(self):
        if self.executor:
            self.executor.shutdown(wait=True)

    def test_one_cancellable(self):
        """
        Test cancelling multiple cancellable futures running one at a time
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=1)

        self.called = 0
        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            f.add_done_callback(self._on_end_task)
            fs.append(f)

        time.sleep(0.1)
        self.executor.cancel()
        self.assertEquals(self.called, 20)
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)

    def test_multiple_simple(self):
        """
        Try to cancel multiple running simple futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = self.executor.submit(self._task, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        cancelled = 0
        for f in fs:
            if f.cancelled():
                cancelled += 1
                self.assertRaises(CancelledError, f.result)
            else:
                self.assertGreaterEqual(f.result(), 1) # should be a number

        self.assertGreaterEqual(cancelled, 10)

    def _task(self, dur):
        time.sleep(dur)
        return dur

    def test_multiple_cancellable(self):
        """
        Try to cancel multiple running cancellable futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)

    def _cancellable_task(self, future, dur=0):
        """
        Fake task
        future
        dur (float): time to wait
        return (float): dur
        """
        now = time.time()
        end = now + dur
        while now < end:
            left = end - now
            ms = future._must_stop.wait(max(0, left))
            if ms:
                raise CancelledError()
            now = time.time()
        return dur

    def _canceller(self, future):
        future._must_stop.set()
        # for now we assume cancel is always successful
        return True

    def _on_end_task(self, future):
        self.called += 1
Exemple #38
0
class Shamrock(model.Actuator):
    """
    Component representing the spectrograph part of the Andor Shamrock
    spectrometers.
    On Linux, only the SR303i is supported, via the I²C cable connected to the
    iDus. Support only works since SDK 2.97.
    Note: we don't handle changing turret.
    """
    def __init__(self, name, role, device, path=None, parent=None, **kwargs):
        """
        device (0<=int or "fake"): device number
        path (None or string): initialisation path of the Andorcam2 SDK or None
          if independent of a camera. If the path is set, a parent should also
          be passed, which is a DigitalCamera component.
        inverted (None): it is not allowed to invert the axes
        """
        # From the documentation:
        # If controlling the shamrock through i2c it is important that both the
        # camera and spectrograph are being controlled through the same calling
        # program and that the DLLs used are contained in the same working
        # folder. The camera MUST be initialized before attempting to
        # communicate with the Shamrock.
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        if device == "fake":
            self._dll = FakeShamrockDLL(parent)
            device = 0
        else:
            self._dll = ShamrockDLL()
        self._path = path or ""
        self._device = device

        try:
            self.Initialize()
        except ShamrockDLL, err:
            raise IOError("Failed to find Andor Shamrock (%s) as device %d" %
                          (name, device))
        try:
            nd = self.GetNumberDevices()
            if device >= nd:
                raise IOError(
                    "Failed to find Andor Shamrock (%s) as device %d" %
                    (name, device))

            if path is None or parent is None:
                raise NotImplementedError(
                    "Shamrock without parent a camera is not implemented")

            ccd = None
            if (parent and hasattr(parent, "_detector")
                    and isinstance(parent._detector, andorcam2.AndorCam2)):
                ccd = parent._detector
            self._hw_access = HwAccessMgr(ccd)

            # for now, it's fixed (and it's unlikely to be useful to allow less than the max)
            max_speed = 1000e-9 / 5  # about 1000 nm takes 5s => max speed in m/s
            self.speed = model.MultiSpeedVA({"wavelength": max_speed},
                                            range=[max_speed, max_speed],
                                            unit="m/s",
                                            readonly=True)

            gchoices = self._getGratingChoices()

            # Find lowest and largest wavelength reachable
            wl_range = (float("inf"), float("-inf"))
            for g in gchoices:
                wmin, wmax = self.GetWavelengthLimits(g)
                wl_range = min(wl_range[0], wmin), max(wl_range[1], wmax)

            # Slit (we only actually care about the input side slit for now)
            slits = {
                "input side": 1,
                "input direct": 2,
                "output side": 3,
                "output direct": 4,
            }
            for slitn, i in slits.items():
                logging.info("Slit %s is %spresent", slitn,
                             "" if self.AutoSlitIsPresent(i) else "not ")

            axes = {
                "wavelength":
                model.Axis(unit="m",
                           range=wl_range,
                           speed=(max_speed, max_speed)),
                "grating":
                model.Axis(choices=gchoices)
            }

            # add slit input direct if available
            # Note: the documentation mentions the width is in mm,
            # but it's probably actually µm (10 is the minimum).
            if self.AutoSlitIsPresent(INPUT_SLIT_SIDE):
                self._slit = INPUT_SLIT_SIDE
                axes["slit"] = model.Axis(
                    unit="m", range=[SLITWIDTHMIN * 1e-6, SLITWIDTHMAX * 1e-6])
            else:
                self._slit = None

            # provides a ._axes
            model.Actuator.__init__(self,
                                    name,
                                    role,
                                    axes=axes,
                                    parent=parent,
                                    **kwargs)

            # set HW and SW version
            self._swVersion = "%s" % (odemis.__version__)
            # TODO: EEPROM contains name of the device, but there doesn't seem to be any function for getting it?!
            self._hwVersion = "%s (s/n: %s)" % ("Andor Shamrock",
                                                self.GetSerialNumber())

            # 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()

        except Exception:
            self.Close()
            raise
Exemple #39
0
class TestExecutor(unittest.TestCase):
    def setUp(self):
        self.executor = None

    def tearDown(self):
        if self.executor:
            self.executor.shutdown(wait=True)

    def test_one_cancellable(self):
        """
        Test cancelling multiple cancellable futures running one at a time
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=1)

        self.called = 0
        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            f.add_done_callback(self._on_end_task)
            fs.append(f)

        time.sleep(0.1)
        self.executor.cancel()
        self.assertEquals(self.called, 20)
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)

    def test_multiple_simple(self):
        """
        Try to cancel multiple running simple futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = self.executor.submit(self._task, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        cancelled = 0
        for f in fs:
            if f.cancelled():
                cancelled += 1
                self.assertRaises(CancelledError, f.result)
            else:
                self.assertGreaterEqual(f.result(), 1)  # should be a number

        self.assertGreaterEqual(cancelled, 10)

    def _task(self, dur):
        time.sleep(dur)
        return dur

    def test_multiple_cancellable(self):
        """
        Try to cancel multiple running cancellable futures
        """
        self.executor = CancellableThreadPoolExecutor(max_workers=10)

        # Put several long task, and cancel all of them
        fs = []
        for i in range(20):
            f = CancellableFuture()
            f.task_canceller = self._canceller
            f._must_stop = threading.Event()
            f = self.executor.submitf(f, self._cancellable_task, f, 3 + i)
            fs.append(f)
        time.sleep(0.1)
        self.executor.cancel()
        for f in fs:
            self.assertTrue(f.cancelled())
            self.assertRaises(CancelledError, f.result)

    def _cancellable_task(self, future, dur=0):
        """
        Fake task
        future
        dur (float): time to wait
        return (float): dur
        """
        now = time.time()
        end = now + dur
        while now < end:
            left = end - now
            ms = future._must_stop.wait(max(0, left))
            if ms:
                raise CancelledError()
            now = time.time()
        return dur

    def _canceller(self, future):
        future._must_stop.set()
        # for now we assume cancel is always successful
        return True

    def _on_end_task(self, future):
        self.called += 1