コード例 #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
コード例 #2
0
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

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

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

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
コード例 #3
0
    def __init__(self, name, role, axes, ranges=None, **kwargs):
        """
        axes (set of string): names of the axes
        """
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}
        init_speed = {}
        for a in axes:
            rng = ranges.get(a, [-0.1, 0.1])
            axes_def[a] = model.Axis(unit="m", range=rng, speed=[0., 10.])
            # start at the centre
            self._position[a] = (rng[0] + rng[1]) / 2
            init_speed[a] = 10.0  # we are super fast!

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

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

        self.speed = model.MultiSpeedVA(init_speed, [0., 10.], "m/s")
コード例 #4
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()
コード例 #5
0
ファイル: static.py プロジェクト: arijitxx/odemis
    def __init__(self, name, role, wlp, **kwargs):
        """
        wlp (list of floats): polynomial for conversion from distance from the 
          center of the CCD to wavelength (in m). So, typically, a first order
          polynomial contains as first element the center wavelength, and as
          second element the light dispersion (in m/m).
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        if not isinstance(wlp, list) or len(wlp) < 1:
            raise ValueError("wlp need to be a list of at least one float")

        self._swVersion = "N/A (Odemis %s)" % odemis.__version__
        self._hwVersion = name

        self._wlp = wlp
        pos = {"wavelength": self._wlp[0]}
        wla = model.Axis(range=(0, 2400e-9), unit="m")
        model.Actuator.__init__(self,
                                name,
                                role,
                                axes={"wavelength": wla},
                                **kwargs)
        self.position = model.VigilantAttribute(pos, unit="m", readonly=True)
コード例 #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
コード例 #7
0
ファイル: zeiss.py プロジェクト: Mahmood-B/odemis
    def __init__(self, name, role, parent, **kwargs):
        """
        axes (set of string): names of the axes
        """

        self.parent = parent

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

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

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition, "Focus position polling")
        self._pos_poll.start()
コード例 #8
0
ファイル: simulated.py プロジェクト: lanery/odemis
    def __init__(self, name, role, axes, ranges=None, **kwargs):
        """
        axes (set of string): names of the axes
        ranges (dict string -> float,float): min/max of the axis
        """
        assert len(axes) > 0
        if ranges is None:
            ranges = {}

        axes_def = {}
        self._position = {}
        init_speed = {}
        for a in axes:
            rng = ranges.get(a, (-0.1, 0.1))
            axes_def[a] = model.Axis(unit="m", range=rng, speed=(0., 10.))
            # start at the centre
            self._position[a] = (rng[0] + rng[1]) / 2
            init_speed[a] = 1.0  # we are fast!

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

        # Special file "stage.fail" => will cause simulation of hardware error
        if os.path.exists("stage.fail"):
            raise HwError("stage.fail file present, simulating error")

        self._executor = model.CancellableThreadPoolExecutor(max_workers=1)

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

        self.speed = model.MultiSpeedVA(init_speed, (0., 10.), "m/s")
コード例 #9
0
ファイル: static.py プロジェクト: ihebdelmic/odemis
    def __init__(self, name, role, wlp, children=None, **kwargs):
        """
        wlp (list of floats): polynomial for conversion from distance from the
          center of the CCD to wavelength (in m):
          w = wlp[0] + wlp[1] * x + wlp[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...). So, typically, a first order
          polynomial contains as first element the center wavelength, and as
          second element the light dispersion (in m/m)
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        if not isinstance(wlp, list) or len(wlp) < 1:
            raise ValueError("wlp need to be a list of at least one float")

        # Note: it used to need a "ccd" child, but not anymore
        self._swVersion = "N/A (Odemis %s)" % odemis.__version__
        self._hwVersion = name

        self._wlp = wlp
        pos = {"wavelength": self._wlp[0]}
        wla = model.Axis(range=(0, 2400e-9), unit="m")
        model.Actuator.__init__(self,
                                name,
                                role,
                                axes={"wavelength": wla},
                                **kwargs)
        self.position = model.VigilantAttribute(pos, unit="m", readonly=True)
