#!/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