Source code for sardana.tango.pool.Motor

#!/usr/bin/env python

##############################################################################
##
# This file is part of Sardana
##
# http://www.sardana-controls.org/
##
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
# Sardana is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
##
# Sardana is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
##
# You should have received a copy of the GNU Lesser General Public License
# along with Sardana.  If not, see <http://www.gnu.org/licenses/>.
##
##############################################################################

"""The sardana tango motor module"""

__all__ = ["Motor", "MotorClass"]

__docformat__ = 'restructuredtext'

import sys
import time

from PyTango import DevFailed, Except, DevVoid, DevShort, \
    DevLong, DevDouble, DevBoolean, DispLevel, DevState, AttrQuality, \
    READ, READ_WRITE, SCALAR, SPECTRUM

from taurus.core.util.log import DebugIt

from sardana import State, SardanaServer
from sardana.sardanautils import str_to_value
from sardana.sardanaattribute import SardanaAttribute
from sardana.pool.poolexception import PoolException
from sardana.tango.core.util import memorize_write_attribute, exception_str, \
    to_tango_type_format, throw_sardana_exception
from sardana.tango.pool.PoolDevice import PoolElementDevice, \
    PoolElementDeviceClass


[docs] class Motor(PoolElementDevice): """The tango motor device class. This class exposes through a tango device the sardana motor (:class:`~sardana.pool.poolmotor.PoolMotor`). .. rubric:: The states The motor interface knows five states which are ON, MOVING, ALARM, FAULT and UNKNOWN. A motor device is in MOVING state when it is moving! It is in ALARM state when it has reached one of the limit switches and is in FAULT if its controller software is not available (impossible to load it) or if a fault is reported from the hardware controller. The motor is in the UNKNOWN state if an exception occurs during the communication between the pool and the hardware controller. When the motor is in ALARM state, its status will indicate which limit switches is active. .. rubric:: The commands The motor interface supports 3 commands on top of the Tango classical Init, State and Status commands. These commands are summarized in the following table: ============== ================ ================ Command name Input data type Output data type ============== ================ ================ Stop void void Abort void void DefinePosition Tango::DevDouble void SaveConfig void void MoveRelative Tango::DevDouble void ============== ================ ================ - **Stop** : It stops a running motion. This command does not have input or output argument. - **Abort** : It aborts a running motion. This command does not have input or output argument. - **DefinePosition** : Loads a position into controller. It has one input argument which is the new position value (a double). It is allowed only in the ON or ALARM states. The unit used for the command input value is the physical unit: millimeters or milli-radians. It is always an absolute position. - **SaveConfig** : Write some of the motor parameters in database. Today, it writes the motor acceleration, deceleration, base_rate and velocity into database as motor device properties. It is allowed only in the ON or ALARM states - **MoveRelative** : Moves the motor by a relative to the current position distance. It has one input argument which is the relative distance (a double). It is allowed only in the ON or ALARM states. The unit used for the command input value is the physical unit: millimeters or milli-radians. The classical Tango Init command destroys the motor and re-create it. .. rubric:: The attributes The motor interface supports several attributes which are summarized in the following table: ============== ================= =========== ======== ========= =============== Name Data type Data format Writable Memorized Operator/Expert ============== ================= =========== ======== ========= =============== Position Tango::DevDouble Scalar R/W No * Operator DialPosition Tango::DevDouble Scalar R No Expert Offset Tango::DevDouble Scalar R/W Yes Expert Acceleration Tango::DevDouble Scalar R/W Yes Expert Base_rate Tango::DevDouble Scalar R/W Yes Expert Deceleration Tango::DevDouble Scalar R/W Yes Expert Velocity Tango::DevDouble Scalar R/W Yes Expert Limit_switches Tango::DevBoolean Spectrum R No Expert SimulationMode Tango::DevBoolean Scalar R No Expert Step_per_unit Tango::DevDouble Scalar R/W Yes Expert Backlash Tango::DevLong Scalar R/W Yes Expert ============== ================= =========== ======== ========= =============== - **Position** : This is read-write scalar double attribute. With the classical Tango min_value and max_value attribute properties, it is easy to define authorized limit for this attribute. See the definition of the DialPosition and Offset attributes to get a precise definition of the meaning of this attribute. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN state. It is also not possible to write this attribute when the motor is already MOVING. The unit used for this attribute is the physical unit e.g. millimeters or milli-radian. It is always an **absolute position** . - **DialPosition** : This attribute is the motor dial position. The following formula links together the Position, DialPosition, Sign and Offset attributes: Position = Sign * DialPosition + Offset This allows to have the motor position centered around any position defined by the Offset attribute (classically the X ray beam position). It is a read only attribute. To set the motor position, the user has to use the Position attribute. It is not allowed to read this attribute when the motor is in FAULT or UNKNOWN mode. The unit used for this attribute is the physical unit: millimeters or milli-radian. It is also always an **absolute** position. - **Offset** : The offset to be applied in the motor position computation. By default set to 0. It is a memorized attribute. It is not allowed to read or write this attribute when the motor is in FAULT, MOVING or UNKNOWN mode. - **Acceleration** : This is an expert read-write scalar double attribute. This parameter value is written in database when the SaveConfig command is executed. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN state. - **Deceleration** : This is an expert read-write scalar double attribute. This parameter value is written in database when the SaveConfig command is executed. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN state. - **Base_rate** : This is an expert read-write scalar double attribute. This parameter value is written in database when the SaveConfig command is executed. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN state. - **Velocity** : This is an expert read-write scalar double attribute. This parameter value is written in database when the SaveConfig command is executed. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN state. - **Limit_switches** : Three limit switches are managed by this attribute. Each of the switch are represented by a boolean value: False means inactive while True means active. It is a read only attribute. It is not possible to read this attribute when the motor is in UNKNOWN mode. It is a spectrum attribute with 3 values which are: - Data[0] : The Home switch value - Data[1] : The Upper switch value - Data[2] : The Lower switch value - **SimulationMode** : This is a read only scalar boolean attribute. When set, all motion requests are not forwarded to the software controller and then to the hardware. When set, the motor position is simulated and is immediately set to the value written by the user. To set this attribute, the user has to used the pool device Tango interface. The value of the position, acceleration, deceleration, base_rate, velocity and offset attributes are memorized at the moment this attribute is set. When this mode is turned off, if the value of any of the previously memorized attributes has changed, it is reapplied to the memorized value. It is not allowed to read this attribute when the motor is in FAULT or UNKNOWN states. - **Step_per_unit** : This is the number of motor step per millimeter or per degree. It is a memorized attribute. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN mode. It is also not allowed to write this attribute when the motor is MOVING. The default value is 1. - **Backlash** : If this attribute is defined to something different than 0, the motor will always stop the motion coming from the same mechanical direction. This means that it could be possible to ask the motor to go a little bit after the desired position and then to return to the desired position. The attribute value is the number of steps the motor will pass the desired position if it arrives from the "wrong" direction. This is a signed value. If the sign is positive, this means that the authorized direction to stop the motion is the increasing motor position direction. If the sign is negative, this means that the authorized direction to stop the motion is the decreasing motor position direction. It is a memorized attribute. It is not allowed to read or write this attribute when the motor is in FAULT or UNKNOWN mode. It is also not allowed to write this attribute when the motor is MOVING. Some hardware motor controllers are able to manage this backlash feature. If it is not the case, the motor interface will implement this behavior. All the motor devices will have the already described attributes but some hardware motor controller supports other features which are not covered by this list of pre-defined attributes. Using Tango dynamic attribute creation, a motor device may have extra attributes used to get/set the motor hardware controller specific features. These are the attributes specified on the controller with :attr:`~sardana.pool.controller.Controller.axis_attribues`. .. rubric:: The properties - **Sleep_bef_last_read** : This property exposes the motor *instability time*. It defines the time in milli-second that the software managing a motor movement will wait between it detects the end of the motion and the last motor position reading. .. rubric:: Getting motor state and limit switches using event The simplest way to know if a motor is moving is to survey its state. If the motor is moving, its state will be MOVING. When the motion is over, its state will be back to ON (or ALARM if a limit switch has been reached). The pool motor interface allows client interested by motor state or motor limit switches value to use the Tango event system subscribing to motor state change event. As soon as a motor starts a motion, its state is changed to MOVING and an event is sent. As soon as the motion is over, the motor state is updated and another event is sent. In the same way, as soon as a change in the limit switches value is detected, a change event is sent to client(s) which have subscribed to change event on the Limit_Switches attribute. .. rubric:: Reading the motor position attribute For each motor, the key attribute is its position. Special care has been taken on this attribute management. When the motor is not moving, reading the Position attribute will generate calls to the controller and therefore hardware access. When the motor is moving, its position is automatically read every 100 milli-seconds and stored in the cache. This means that a client reading motor Position attribute while the motor is moving will get the position from the cache and will not generate extra controller calls. It is also possible to get a motor position using the Tango event system. When the motor is moving, an event is sent to the registered clients when the change event criterion is true. By default, this change event criterion is set to be a difference in position of 1. It is tunable on a motor basis using the classical motor Position attribute abs_change property or at the pool device basis using its DefaultMotPos_AbsChange property. Anyway, not more than 10 events could be sent by second. Once the motion is over, the motor position is made unavailable from the Tango polling buffer and is read a last time after a tunable waiting time (Sleep_bef_last_read property). A forced change event with this value is sent to clients using events. """ # Position was memorized in early sardana version (approx. at 2012) # ignore it now to avoid triggering motion on the Pool startup ignore_memorized_attrs_during_initialization = ("Position", ) def __init__(self, dclass, name): """Constructor""" self.in_write_position = False PoolElementDevice.__init__(self, dclass, name)
[docs] def init(self, name): PoolElementDevice.init(self, name)
def _is_allowed(self, req_type): return PoolElementDevice._is_allowed(self, req_type)
[docs] def get_motor(self): return self.element
[docs] def set_motor(self, motor): self.element = motor
motor = property(get_motor, set_motor)
[docs] def set_write_dial_position_to_db(self): dial = self.motor.get_dial_position_attribute() if dial.has_write_value(): data = dict(DialPosition=dict( __value=dial.w_value, __value_ts=dial.w_timestamp)) db = self.get_database() db.put_device_attribute_property(self.get_name(), data)
[docs] def get_write_dial_position_from_db(self): name = 'DialPosition' db = self.get_database() pos_props = db.get_device_attribute_property( self.get_name(), name)[name] w_pos = pos_props["__value"][0] _, _, attr_info = self.get_dynamic_attributes()[0][name] w_pos = str_to_value(w_pos, attr_info.dtype, attr_info.dformat) w_pos, w_ts = float(pos_props["__value"][0]), None if "__value_ts" in pos_props: w_ts = float(pos_props["__value_ts"][0]) return w_pos, w_ts
[docs] @DebugIt() def delete_device(self): PoolElementDevice.delete_device(self) motor = self.motor if motor is not None: motor.remove_listener(self.on_motor_changed) self.motor = None
[docs] @DebugIt() def init_device(self): PoolElementDevice.init_device(self) id_ = self.get_id() try: self.motor = motor = self.pool.get_element_by_id(id_) except KeyError: full_name = self.get_full_name() name = self.alias or full_name svr_running = SardanaServer.server_state == State.Running self.motor = motor = \ self.pool.create_element(type="Motor", name=name, full_name=full_name, id=id_, axis=self.Axis, ctrl_id=self.get_ctrl_id(), handle_ctrl_err=not svr_running) if self.instrument is not None: motor.set_instrument(self.instrument) # if in constructor, for all memorized no init attributes (position) # let poolmotor know their write values if self.in_constructor: try: w_pos, w_ts = self.get_write_dial_position_from_db() self.in_write_position = True try: motor.set_write_position(w_pos, timestamp=w_ts) finally: self.in_write_position = False except KeyError: pass else: ctrl = self.motor.get_controller() ctrl.add_element(self.motor, propagate=0) if self.Sleep_bef_last_read > 0: motor.set_instability_time(self.Sleep_bef_last_read / 1000) motor.add_listener(self.on_motor_changed) self.set_state(DevState.ON)
[docs] def on_motor_changed(self, event_source, event_type, event_value): try: self._on_motor_changed(event_source, event_type, event_value) except DevFailed: raise except: msg = 'Error occurred "on_motor_changed(%s.%s): %s"' exc_info = sys.exc_info() self.error(msg, self.motor.name, event_type.name, exception_str(*exc_info[:2])) self.debug("Details", exc_info=exc_info)
def _on_motor_changed(self, event_source, event_type, event_value): # during server startup and shutdown avoid processing element # creation events if SardanaServer.server_state != State.Running: return timestamp = time.time() name = event_type.name.lower() if name == "w_position" and not self.in_write_position: self.debug("Storing dial set point: %s", self.motor.dial_position.w_value) self.set_write_dial_position_to_db() return try: attr = self.get_attribute_by_name(name) except DevFailed: return quality = AttrQuality.ATTR_VALID priority = event_type.priority value, w_value, error = None, None, None if name == "state": value = self.calculate_tango_state(event_value) elif name == "status": value = self.calculate_tango_status(event_value) else: if isinstance(event_value, SardanaAttribute): if event_value.error: error = Except.to_dev_failed(*event_value.exc_info) else: value = event_value.value timestamp = event_value.timestamp else: value = event_value state = self.motor.get_state(propagate=0) if name == "position": w_value = event_source.get_position_attribute().w_value if state == State.Moving: quality = AttrQuality.ATTR_CHANGING elif name == "dialposition" and state == State.Moving: quality = AttrQuality.ATTR_CHANGING self.set_attribute(attr, value=value, w_value=w_value, timestamp=timestamp, quality=quality, priority=priority, error=error, synch=False)
[docs] def always_executed_hook(self): pass
def read_attr_hardware(self, data): pass
[docs] def get_dynamic_attributes(self): cache_built = hasattr(self, "_dynamic_attributes_cache") std_attrs, dyn_attrs = \ PoolElementDevice.get_dynamic_attributes(self) if not cache_built: # For position attribute, listen to what the controller says for data # type (between long and float) pos = std_attrs.get('position') if pos is not None: _, data_info, attr_info = pos ttype, _ = to_tango_type_format(attr_info.dtype) data_info[0][0] = ttype return std_attrs, dyn_attrs
[docs] def initialize_dynamic_attributes(self): attrs = PoolElementDevice.initialize_dynamic_attributes(self) detect_evts = "position", "dialposition", non_detect_evts = "limit_switches", "step_per_unit", "offset", \ "sign", "velocity", "acceleration", "deceleration", "base_rate", \ "backlash" for attr_name in detect_evts: if attr_name in attrs: self.set_change_event(attr_name, True, True) for attr_name in non_detect_evts: if attr_name in attrs: self.set_change_event(attr_name, True, False)
[docs] def read_Position(self, attr): motor = self.motor use_cache = motor.is_in_operation() and not self.Force_HW_Read state = motor.get_state(cache=use_cache, propagate=0) position = motor.get_position(cache=use_cache, propagate=0) if position.error: Except.throw_python_exception(*position.exc_info) quality = None if state == State.Moving: quality = AttrQuality.ATTR_CHANGING self.set_attribute(attr, value=position.value, w_value=position.w_value, quality=quality, priority=0, timestamp=position.timestamp)
[docs] def write_Position(self, attr): self.in_write_position = True position = attr.get_write_value() try: self.info("write_Position(%s)", position) try: self.wait_for_operation() except: raise Exception("Cannot move: already in motion") try: self.motor.position = position except PoolException as pe: throw_sardana_exception(pe) # manually store write dial position in the database self.set_write_dial_position_to_db() finally: self.in_write_position = False
[docs] def read_Acceleration(self, attr): attr.set_value(self.motor.get_acceleration(cache=False))
[docs] @memorize_write_attribute def write_Acceleration(self, attr): self.motor.acceleration = attr.get_write_value()
[docs] def read_Deceleration(self, attr): attr.set_value(self.motor.get_deceleration(cache=False))
[docs] @memorize_write_attribute def write_Deceleration(self, attr): self.motor.deceleration = attr.get_write_value()
[docs] def read_Base_rate(self, attr): attr.set_value(self.motor.get_base_rate(cache=False))
[docs] @memorize_write_attribute def write_Base_rate(self, attr): self.motor.base_rate = attr.get_write_value()
[docs] def read_Velocity(self, attr): attr.set_value(self.motor.get_velocity(cache=False))
[docs] @memorize_write_attribute def write_Velocity(self, attr): self.motor.velocity = attr.get_write_value()
[docs] def read_Offset(self, attr): attr.set_value(self.motor.get_offset(cache=False).value)
[docs] @memorize_write_attribute def write_Offset(self, attr): self.motor.offset = attr.get_write_value()
[docs] def read_DialPosition(self, attr): motor = self.motor use_cache = motor.is_in_operation() and not self.Force_HW_Read state = motor.get_state(cache=use_cache, propagate=0) dial_position = motor.get_dial_position(cache=use_cache, propagate=0) if dial_position.error: Except.throw_python_exception(*dial_position.exc_info) quality = None if state == State.Moving: quality = AttrQuality.ATTR_CHANGING self.set_attribute(attr, value=dial_position.value, quality=quality, priority=0, timestamp=dial_position.timestamp)
[docs] def read_Step_per_unit(self, attr): attr.set_value(self.motor.get_step_per_unit(cache=False))
[docs] @memorize_write_attribute def write_Step_per_unit(self, attr): step_per_unit = attr.get_write_value() self.motor.step_per_unit = step_per_unit
[docs] def read_Backlash(self, attr): attr.set_value(self.motor.get_backlash(cache=False))
[docs] @memorize_write_attribute def write_Backlash(self, attr): self.motor.backlash = attr.get_write_value()
[docs] def read_Sign(self, attr): sign = self.motor.get_sign(cache=False).value attr.set_value(sign)
[docs] @memorize_write_attribute def write_Sign(self, attr): self.motor.sign = attr.get_write_value()
[docs] def read_Limit_switches(self, attr): motor = self.motor use_cache = motor.is_in_operation() and not self.Force_HW_Read limit_switches = motor.get_limit_switches(cache=use_cache) self.set_attribute(attr, value=limit_switches.value, priority=0, timestamp=limit_switches.timestamp)
[docs] def DefinePosition(self, argin): self.motor.define_position(argin) # update write value of position attribute pos_attr = self.get_wattribute_by_name("position") pos_attr.set_write_value(argin)
[docs] def is_DefinePosition_allowed(self): if self.get_state() in (DevState.FAULT, DevState.MOVING, DevState.UNKNOWN): return False return True
[docs] def SaveConfig(self): raise NotImplementedError
[docs] def is_SaveConfig_allowed(self): if self.get_state() in (DevState.FAULT, DevState.MOVING, DevState.UNKNOWN): return False return True
[docs] def MoveRelative(self, argin): raise NotImplementedError
[docs] def is_MoveRelative_allowed(self): if self.get_state() in (DevState.FAULT, DevState.MOVING, DevState.UNKNOWN): return False return True
[docs] def get_attributes_to_restore(self): """Make sure position is the last attribute to restore""" restore_attributes = PoolElementDevice.get_attributes_to_restore(self) try: restore_attributes.remove('Position') restore_attributes.append('Position') except ValueError: pass return restore_attributes
is_Position_allowed = _is_allowed is_Acceleration_allowed = _is_allowed is_Deceleration_allowed = _is_allowed is_Base_rate_allowed = _is_allowed is_Velocity_allowed = _is_allowed is_Offset_allowed = _is_allowed is_DialPosition_allowed = _is_allowed is_Step_per_unit_allowed = _is_allowed is_Backlash_allowed = _is_allowed is_Sign_allowed = _is_allowed is_Limit_switches_allowed = _is_allowed
[docs] class MotorClass(PoolElementDeviceClass): # Class Properties class_property_list = { } # Device Properties device_property_list = { 'Sleep_bef_last_read': [DevLong, "Number of mS to sleep before the last read during a motor " "movement", 0], '_Acceleration': [DevDouble, "", -1], '_Deceleration': [DevDouble, "", -1], '_Velocity': [DevDouble, "", -1], '_Base_rate': [DevDouble, "", -1], } device_property_list.update(PoolElementDeviceClass.device_property_list) # Command definitions cmd_list = { 'DefinePosition': [[DevDouble, "New position"], [DevVoid, ""]], 'SaveConfig': [[DevVoid, ""], [DevVoid, ""]], 'MoveRelative': [[DevDouble, "amount to move"], [DevVoid, ""]], } cmd_list.update(PoolElementDeviceClass.cmd_list) # Attribute definitions attr_list = {} attr_list.update(PoolElementDeviceClass.attr_list) standard_attr_list = { 'Position': [[DevDouble, SCALAR, READ_WRITE], {'abs_change': '1.0', }], 'Acceleration': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", }], 'Deceleration': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", }], 'Base_rate': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", 'label': 'Base rate', }], 'Velocity': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", }], 'Offset': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", 'Display level': DispLevel.EXPERT}], 'DialPosition': [[DevDouble, SCALAR, READ], {'abs_change': '5.0', 'label': "Dial position", 'Display level': DispLevel.EXPERT}], 'Step_per_unit': [[DevDouble, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", 'label': "Steps p/ unit", 'Display level': DispLevel.EXPERT}], 'Backlash': [[DevLong, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", 'Display level': DispLevel.EXPERT}], 'Sign': [[DevShort, SCALAR, READ_WRITE], {'Memorized': "true_without_hard_applied", 'Display level': DispLevel.EXPERT}], 'Limit_switches': [[DevBoolean, SPECTRUM, READ, 3], {'label': "Limit switches (H,U,L)", 'description': "This attribute is the motor " "limit switches state. It's an array with 3 \n" "elements which are:\n" "0 - The home switch\n" "1 - The upper limit switch\n" "2 - The lower limit switch\n" "False means not active. True means active"}], } standard_attr_list.update(PoolElementDeviceClass.standard_attr_list) def _get_class_properties(self): ret = PoolElementDeviceClass._get_class_properties(self) ret['Description'] = "Motor device class" ret['InheritedFrom'].insert(0, 'PoolElementDevice') return ret