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()
def __init__(self, name, role, port, axes, inverted=None, param_file=None, **kwargs): """ :param axes (dict: str -> dict): axis name --> axis parameters Each axis is specified by a set of parameters. After successful configuration with the pmconfig.py script, the only required parameter for a default motor is the address which was set during the configuration process. The spc parameter (conversion between motor steps and encoder counts) is typically saved in the flash memory of the controller during the configuration process. The flash value is overridden by the value in the parameter dict. Depending on the type of motor, the encoder_resolution and range might need to be adjusted. Axis parameters: axis_number (0 <= int <= 127): typically 1-3 for x-z, required closed_loop (bool): True for closed loop (with encoder), default to False encoder_resolution (float): number of encoder counts per meter, default to 1.22e-9 motorstep_resolution (float): number of motor steps per m, default to 5e-6 range (tuple of float): in m, default to STROKE_RANGE speed (float): speed in m/s unit (str), default to m :param param_file (str or None): (absolute or relative) path to a tmcm.tsv file which will be used to initialise the axis parameters. """ self._axis_map = {} # axis name -> axis number used by controller self._closed_loop = {} # axis name (str) -> bool (True if closed loop) self._speed = {} # axis name (str) -> speed in unit/s self._speed_steps = { } # axis name (str) -> int, speed in steps per meter self._counts_per_meter = {} # axis name (str) -> float self._steps_per_meter = {} # axis name (str) -> float self._portpattern = port # Parse axis parameters and create axis axes_def = {} # axis name -> Axis object for axis_name, axis_par in axes.items(): if 'axis_number' in axis_par: axis_num = axis_par['axis_number'] if axis_num not in range(128): raise ValueError( "Invalid axis number %s, needs to be 0 <= int <= 127." % axis_num) elif axis_num in self._axis_map.values(): axname = self._axis_map[axis_num] raise ValueError( "Invalid axis number %s, already assigned to axis %s." % (axis_num, axname)) else: self._axis_map[axis_name] = axis_par['axis_number'] else: raise ValueError("Axis %s has no axis number." % axis_name) if 'closed_loop' in axis_par: closed_loop = axis_par['closed_loop'] else: closed_loop = False logging.info( "Axis parameter \"closed_loop\" not specified for axis %s. Assuming open-loop.", axis_name) self._closed_loop[axis_name] = closed_loop if 'motorstep_resolution' in axis_par: self._steps_per_meter[ axis_name] = 1 / axis_par['motorstep_resolution'] else: self._steps_per_meter[ axis_name] = 1 / DEFAULT_MOTORSTEP_RESOLUTION logging.info( "Axis %s has no motorstep resolution, assuming %s.", axis_name, DEFAULT_MOTORSTEP_RESOLUTION) if 'encoder_resolution' in axis_par: self._counts_per_meter[ axis_name] = 1 / axis_par['encoder_resolution'] else: self._counts_per_meter[ axis_name] = 1 / DEFAULT_ENCODER_RESOLUTION logging.info("Axis %s has no encoder resolution, assuming %s.", axis_name, DEFAULT_ENCODER_RESOLUTION) if 'range' in axis_par: axis_range = [ float(axis_par['range'][0]), float(axis_par['range'][1]) ] else: axis_range = STROKE_RANGE logging.info("Axis %s has no range. Assuming %s", axis_name, axis_range) if 'speed' in axis_par: self._speed[axis_name] = axis_par['speed'] else: self._speed[axis_name] = DEFAULT_AXIS_SPEED logging.info( "Axis %s was not given a speed value. Assuming %s", axis_name, self._speed[axis_name]) self._speed_steps[axis_name] = int( round(self._speed[axis_name] * self._steps_per_meter[axis_name])) if 'unit' in axis_par: axis_unit = axis_par['unit'] else: axis_unit = "m" logging.info("Axis %s has no unit. Assuming %s", axis_name, axis_unit) ad = model.Axis(canAbs=closed_loop, unit=axis_unit, range=axis_range) axes_def[axis_name] = ad Actuator.__init__(self, name, role, axes=axes_def, inverted=inverted, **kwargs) self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time self._ser_access = threading.RLock() # Connect to hardware self._port = None # port number min_axis = min(self._axis_map.values()) self._serial = self._findDevice(port, min_axis) self._recovering = False # Get version hwVersions = [] for ax_name, ax_num in self._axis_map.items(): ver = self.getVersion(ax_num) sn = self.getSerialNumber(ax_num) hwVersions.append("Axis %s ('%s') version: %s, " % (ax_num, ax_name, ver) + "serial number: %s" % sn) self._hwVersion = ", ".join(hwVersions) logging.debug("Hardware versions: %s", hwVersions) # Configuration for axis in self._axis_map.values(): self.setWaveform(axis, WAVEFORM_DELTA) driver_name = getSerialDriver(self._port) self._swVersion = "Serial driver: %s" % (driver_name, ) # Position and referenced VAs self.position = model.VigilantAttribute({}, unit="m", readonly=True) self.referenced = model.VigilantAttribute({}, readonly=True) self._updatePosition() for axname in self._axis_map.keys(): self.referenced.value[ axname] = False # just assume they haven't been referenced self.speed = model.VigilantAttribute(self._speed, unit="m/s", readonly=True) # Write parameters from parameter file if param_file: if not os.path.isabs(param_file): param_file = os.path.join(os.path.dirname(__file__), param_file) try: f = open(param_file) except Exception as ex: raise ValueError("Failed to open file %s: %s" % (param_file, ex)) try: axis_params = self.parse_tsv_config(f) except Exception as ex: raise ValueError("Failed to parse file %s: %s" % (param_file, ex)) f.close() logging.debug("Extracted param file config: %s", axis_params) self.apply_params(axis_params)
def __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)
def __init__(self, name, role, port, turret=None, calib=None, _noinit=False, dependencies=None, **kwargs): """ port (string): name of the serial port to connect to. turret (None or 1<=int<=3): turret number set-up. If None, consider that the current turret known by the device is correct. calib (None or list of (int, int and 5 x (float or str))): calibration data, as saved by Winspec. Data can be either in float or as an hexadecimal value "hex:9a,99,99,99,99,79,40,40" blaze in nm, groove gl/mm, center adjust, slope adjust, focal length, inclusion angle, detector angle inverted (None): it is not allowed to invert the axes dependencies (dict str -> Component): "ccd" should be the CCD used to acquire the spectrum. _noinit (boolean): for internal use only, don't try to initialise the device """ if kwargs.get("inverted", None): raise ValueError("Axis of spectrograph cannot be inverted") # start with this opening the port: if it fails, we are done try: self._serial = self.openSerialPort(port) except serial.SerialException: raise HwError( "Failed to find spectrograph %s (on port '%s'). " "Check the device is turned on and connected to the " "computer. You might need to turn it off and on again." % (name, port)) self._port = port # to acquire before sending anything on the serial port self._ser_access = threading.Lock() self._try_recover = False if _noinit: return self._initDevice() self._try_recover = True try: self._ccd = dependencies["ccd"] except (TypeError, KeyError): # TODO: only needed if there is calibration info (for the pixel size) # otherwise it's fine without CCD. raise ValueError("Spectrograph needs a dependency 'ccd'") # according to the model determine how many gratings per turret model_name = self.GetModel() self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3) if turret is not None: if turret < 1 or turret > self.max_gratings: raise ValueError( "Turret number given is %s, while expected a value between 1 and %d" % (turret, self.max_gratings)) self.SetTurret(turret) self._turret = turret else: self._turret = self.GetTurret() # for now, it's fixed (and it's unlikely to be useful to allow less than the max) max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s", readonly=True) gchoices = self.GetGratingChoices() # remove the choices which are not valid for the current turret for c in gchoices: t = 1 + (c - 1) // self.max_gratings if t != self._turret: del gchoices[c] # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?) # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?) # cf http://www.roperscientific.de/gratingcalcmaster.html # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?) # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !? axes = { "wavelength": model.Axis(unit="m", range=(0, 2400e-9), speed=(max_speed, max_speed)), "grating": model.Axis(choices=gchoices) } # provides a ._axes model.Actuator.__init__(self, name, role, axes=axes, dependencies=dependencies, **kwargs) # First step of parsing calib parmeter: convert to (int, int) -> ... calib = calib or () if not isinstance(calib, collections.Iterable): raise ValueError("calib parameter must be in the format " "[blz, gl, ca, sa, fl, ia, da], " "but got %s" % (calib, )) dcalib = {} for c in calib: if not isinstance(c, collections.Iterable) or len(c) != 7: raise ValueError("calib parameter must be in the format " "[blz, gl, ca, sa, fl, ia, da], " "but got %s" % (c, )) gt = (c[0], c[1]) if gt in dcalib: raise ValueError( "calib parameter contains twice calibration for " "grating (%d nm, %d gl/mm)" % gt) dcalib[gt] = c[2:] # store calibration for pixel -> wavelength conversion and wavelength offset # int (grating number 1 -> 9) -> center adjust, slope adjust, # focal length, inclusion angle/2, detector angle self._calib = {} # TODO: read the info from MONO-EESTATUS (but it's so # huge that it's not fun to parse). There is also detector angle. dfl = FOCAL_LENGTH_OFFICIAL[model_name] # m dia = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name]) # rad for i in gchoices: # put default values self._calib[i] = (0, 0, dfl, dia, 0) try: blz = self._getBlaze(i) # m gl = self._getGrooveDensity(i) # gl/m except ValueError: logging.warning("Failed to parse info of grating %d" % i, exc_info=True) continue # parse calib info gt = (int(blz * 1e9), int(gl * 1e-3)) if gt in dcalib: calgt = dcalib[gt] ca = self._readCalibVal(calgt[0]) # ratio sa = self._readCalibVal(calgt[1]) # ratio fl = self._readCalibVal(calgt[2]) * 1e-3 # mm -> m ia = math.radians(self._readCalibVal(calgt[3])) # ° -> rad da = math.radians(self._readCalibVal(calgt[4])) # ° -> rad self._calib[i] = ca, sa, fl, ia, da logging.info( "Calibration data for grating %d (%d nm, %d gl/mm) " "-> %s" % (i, gt[0], gt[1], self._calib[i])) else: logging.warning("No calibration data for grating %d " "(%d nm, %d gl/mm)" % (i, gt[0], gt[1])) # set HW and SW version self._swVersion = "%s (serial driver: %s)" % ( odemis.__version__, driver.getSerialDriver(port)) self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown")) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time # for storing the latest calibrated wavelength value self._wl = (None, None, None ) # grating id, raw center wl, calibrated center wl # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute({}, unit="m", readonly=True) self._updatePosition()
def __init__(self, name, role, children, backlash, **kwargs): """ children (dict str -> Stage): dict containing one component, the stage to wrap backlash (dict str -> float): for each axis of the stage, the additional distance to move (and the direction). If an axis of the stage is not present, then it’s the same as having 0 as backlash (=> no antibacklash motion is performed for this axis) """ if len(children) != 1: raise ValueError("AntiBacklashActuator needs 1 child") for a, v in backlash.items(): if not isinstance(a, basestring): raise ValueError("Backlash key must be a string but got '%s'" % (a,)) if not isinstance(v, numbers.Real): raise ValueError("Backlash value of %s must be a number but got '%s'" % (a, v)) self._child = children.values()[0] self._backlash = backlash axes_def = {} for an, ax in self._child.axes.items(): axes_def[an] = copy.deepcopy(ax) axes_def[an].canUpdate = True if an in backlash and hasattr(ax, "range"): # Restrict the range to have some margin for the anti-backlash move rng = ax.range if rng[1] - rng[0] < abs(backlash[an]): raise ValueError("Backlash of %g m is bigger than range %s" % (backlash[an], rng)) if backlash[an] > 0: axes_def[an].range = (rng[0] + backlash[an], rng[1]) else: axes_def[an].range = (rng[0], rng[1] + backlash[an]) # Whether currently a backlash shift is applied on an axis # If True, moving the axis by the backlash value would restore its expected position # _shifted_lock must be taken before modifying this attribute self._shifted = {a: False for a in axes_def.keys()} self._shifted_lock = threading.Lock() # look for axes in backlash not existing in the child missing = set(backlash.keys()) - set(axes_def.keys()) if missing: raise ValueError("Child actuator doesn't have the axes %s" % (missing,)) model.Actuator.__init__(self, name, role, axes=axes_def, children=children, **kwargs) # will take care of executing axis moves asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time # Duplicate VAs which are just identical # TODO: shall we "hide" the antibacklash move by not updating position # while doing this move? self.position = self._child.position if model.hasVA(self._child, "referenced"): self.referenced = self._child.referenced if model.hasVA(self._child, "speed"): self.speed = self._child.speed
def __init__(self, name, role, children, axis_name, positions, cycle=None, **kwargs): """ name (string) role (string) children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis axis_name (str): axis name in the child actuator positions (set or dict value -> str): positions where the actuator is allowed to move cycle (float): if not None, it means the actuator does a cyclic move and this value represents a full cycle """ # TODO: forbid inverted if len(children) != 1: raise ValueError("FixedPositionsActuator needs precisely one child") self._cycle = cycle self._move_sum = 0 self._position = {} self._referenced = {} axis, child = children.items()[0] self._axis = axis self._child = child self._caxis = axis_name self._positions = positions # Executor used to reference and move to nearest position self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time if not isinstance(child, model.ComponentBase): raise ValueError("Child %s is not a component." % (child,)) if not hasattr(child, "axes") or not isinstance(child.axes, dict): raise ValueError("Child %s is not an actuator." % child.name) if cycle is not None: # just an offset to reference switch position self._offset = self._cycle / len(self._positions) if not all(0 <= p < cycle for p in positions.keys()): raise ValueError("Positions must be between 0 and %s (non inclusive)" % (cycle,)) ac = child.axes[axis_name] axes = {axis: model.Axis(choices=positions, unit=ac.unit)} # TODO: allow the user to override the unit? model.Actuator.__init__(self, name, role, axes=axes, children=children, **kwargs) self._position = {} self.position = model.VigilantAttribute({}, readonly=True) logging.debug("Subscribing to position of child %s", child.name) child.position.subscribe(self._update_child_position, init=True) if model.hasVA(child, "referenced") and axis_name in child.referenced.value: self._referenced[axis] = child.referenced.value[axis_name] self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True) child.referenced.subscribe(self._update_child_ref) # If the axis can be referenced => do it now (and move to a known position) # In case of cyclic move always reference if not self._referenced.get(axis, True) or (self._cycle and axis in self._referenced): # The initialisation will not fail if the referencing fails f = self.reference({axis}) f.add_done_callback(self._on_referenced) else: # If not at a known position => move to the closest known position nearest = util.find_closest(self._child.position.value[self._caxis], self._positions.keys()) self.moveAbs({self._axis: nearest}).result()
def __init__(self, name, role, children, **kwargs): """ children (dict str -> actuator): names to ConvertStage and SEM sample stage """ # SEM stage self._master = None # Optical stage self._slave = None for crole, child in children.items(): # Check if children are actuators if not isinstance(child, model.ComponentBase): raise ValueError("Child %s is not a component." % child) if not hasattr(child, "axes") or not isinstance(child.axes, dict): raise ValueError("Child %s is not an actuator." % child.name) if "x" not in child.axes or "y" not in child.axes: raise ValueError("Child %s doesn't have both x and y axes" % child.name) if crole == "slave": self._slave = child elif crole == "master": self._master = child else: raise ValueError("Child given to CoupledStage must be either 'master' or 'slave', but got %s." % crole) if self._master is None: raise ValueError("CoupledStage needs a master child") if self._slave is None: raise ValueError("CoupledStage needs a slave child") # TODO: limit the range to the minimum of master/slave? axes_def = {} for an in ("x", "y"): axes_def[an] = copy.deepcopy(self._master.axes[an]) axes_def[an].canUpdate = False model.Actuator.__init__(self, name, role, axes=axes_def, children=children, **kwargs) self._metadata[model.MD_HW_NAME] = "CoupledStage" # will take care of executing axis moves asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time self._position = {} # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute({}, unit="m", readonly=True) self._updatePosition() # TODO: listen to master position to update the position? => but # then it might get updated too early, before the slave has finished # moving. self.referenced = model.VigilantAttribute({}, readonly=True) # listen to changes from children for c in self.children.value: if model.hasVA(c, "referenced"): logging.debug("Subscribing to reference of child %s", c.name) c.referenced.subscribe(self._onChildReferenced) self._updateReferenced() self._stage_conv = None self._createConvertStage()
def __init__(self, name, role, locator, ref_on_init=False, actuator_speed=0.1, axes=None, **kwargs): """ A driver for a SmarAct SmarPod Actuator. This driver uses a DLL provided by SmarAct which connects via USB or TCP/IP using a locator string. name: (str) role: (str) locator: (str) Use "fake" for a simulator. For a real device, MCS controllers with USB interface can be addressed with the following locator syntax: usb:id:<id> where <id> is the first part of a USB devices serial number which is printed on the MCS controller. If the controller has a TCP/IP connection, use: network:<ip>:<port> ref_on_init: (bool) determines if the controller should automatically reference on initialization actuator_speed: (double) the default speed (in m/s) of the actuators axes: dict str (axis name) -> dict (axis parameters) axis parameters: { range: [float, float], default is -1 -> 1 unit: (str) default will be set to 'm' } """ if not axes: raise ValueError("Needs at least 1 axis.") if locator != "fake": self.core = SmarPodDLL() else: self.core = FakeSmarPodDLL() # 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 self._locator = c_char_p(locator.encode("ascii")) self._options = c_char_p("".encode( "ascii")) # In the current version, this must be an empty string. for axis_name, axis_par in axes.items(): 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" ad = model.Axis(canAbs=True, unit=axis_unit, range=axis_range) axes_def[axis_name] = ad # Connect to the device self._id = c_uint() self.core.Smarpod_Open(byref(self._id), SmarPodDLL.hwModel, self._locator, self._options) logging.debug("Successfully connected to SmarPod Controller ID %d", self._id.value) self.core.Smarpod_SetSensorMode(self._id, SmarPodDLL.SMARPOD_SENSORS_ENABLED) model.Actuator.__init__(self, name, role, axes=axes_def, **kwargs) # Add metadata self._swVersion = self.GetSwVersion() self._metadata[model.MD_SW_VERSION] = self._swVersion logging.debug("Using SmarPod library version %s", self._swVersion) self.position = model.VigilantAttribute({}, readonly=True) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(1) # one task at a time referenced = c_int() self.core.Smarpod_IsReferenced(self._id, byref(referenced)) # define the referenced VA from the query axes_ref = {a: referenced.value for a, i in self.axes.items()} # VA dict str(axis) -> bool self.referenced = model.VigilantAttribute(axes_ref, readonly=True) # If ref_on_init, referenced immediately. if referenced.value: logging.debug("SmarPod is referenced") else: logging.warning( "SmarPod is not referenced. The device will not function until referencing occurs." ) if ref_on_init: self.reference().result() # Use a default actuator speed self.SetSpeed(actuator_speed) self._speed = self.GetSpeed() self._accel = self.GetAcceleration() self._updatePosition()
def __init__(self, name, role, port, pin_map=None, delay=None, init=None, ids=None, termination=None, **kwargs): ''' port (str): port name pin_map (dict of str -> int): names of the components and the pin where the component is connected. delay (dict str -> float): time to wait for each component after it is turned on. init (dict str -> boolean): turn on/off the corresponding component upon initialization. ids (list str): EEPROM ids expected to be detected during initialization. termination (dict str -> bool/None): indicate for every component if it should be turned off on termination (False), turned on (True) or left as-is (None). Raise an exception if the device cannot be opened ''' if pin_map: self.powered = list(pin_map.keys()) else: self.powered = [] model.PowerSupplier.__init__(self, name, role, **kwargs) # TODO: catch errors and convert to HwError self._ser_access = threading.Lock() self._file = None self._port = self._findDevice(port) # sets ._serial and ._file logging.info("Found Power Control device on port %s", self._port) # Get identification of the Power control device self._idn = self._getIdentification() driver_name = driver.getSerialDriver(self._port) self._swVersion = "serial driver: %s" % (driver_name, ) self._hwVersion = "%s" % (self._idn, ) pin_map = pin_map or {} self._pin_map = pin_map delay = delay or {} # fill the missing pairs with 0 values self._delay = dict.fromkeys(pin_map, 0) self._delay.update(delay) self._last_start = dict.fromkeys(self._delay, time.time()) # only keep components that should be changed on termination termination = termination or {} self._termination = { k: v for k, v in termination.items() if v is not None } for comp in self._termination: if comp not in pin_map: raise ValueError( "Component %s in termination not found in pin_map." % comp) # will take care of executing switch asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time self._supplied = {} self.supplied = model.VigilantAttribute(self._supplied, readonly=True) self._updateSupplied() init = init or {} # Remove all None's from the dict, so it can be passed as-is to _doSupply() init = {k: v for k, v in init.items() if v is not None} for comp in init: if comp not in pin_map: raise ValueError("Component %s in init not found in pin_map." % comp) try: self._doSupply(init, apply_delay=False) except IOError as ex: # This is in particular to handle some cases where the device resets # when turning on the power. One or more trials and the logging.exception("Failure during turning on initial power.") raise HwError( "Device error when initialising power: %s. " "Try again or contact support if the problem persists." % (ex, )) self.memoryIDs = model.VigilantAttribute(None, readonly=True, getter=self._getIdentities) if ids: mem_ids = self.memoryIDs.value for eid in ids: if eid not in mem_ids: raise HwError("EEPROM id %s was not detected. Make sure " "all EEPROM components are connected." % (eid, ))
def __init__(self, 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._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) self._hwVersion = str(self._id) self._swversion = self._version # Get the position in object coord with the offset applied. # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute({}, readonly=True) self._updatePosition() self._speed = speed self._accel = accel self._decel = decel # set offset due to mounting of components (float) self._metadata[model.MD_POS_COR] = {} # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(1) # one task at a time # Check the error state self.checkError()
def __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()
def __init__(self, name, role, device, path=None, camera=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. camera (None or AndorCam2): Required 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(camera) device = 0 else: self._dll = ShamrockDLL() self._device = device if (path is None) != (camera is None): raise ValueError( "Shamrock needs both path and parent (a camera) or none of them" ) self._path = path or "" self._camera = camera try: self.Initialize() except ShamrockError: raise HwError("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)) ccd = None if (camera and hasattr(camera, "_detector") and isinstance(camera._detector, andorcam2.AndorCam2)): ccd = camera._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) # FIXME: for now the SDK 2.99 with SR193, commands will fail if not # separated by some delay (eg, 1s) gchoices = self._getGratingChoices() # The actual limits are per grating. We cannot provide this much # info via the .axes attribute, so just lowest and largest # wavelength reachable wl_range = (float("inf"), float("-inf")) for g in gchoices: try: wmin, wmax = self.GetWavelengthLimits(1) except ShamrockError: logging.exception( "Failed to find wavelength limit for grating %d", g) continue 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, **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
def __init__(self, name, role, port, turret=None, _noinit=False, **kwargs): """ port (string): name of the serial port to connect to. turret (None or 1<=int<=3): turret number set-up. If None, consider that the current turret known by the device is correct. inverted (None): it is not allowed to invert the axes _noinit (boolean): for internal use only, don't try to initialise the device """ if kwargs.get("inverted", None): raise ValueError("Axis of spectrograph cannot be inverted") # start with this opening the port: if it fails, we are done try: self._serial = self.openSerialPort(port) except serial.SerialException: raise HwError("Failed to find spectrograph %s (on port '%s'). " "Check the device is turned on and connected to the " "computer. You might need to turn it off and on again." % (name, port)) self._port = port # to acquire before sending anything on the serial port self._ser_access = threading.Lock() self._try_recover = False if _noinit: return self._initDevice() self._try_recover = True # according to the model determine how many gratings per turret model_name = self.GetModel() self.max_gratings = MAX_GRATINGS_NUM.get(model_name, 3) if turret is not None: if turret < 1 or turret > self.max_gratings: raise ValueError("Turret number given is %s, while expected a value between 1 and %d" % (turret, self.max_gratings)) self.SetTurret(turret) self._turret = turret else: self._turret = self.GetTurret() # for now, it's fixed (and it's unlikely to be useful to allow less than the max) max_speed = 1000e-9 / 10 # about 1000 nm takes 10s => max speed in m/s self.speed = model.MultiSpeedVA(max_speed, range=[max_speed, max_speed], unit="m/s", readonly=True) gchoices = self.GetGratingChoices() # remove the choices which are not valid for the current turret for c in gchoices: t = 1 + (c - 1) // self.max_gratings if t != self._turret: del gchoices[c] # TODO: report the grating with its wavelength range (possible to compute from groove density + blaze wl?) # range also depends on the max grating angle (40°, CCD pixel size, CCD horizontal size, focal length,+ efficienty curve?) # cf http://www.roperscientific.de/gratingcalcmaster.html # TODO: a more precise way to find the maximum wavelength (looking at the available gratings?) # TODO: what's the min? 200nm seems the actual min working, although wavelength is set to 0 by default !? axes = {"wavelength": model.Axis(unit="m", range=(0, 2400e-9), speed=(max_speed, max_speed)), "grating": model.Axis(choices=gchoices) } # provides a ._axes model.Actuator.__init__(self, name, role, axes=axes, **kwargs) # set HW and SW version self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver.getSerialDriver(port)) self._hwVersion = "%s (s/n: %s)" % (model_name, (self.GetSerialNumber() or "Unknown")) # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time pos = {"wavelength": self.GetWavelength(), "grating": self.GetGrating()} # RO, as to modify it the client must use .moveRel() or .moveAbs() self.position = model.VigilantAttribute(pos, unit="m", readonly=True) # store focal length and inclusion angle for the polynomial computation try: self._focal_length = FOCAL_LENGTH_OFFICIAL[model_name] self._inclusion_angle = math.radians(INCLUSION_ANGLE_OFFICIAL[model_name]) except KeyError: self._focal_length = None self._inclusion_angle = None
def __init__(self, name, role, children=None, sn=None, port=None, axis="rz", inverted=None, positions=None, **kwargs): """ children (dict string->model.HwComponent): they are not actually used. This is currently in place just to enforce PMT control to be initialized before the Fiber Flipper since we need the relay reset to happen before the flipper is turned on. sn (str): serial number (recommended) port (str): port name (only if sn is not specified) axis (str): name of the axis inverted (set of str): names of the axes which are inverted (IOW, either empty or the name of the axis) positions (None, or list of 2 tuples (value, str)): positions values and their corresponding name. If None: 0 and Pi/2 are used, without names. """ if (sn is None and port is None) or (sn is not None and port is not None): raise ValueError("sn or port argument must be specified (but not both)") if sn is not None: if not sn.startswith(SN_PREFIX_MFF) or len(sn) != 8: logging.warning("Serial number '%s' is unexpected for a MFF " "device (should be 8 digits starting with %s).", sn, SN_PREFIX_MFF) self._port = self._getSerialPort(sn) self._sn = sn else: self._port = port # The MFF returns no serial number from GetInfo(), so find via USB try: self._sn = self._getSerialNumber(port) logging.info("Found serial number %s for device %s", self._sn, name) except LookupError: self._sn = None self._serial = self._openSerialPort(self._port) self._ser_access = threading.RLock() # reentrant, so that recovery can keep sending messages self._recover = False self._initHw() # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time if positions is None: positions = ((0, None), (math.radians(90), None)) else: if len(positions) != 2 or any(len(p) != 2 for p in positions): raise ValueError("Positions must be exactly 2 tuples of 2 values") # TODO: have the standard inverted Actuator functions work on enumerated axis if inverted and axis in inverted: positions = (positions[1], positions[0]) self._pos_to_jog = {positions[0][0]: 1, positions[1][0]: 2} self._status_to_pos = {STA_FWD_HLS: positions[0][0], STA_RVS_HLS: positions[1][0]} if positions[0][1] is None: choices = set(p[0] for p in positions) else: choices = dict(positions) # TODO: add support for speed axes = {axis: model.Axis(unit="rad", choices=choices)} model.Actuator.__init__(self, name, role, axes=axes, **kwargs) driver_name = driver.getSerialDriver(self._port) self._swVersion = "%s (serial driver: %s)" % (odemis.__version__, driver_name) try: snd, modl, typ, fmv, notes, hwv, state, nc = self.GetInfo() except IOError: # This is the first communication with the hardware, if it fails # it can be a sign the device is in a bad state. (it is known to # fail when turned on and plugged in before the host computer is # turned on) logging.exception("GetInfo() failed.") raise HwError("USB device with S/N %s seems in bad state. " "Check that the Thorlabs filter flipper was " "turned on *after* the host computer." % sn) self._hwVersion = "%s v%d (firmware %s)" % (modl, hwv, fmv) # It has worked at least once, so if it fails, there are hopes self._recover = True self.position = model.VigilantAttribute({}, readonly=True) self._updatePosition()
def __init__(self, name, role, children, axes_map, ref_on_init=None, **kwargs): """ name (string) role (string) children (dict str -> actuator): axis name (in this actuator) -> actuator to be used for this axis axes_map (dict str -> str): axis name in this actuator -> axis name in the child actuator ref_on_init (None, list or dict (str -> float or None)): axes to be referenced during initialization. If it's a dict, it will go the indicated position after referencing, otherwise, it'll stay where it is. """ if not children: raise ValueError("MultiplexActuator needs children") if set(children.keys()) != set(axes_map.keys()): raise ValueError("MultiplexActuator needs the same keys in children and axes_map") # Convert ref_on_init list to dict with no explicit move after if isinstance(ref_on_init, list): ref_on_init = {a: None for a in ref_on_init} self._ref_on_init = ref_on_init or {} self._axis_to_child = {} # axis name => (Actuator, axis name) self._position = {} self._speed = {} self._referenced = {} axes = {} for axis, child in children.items(): caxis = axes_map[axis] self._axis_to_child[axis] = (child, caxis) # Ducktyping (useful to support also testing with MockComponent) # At least, it has .axes if not isinstance(child, model.ComponentBase): raise ValueError("Child %s is not a component." % (child,)) if not hasattr(child, "axes") or not isinstance(child.axes, dict): raise ValueError("Child %s is not an actuator." % child.name) axes[axis] = copy.deepcopy(child.axes[caxis]) self._position[axis] = child.position.value[axes_map[axis]] if model.hasVA(child, "speed") and caxis in child.speed.value: self._speed[axis] = child.speed.value[caxis] if model.hasVA(child, "referenced") and caxis in child.referenced.value: self._referenced[axis] = child.referenced.value[caxis] # this set ._axes and ._children model.Actuator.__init__(self, name, role, axes=axes, children=children, **kwargs) if len(self.children.value) > 1: # will take care of executing axis move asynchronously self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time # TODO: make use of the 'Cancellable' part (for now cancelling a running future doesn't work) else: # Only one child => optimize by passing all requests directly self._executor = None # keep a reference to the subscribers so that they are not # automatically garbage collected self._subfun = [] children_axes = {} # dict actuator -> set of string (our axes) for axis, (child, ca) in self._axis_to_child.items(): logging.debug("adding axis %s to child %s", axis, child.name) if child in children_axes: children_axes[child].add(axis) else: children_axes[child] = {axis} # position & speed: special VAs combining multiple VAs self.position = model.VigilantAttribute(self._position, readonly=True) for c, ax in children_axes.items(): def update_position_per_child(value, ax=ax, c=c): logging.debug("updating position of child %s", c.name) for a in ax: try: self._position[a] = value[axes_map[a]] except KeyError: logging.error("Child %s is not reporting position of axis %s", c.name, a) self._updatePosition() c.position.subscribe(update_position_per_child) self._subfun.append(update_position_per_child) # TODO: change the speed range to a dict of speed ranges self.speed = model.MultiSpeedVA(self._speed, [0., 10.], setter=self._setSpeed) for axis in self._speed.keys(): c, ca = self._axis_to_child[axis] def update_speed_per_child(value, a=axis, ca=ca, cname=c.name): try: self._speed[a] = value[ca] except KeyError: logging.error("Child %s is not reporting speed of axis %s (%s): %s", cname, a, ca, value) self._updateSpeed() c.speed.subscribe(update_speed_per_child) self._subfun.append(update_speed_per_child) # whether the axes are referenced self.referenced = model.VigilantAttribute(self._referenced.copy(), readonly=True) for axis in self._referenced.keys(): c, ca = self._axis_to_child[axis] def update_ref_per_child(value, a=axis, ca=ca, cname=c.name): try: self._referenced[a] = value[ca] except KeyError: logging.error("Child %s is not reporting reference of axis %s (%s)", cname, a, ca) self._updateReferenced() c.referenced.subscribe(update_ref_per_child) self._subfun.append(update_ref_per_child) for axis, pos in self._ref_on_init.items(): # If the axis can be referenced => do it now (and move to a known position) if axis not in self._referenced: raise ValueError("Axis '%s' cannot be referenced, while should be referenced at init" % (axis,)) if not self._referenced[axis]: # The initialisation will not fail if the referencing fails, but # the state of the component will be updated def _on_referenced(future, axis=axis): try: future.result() except Exception as e: c, ca = self._axis_to_child[axis] c.stop({ca}) # prevent any move queued self.state._set_value(e, force_write=True) logging.exception(e) f = self.reference({axis}) f.add_done_callback(_on_referenced) # If already referenced => directly move # otherwise => put move on the queue, so that any move by client will # be _after_ the init position. if pos is not None: self.moveAbs({axis: pos})
def __init__(self, name, role, port, prot_time=1e-3, prot_curr=30e-6, relay_cycle=None, powered=None, **kwargs): ''' port (str): port name prot_time (float): protection trip time (in s) prot_curr (float): protection current threshold (in Amperes) relay_cycle (None or 0<float): if not None, will power cycle the relay with the given delay (in s) powered (list of str or None): set of the HwComponents controlled by the relay Raise an exception if the device cannot be opened ''' if powered is None: powered = [] self.powered = powered model.PowerSupplier.__init__(self, name, role, **kwargs) # get protection time (s) and current (A) properties if not 0 <= prot_time < 1e3: raise ValueError("prot_time should be a time (in s) but got %s" % (prot_time, )) self._prot_time = prot_time if not 0 <= prot_curr <= 100e-6: raise ValueError("prot_curr (%s A) is not between 0 and 100.e-6" % (prot_curr, )) self._prot_curr = prot_curr # TODO: catch errors and convert to HwError self._ser_access = threading.Lock() self._port = self._findDevice(port) # sets ._serial logging.info("Found PMT Control device on port %s", self._port) # Get identification of the PMT control device self._idn = self._getIdentification() driver_name = driver.getSerialDriver(self._port) self._swVersion = "serial driver: %s" % (driver_name, ) self._hwVersion = "%s" % (self._idn, ) # Set protection current and time self._setProtectionCurrent(self._prot_curr) self._setProtectionTime(self._prot_time) # gain, powerSupply and protection VAs self.protection = model.BooleanVA(True, setter=self._setProtection, getter=self._getProtection) self._setProtection(True) gain_rng = (MIN_VOLT, MAX_VOLT) gain = self._getGain() self.gain = model.FloatContinuous(gain, gain_rng, unit="V", setter=self._setGain) self.powerSupply = model.BooleanVA(True, setter=self._setPowerSupply) self._setPowerSupply(True) # will take care of executing supply asynchronously self._executor = CancellableThreadPoolExecutor( max_workers=1) # one task at a time # relay initialization if relay_cycle is not None: logging.info("Power cycling the relay for %f s", relay_cycle) self.setRelay(False) time.sleep(relay_cycle) # Reset if no powered provided if not powered: self.setRelay(True) else: self._supplied = {} self.supplied = model.VigilantAttribute(self._supplied, readonly=True) self._updateSupplied()
def __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)