コード例 #10
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
コード例 #11
0
ファイル: tlfw.py プロジェクト: PierreBizouard/odemis
    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]
        }
コード例 #12
0
    def __init__(self, name, role, port, axes, **kwargs):
        """
        port (string): name of the serial port to connect to the controllers
        axes (dict string=> 2-tuple (0<=int<=15, 0<=int<=1)): the configuration of the network.
         for each axis name the controller address and channel
         Note that even if it's made of several controllers, each controller is 
         _not_ seen as a child from the odemis model point of view.
        """

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

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

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

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

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

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

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

        self._action_mgr = ActionManager(self)
        self._action_mgr.start()
コード例 #13
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)
コード例 #14
0
ファイル: zeiss.py プロジェクト: effting/odemis
    def __init__(self, name, role, parent, rng=None, **kwargs):
        """
        inverted (set of str): names of the axes which are inverted
        rng (dict str -> (float,float)): axis name -> min/max of the position on
          this axis. Note: if the axis is inverted, the range passed will be
          inverted. Also, if the hardware reports position outside of the range,
          move might fail, as it is considered outside of the range.
        """

        if rng is None:
            rng = {}

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

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

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

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
コード例 #15
0
    def __init__(self, name, role, **kwargs):
        axes_def = {"z": model.Axis(unit="m", range=[-0.3, 0.3])}
        self._position = {"z": 0}

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

        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute(
                                    self._applyInversionAbs(self._position),
                                    unit="m", readonly=True)
コード例 #16
0
ファイル: mock.py プロジェクト: ihebdelmic/odemis
    def __init__(self,
                 name,
                 role,
                 _realcls,
                 parent=None,
                 children=None,
                 _vas=None,
                 daemon=None,
                 **kwargs):
        """
        _realcls (class): the class we pretend to be
        _vas (list of string): a list of mock vigilant attributes to create
        """
        model.HwComponent.__init__(self,
                                   name,
                                   role,
                                   daemon=daemon,
                                   parent=parent)
        if len(kwargs) > 0:
            logging.debug("Component '%s' got init arguments %r", name, kwargs)

        # Special handling of actuators, for actuator wrappers
        # Can not be generic for every roattribute, as we don't know what to put as value
        if issubclass(_realcls, model.Actuator):
            self.axes = {"x": model.Axis(range=[-1, 1])}
            # make them roattributes for proxy
            self._odemis_roattributes = ["axes"]

        if _vas is not None:
            for va in _vas:
                self.__dict__[va] = model.VigilantAttribute(None)

        if not children:
            children = {}

        cc = set()
        for child_name, child_args in children.items():
            # we don't care of child_name as it's only for internal use in the real component

            if isinstance(child_args, dict):  # delegation
                # the real class is unknown, so just give a generic one
                logging.debug("Instantiating mock child component %s",
                              child_name)
                child = MockComponent(_realcls=model.HwComponent,
                                      parent=self,
                                      daemon=daemon,
                                      **child_args)
            else:  # explicit creation (already done)
                child = child_args

            cc.add(child)

        # use explicit setter to be sure the changes are notified
        self.children.value = self.children.value | cc
コード例 #17
0
ファイル: static.py プロジェクト: ihebdelmic/odemis
    def __init__(self, name, role, band, **kwargs):
        """
        name (string): should be the name of the product (for metadata)
        band ((list of) 2-tuple of float > 0): (m) lower and higher bound of the
          wavelength of the light which goes _through_. If it's a list, it implies
          that the filter is multi-band.
        """
        # One enumerated axis: band
        # Create a 2-tuple or a set of 2-tuples
        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(tuple(b) for b in 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 > max_val or high > max_val:
                raise ValueError(
                    "Band contains very high values for light "
                    "wavelength, ensure the value is in meters: %r." % band)

        # TODO: have the position as the band value?
        band_axis = model.Axis(choices={0: band})

        model.Actuator.__init__(self,
                                name,
                                role,
                                axes={"band": band_axis},
                                **kwargs)
        self._swVersion = "N/A (Odemis %s)" % odemis.__version__
        self._hwVersion = name

        # Will always stay at position 0
        self.position = model.VigilantAttribute({"band": 0}, readonly=True)

        # TODO: MD_OUT_WL or MD_IN_WL depending on affect
        self._metadata = {model.MD_FILTER_NAME: name, model.MD_OUT_WL: band}
コード例 #18
0
ファイル: simulated.py プロジェクト: effting/odemis
    def __init__(self, name, role, positions, has_pressure=True, **kwargs):
        """
        Initialises the component
        positions (list of str): each pressure positions supported by the
          component (among the allowed ones)
        has_pressure (boolean): if True, has a pressure VA with the current
         pressure.
        """
        # TODO: or just provide .targetPressure (like .targetTemperature) ?
        # Or maybe provide .targetPosition: position that would be reached if
        # all the requested move were instantly applied?

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

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

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

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

        # will take care of executing axis move asynchronously
        self._executor = CancellableThreadPoolExecutor(
            max_workers=1)  # one task at a time
コード例 #19
0
    def __init__(self, name, role, parent, rng=None, **kwargs):
        if rng is None:
            rng = {}
        stage_info = parent.stage_info()
        if "x" not in rng:
            rng["x"] = stage_info["range"]["x"]
        if "y" not in rng:
            rng["y"] = stage_info["range"]["y"]
        if "z" not in rng:
            rng["z"] = stage_info["range"]["z"]

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

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

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

        # Refresh regularly the position
        self._pos_poll = util.RepeatingTimer(5, self._refreshPosition,
                                             "Position polling")
        self._pos_poll.start()
コード例 #20
0
    def __init__(self, name, role, port, axes, ustepsize, refproc=None, temp=False, **kwargs):
        """
        port (str): port name (use /dev/fake for a simulator)
        axes (list of str): names of the axes, from the 1st to the 3rd.
        ustepsize (list of float): size of a microstep in m (the smaller, the
          bigger will be a move for a given distance in m)
        refproc (str or None): referencing (aka homing) procedure type. Use
          None to indicate it's not possible (no reference/limit switch) or the
          name of the procedure. For now only "2xFinalForward" is accepted.
        temp (bool): if True, will read the temperature from the analogue input
         (10 mV <-> 1 °C)
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        """
        if len(axes) != 3:
            raise ValueError("Axes must be a list of 3 axis names (got %s)" % (axes,))
        self._axes_names = axes # axes names in order

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

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

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

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

        self._resynchonise()

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

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

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

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

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

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

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

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

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

        if temp:
            # One sensor is at the top, one at the bottom of the sample holder.
            # The most interesting is the temperature difference, so just
            # report both.
            self.temperature = model.FloatVA(0, unit=u"°C", readonly=True)
            self.temperature1 = model.FloatVA(0, unit=u"°C", readonly=True)
            self._temp_timer = util.RepeatingTimer(10, self._updateTemperatureVA,
                                                  "TMCM temperature update")
            self._updateTemperatureVA() # make sure the temperature is correct
            self._temp_timer.start()
コード例 #21
0
ファイル: tlaptmf.py プロジェクト: thomasaarholt/odemis
    def __init__(self, name, role, children=None, sn=None, port=None, axis="rz",
                 inverted=None, positions=None, **kwargs):
        """
        children (dict string->model.HwComponent): they are not actually used.
            This is currently in place just to enforce PMT control to be
            initialized before the Fiber Flipper since we need the relay reset
            to happen before the flipper is turned on.
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis)
        positions (None, or list of 2 tuples (value, str)): positions values and
         their corresponding name. If None: 0 and Pi/2 are used, without names.
        """
        if (sn is None and port is None) or (sn is not None and port is not None):
            raise ValueError("sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning("Serial number '%s' is unexpected for a MFF "
                                "device (should be 8 digits starting with %s).",
                                sn, SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
            self._sn = sn
        else:
            self._port = port
            # The MFF returns no serial number from GetInfo(), so find via USB
            try:
                self._sn = self._getSerialNumber(port)
                logging.info("Found serial number %s for device %s", self._sn, name)
            except LookupError:
                self._sn = None

        self._serial = self._openSerialPort(self._port)
        self._ser_access = threading.RLock()  # reentrant, so that recovery can keep sending messages
        self._recover = False
        self._initHw()

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

        if positions is None:
            positions = ((0, None), (math.radians(90), None))
        else:
            if len(positions) != 2 or any(len(p) != 2 for p in positions):
                raise ValueError("Positions must be exactly 2 tuples of 2 values")

        # TODO: have the standard inverted Actuator functions work on enumerated axis
        if inverted and axis in inverted:
            positions = (positions[1], positions[0])

        self._pos_to_jog = {positions[0][0]: 1,
                            positions[1][0]: 2}
        self._status_to_pos = {STA_FWD_HLS: positions[0][0],
                               STA_RVS_HLS: positions[1][0]}

        if positions[0][1] is None:
            choices = set(p[0] for p in positions)
        else:
            choices = dict(positions)

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

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name)
        try:
            snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        except IOError:
            # This is the first communication with the hardware, if it fails
            # it can be a sign the device is in a bad state. (it is known to
            # fail when turned on and plugged in before the host computer is
            # turned on)
            logging.exception("GetInfo() failed.")
            raise HwError("USB device with S/N %s seems in bad state. "
                          "Check that the Thorlabs filter flipper was "
                          "turned on *after* the host computer." % sn)
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

        # It has worked at least once, so if it fails, there are hopes
        self._recover = True

        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
コード例 #22
0
ファイル: andorshrk.py プロジェクト: PierreBizouard/odemis
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
コード例 #23
0
    def __init__(self,
                 name,
                 role,
                 port,
                 axes,
                 inverted=None,
                 param_file=None,
                 **kwargs):
        """
        :param axes (dict: str -> dict): axis name --> axis parameters
            Each axis is specified by a set of parameters.
            After successful configuration with the pmconfig.py script, the only required parameter for a default motor
            is the address which was set during the configuration process.
            The spc parameter (conversion between motor steps and encoder counts) is typically saved in the flash
            memory of the controller during the configuration process. The flash value is overridden by the
            value in the parameter dict.
            Depending on the type of motor, the encoder_resolution and range might need to be adjusted.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Write parameters from parameter file
        if param_file:
            if not os.path.isabs(param_file):
                param_file = os.path.join(os.path.dirname(__file__),
                                          param_file)
            try:
                f = open(param_file)
            except Exception as ex:
                raise ValueError("Failed to open file %s: %s" %
                                 (param_file, ex))
            try:
                axis_params = self.parse_tsv_config(f)
            except Exception as ex:
                raise ValueError("Failed to parse file %s: %s" %
                                 (param_file, ex))
            f.close()
            logging.debug("Extracted param file config: %s", axis_params)
            self.apply_params(axis_params)
コード例 #24
0
ファイル: tlaptmf.py プロジェクト: arijitxx/odemis
    def __init__(self,
                 name,
                 role,
                 sn=None,
                 port=None,
                 axis="rz",
                 inverted=None,
                 **kwargs):
        """
        sn (str): serial number (recommended)
        port (str): port name (only if sn is not specified)
        axis (str): name of the axis
        inverted (set of str): names of the axes which are inverted (IOW, either
         empty or the name of the axis) 
        """
        if (sn is None and port is None) or (sn is not None
                                             and port is not None):
            raise ValueError(
                "sn or port argument must be specified (but not both)")
        if sn is not None:
            if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8:
                logging.warning(
                    "Serial number '%s' is unexpected for a MFF "
                    "device (should be 8 digits starting with %s).", sn,
                    SN_PREFIX_MFF)
            self._port = self._getSerialPort(sn)
        else:
            self._port = port

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

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

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

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

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

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

        driver_name = driver.getSerialDriver(self._port)
        self._swVersion = "%s (serial driver: %s)" % (odemis.__version__,
                                                      driver_name)
        try:
            snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo()
        except IOError:
            # This is the first communication with the hardware, if it fails
            # it can be a sign the device is in a bad state. (it is known to
            # fail when turned on and plugged in before the host computer is
            # turned on)
            raise HwError("No USB device with S/N %s. "
                          "Check that the Thorlabs filter flipper was "
                          "turned on *after* the host computer." % sn)
        self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv)

        self.position = model.VigilantAttribute({}, readonly=True)
        self._updatePosition()
コード例 #25
0
ファイル: spectrapro.py プロジェクト: ihebdelmic/odemis
    def __init__(self,
                 name,
                 role,
                 port,
                 turret=None,
                 calib=None,
                 _noinit=False,
                 dependencies=None,
                 **kwargs):
        """
        port (string): name of the serial port to connect to.
        turret (None or 1<=int<=3): turret number set-up. If None, consider that
          the current turret known by the device is correct.
        calib (None or list of (int, int and 5 x (float or str))):
          calibration data, as saved by Winspec. Data can be either in float
          or as an hexadecimal value "hex:9a,99,99,99,99,79,40,40"
           blaze in nm, groove gl/mm, center adjust, slope adjust,
           focal length, inclusion angle, detector angle
        inverted (None): it is not allowed to invert the axes
        dependencies (dict str -> Component): "ccd" should be the CCD used to acquire
         the spectrum.
        _noinit (boolean): for internal use only, don't try to initialise the device
        """
        if kwargs.get("inverted", None):
            raise ValueError("Axis of spectrograph cannot be inverted")

        # start with this opening the port: if it fails, we are done
        try:
            self._serial = self.openSerialPort(port)
        except serial.SerialException:
            raise HwError(
                "Failed to find spectrograph %s (on port '%s'). "
                "Check the device is turned on and connected to the "
                "computer. You might need to turn it off and on again." %
                (name, port))
        self._port = port

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

        self._try_recover = False
        if _noinit:
            return

        self._initDevice()
        self._try_recover = True

        try:
            self._ccd = dependencies["ccd"]
        except (TypeError, KeyError):
            # TODO: only needed if there is calibration info (for the pixel size)
            # otherwise it's fine without CCD.
            raise ValueError("Spectrograph needs a dependency 'ccd'")

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

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

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

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

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

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

        # First step of parsing calib parmeter: convert to (int, int) -> ...
        calib = calib or ()
        if not isinstance(calib, collections.Iterable):
            raise ValueError("calib parameter must be in the format "
                             "[blz, gl, ca, sa, fl, ia, da], "
                             "but got %s" % (calib, ))
        dcalib = {}
        for c in calib:
            if not isinstance(c, collections.Iterable) or len(c) != 7:
                raise ValueError("calib parameter must be in the format "
                                 "[blz, gl, ca, sa, fl, ia, da], "
                                 "but got %s" % (c, ))
            gt = (c[0], c[1])
            if gt in dcalib:
                raise ValueError(
                    "calib parameter contains twice calibration for "
                    "grating (%d nm, %d gl/mm)" % gt)
            dcalib[gt] = c[2:]

        # store calibration for pixel -> wavelength conversion and wavelength offset
        # int (grating number 1 -> 9) -> center adjust, slope adjust,
        #     focal length, inclusion angle/2, detector angle
        self._calib = {}
        # TODO: read the info from MONO-EESTATUS (but it's so
        # huge that it's not fun to parse). There is also detector angle.
        dfl = FOCAL_LENGTH_OFFICIAL[model_name]  # m
        dia = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name])  # rad
        for i in gchoices:
            # put default values
            self._calib[i] = (0, 0, dfl, dia, 0)
            try:
                blz = self._getBlaze(i)  # m
                gl = self._getGrooveDensity(i)  # gl/m
            except ValueError:
                logging.warning("Failed to parse info of grating %d" % i,
                                exc_info=True)
                continue

            # parse calib info
            gt = (int(blz * 1e9), int(gl * 1e-3))
            if gt in dcalib:
                calgt = dcalib[gt]
                ca = self._readCalibVal(calgt[0])  # ratio
                sa = self._readCalibVal(calgt[1])  # ratio
                fl = self._readCalibVal(calgt[2]) * 1e-3  # mm -> m
                ia = math.radians(self._readCalibVal(calgt[3]))  # ° -> rad
                da = math.radians(self._readCalibVal(calgt[4]))  # ° -> rad
                self._calib[i] = ca, sa, fl, ia, da
                logging.info(
                    "Calibration data for grating %d (%d nm, %d gl/mm) "
                    "-> %s" % (i, gt[0], gt[1], self._calib[i]))
            else:
                logging.warning("No calibration data for grating %d "
                                "(%d nm, %d gl/mm)" % (i, gt[0], gt[1]))

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

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

        # for storing the latest calibrated wavelength value
        self._wl = (None, None, None
                    )  # grating id, raw center wl, calibrated center wl
        # RO, as to modify it the client must use .moveRel() or .moveAbs()
        self.position = model.VigilantAttribute({}, unit="m", readonly=True)
        self._updatePosition()
コード例 #26
0
ファイル: piezomotor.py プロジェクト: lanery/odemis
    def __init__(self, name, role, port, axes, inverted=None, **kwargs):
        """
        :param axes (dict: {"x", "y", "z"} --> dict): axis name --> axis parameters
            Each axis is specified by a set of parameters.
            After successful configuration with the pmconfig.py script, the only required parameter for a default motor
            is the address which was set during the configuration process.
            The spc parameter (conversion between motor steps and encoder counts) is typically saved in the flash
            memory of the controller during the configuration process. The flash value is overridden by the
            value in the parameter dict.
            Depending on the type of motor, the encoder_resolution and range might need to be adjusted.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self._resynchonise()

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

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

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

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

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

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

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

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

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

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

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

        max_speed = max(a.speed[1] for a in axes_def.values())
        self.speed = model.MultiSpeedVA(speed,
                                        range=(0, max_speed),
                                        unit="m/s",
                                        setter=self._setSpeed)
コード例 #29
0
ファイル: npmc.py プロジェクト: effting/odemis
    def __init__(self, name, role, port, axes=None, **kwargs):
        """
        A driver for a Newport ESP 301 Stage Actuator.
        This driver supports a serial connection. Note that as of the Linux
        kernel 4.13, the USB connection is known to _not_ work, as the TI 3410
        chipset apparently behind is not handled properly. Use a of the
        RS-232 port is required (via a USB adapter if necessary).

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

        inverted: (bool) defines if the axes are inverted

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Check the error state
        self.checkError()
コード例 #30
0
ファイル: simulated.py プロジェクト: effting/odemis
    def __init__(self, name, role, vas=None, axes=None, **kwargs):
        """
        Only VA's specified in vas are created. Their type is determined based on the supplied initial value, and the
        presence of range or choices in.
        Both the presence of VA's and the presence of axes are optional.

        vas (dict (string -> dict (string -> any))): This dict maps desired VA names to a dict with VA properties. The
        VA property dict can contain the following keys:
            "value" (any): initial values of the VA
            "readonly" (bool): optional, True for read only VA, defaults to False
            "unit" (str or None): optional, the unit of the VA, defaults to None
            "range" (float, float): optional, min/max of the VA. Incompatible with "choices".
            "choices" (list, set or dict): optional, possible values available to the VA.
               Incompatible with "range".
        axes (dict (string -> dict (string -> any))): dict mapping desired axis names to dicts with axis properties. The
        axis property dict can contain the following keys:
            "unit" (str): optional, unit of the axis, defaults to "m"
            "range" (float, float): optional, min/max of the axis, defaults to (-0.1, 0.1)
            "choices" (dict): optional, alternative to ranges, these are the choices of the axis
            "speed" (float, float): optional, allowable range of speeds, defaults to (0., 10.)

        """
        # Create desired VA's
        if vas:
            for vaname, vaprop in vas.items():
                # Guess an appropriate VA type based on the initial value and the presence of range or choices
                try:
                    value = vaprop["value"]
                except AttributeError:
                    # TODO: support "short-cut" by using a choice or range
                    raise AttributeError(
                        f"VA {vaname}, does not have a 'value' key.")

                if "choices" in vaprop:
                    if "range" in vaprop:
                        raise ValueError(
                            f"VA {vaname}, has both a range and choice, only one is possible."
                        )
                    # Always keep it simple as "VAEnumerated", it fits any type.
                    vaclass = model.VAEnumerated
                    # The "choices" argument can be either a dict or a set.
                    # However, YAML, doesn't supports set. So we accept list,
                    # and convert to a set.
                    if isinstance(vaprop["choices"], list):
                        vaprop["choices"] = set(vaprop["choices"])
                elif isinstance(value, str):
                    if "range" in vaprop:
                        raise ValueError("String doesn't support range")
                    vaclass = model.StringVA
                elif isinstance(value, bool):
                    if "range" in vaprop:
                        raise ValueError("Boolean doesn't support range")
                    vaclass = model.BooleanVA
                elif isinstance(value, float):
                    if "range" in vaprop:
                        vaclass = model.FloatContinuous
                    else:
                        vaclass = model.FloatVA
                elif isinstance(value, int):
                    if "range" in vaprop:
                        vaclass = model.IntContinuous
                    else:
                        vaclass = model.IntVA
                elif isinstance(value, Iterable):
                    # It's a little tricky because YAML only supports lists.
                    # So we guess a ListVA for the basic type (which is the most full-feature),
                    # and if there is a range, use TupleContinuous, as List doesn't
                    # support a range.
                    if "range" in vaprop:
                        vaclass = model.TupleContinuous
                    else:
                        vaclass = model.ListVA
                else:
                    raise ValueError(
                        f"VA {vaname}, has unsupported value type {value.__class__.__name__}."
                    )

                va = vaclass(**vaprop)
                setattr(self, vaname, va)

        # Create desired axes
        axes_def = {}
        if axes:
            self._position = {}
            init_speed = {}
            for axisname, axisprop in axes.items():
                init_speed[axisname] = 1.0  # we are fast!
                if "range" not in axisprop and "choices" not in axisprop:  # if no range nor choices are defined
                    axisprop["range"] = (-0.1, 0.1)  # use the default range
                if "speed" not in axisprop:
                    axisprop["speed"] = (0., 10.)  # default speed
                axes_def[axisname] = model.Axis(**axisprop)
                if "range" in axisprop:
                    self._position[axisname] = (
                        axisprop["range"][0] +
                        axisprop["range"][1]) / 2  # start at the centre
                else:
                    self._position[axisname] = next(iter(
                        axisprop["choices"]))  # start at an arbitrary value

            self._executor = model.CancellableThreadPoolExecutor(max_workers=1)

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

            self.speed = model.MultiSpeedVA(init_speed, (0., 10.), "m/s")

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

        if hasattr(self, "position"):
            self._updatePosition()