"""
Create methods or attributes from a dictionary.
Call the methode read() for get, or write() for set attribute, with some checks before and after calling.
:Date: |today|
:Author: Semi-ATE <info@Semi-ATE.org>
"""
from pylab_ml.base_instrument import logger
from enum import Enum
import hightime
from pylab_ml.common.common import str2num
[docs]
class create_attributes(object):
"""
Create methods or attributes from a dictionary.
:Date: |today|
:Author: Semi-ATE <info@Semi-ATE.org>
Call the methode read() for get, or write() for set attribute, with some checks before and after calling.
Syntax from this dictonary to create the attributes:
| {attribute/method name : (Device command for read/write) , range, call functions}
:Attribute/Method name:
Your provided name for the attribute/method
:Device command:
* The read nd write command for the instance, e.q. 'TMPA?' or None.
* Or the method name e.q. 'current_level_autorange'.
:Range:
Could be None, Enum or range value (integer or float)
:Call functions:
Before or after the instance write or read, you can define a functionname, which will be call and manipulate the get/set value
* 'gb'= get_before() -> get, call function before the read instance. Do something before the read instance
* 'ga'= get_after() -> get, call function after the read instance. Do something with the value, e.q. translate from hex to integer
* 'gac'= get_after_check() -> get, call function after the read instance and check.
* 'sb'= set_before() -> set, call function before write instance
* 'sac'= set_after_check() -> set, call function after check, before write instance
* 'sa'= set_after -> set, call function after write instance
The functions itself, have to return the modified value or None if value are not modified.
If you get an error in the function, than you should set your return value to ATTR_ERROR
Example 1:
>>> class test(create_attributes)
>>>
>>> properties = {'bitTime' : (('?bt', 'sbt'), [10, 3400], {'ga': '_hex2dec(value)', 'sac': '_dec2hex(value)', 'sa': 'readresult(0)'}),
>>> 'airtemp' : (('TMPA?', None), None, None),
>>> 'dutsensortype' : (('DSNS?','DSNS'), [0,4], None),
>>> 'blaba' : (('BLAA?','BLAB'), [1.0,4.7]; None),
>>> 'compressor' : (('COOL?','COOL'), 'Compressor', {'sac': '_compressor(value)'}),
>>> }
>>>
>>> class Compressor(Enum) :
>>> off= 0
>>> on = 1
>>>
>>> def setup_inst(self):
>>> self.createattributes(self.properties) # <-- add this line in your setup_inst
>>> super().setup_inst()
==> This will create following attributes:
>>> # create attribute bitTime with get/set :
>>> self.bitTime # get attribute : call the methode inst.query('?bt')
MEASURE - 'yourDevice'.bitTime == 480
480
>>> self.bitTime = 20 # set attribute : check if value is integer, and 10<=value<=3400,
>>> # if ok than call inst.write('sbt')
MEASURE - 'yourDevice'.bitTime := 20
>>> # Create attribute airtemp with get:
>>> self.airtemp
MEASURE - 'yourDevice'.airtemp == 22.2
22.2
>>> # Create attribute dutsensortype with get/set :
>>> self.dutsensortype # write inst.query('DSNS?'), return with int(value)
MEASURE - 'yourDevice'.dutsensortype == 0
0
>>> self.dutsensortype = 3 # check if value is integer, and 0<=value<=4,
>>> # if ok than inst.write('DSNS 3')
MEASURE - 'yourDevice'.dutsensortype := 3
3
>>> self.dutsensortype = 5
ERROR - 'yourDevice'.dutsensortype := 5 outside limits, choose [0, 4]
>>> # Create attribute blaba with get/set :
>>> self.blaba # inst.query('BLAA?'), return with float(value)
MEASURE - 'yourDevice'.blaba == 3.0
3.0
>>> self.blaba = 3.4 # check if value is float, and 1.0<=value<=4.7,
>>> # if ok than inst.write('BLAB')
MEASURE - 'yourDevice'.blaba := 3.4
3.4
>>> # Create attribute compressor with get/set and values is Enum:
>>> self.compressor #inst.query('COOL?'), return with the enum Compressor
MEASURE - 'yourDevice'.compressor == Compressor.on
<Compressor.on: 1>
>>> compressor = Compressor.on # check if value in Compressor
>>> # if ok than call _compressor(Compressor.on), and than inst.write('COOL 1'),
MEASURE - 'yourDevice'.compressor := Compressor.on
>>> compressor = 'on' # shorter but the same as before
MEASURE - 'yourDevice'.compressor := Compressor.on
>>> compressor = 1 # also possible
MEASURE - 'yourDevice'.compressor := Compressor.on
Example 2, for calling inst.methode (none read/write):
>>> properties = {'auto_zero': ('auto_zero', 'backend.AutoZero', {'sac': 'checkstate(uncommitted)'}),
>>> 'aperture_time_units': ('aperture_time_units', 'backend.ApertureTimeUnits', {'sac': 'checkstate(uncommitted)'}),
>>> 'aperture_time': ('aperture_time', None, {'sac': 'checkstate(uncommitted)'}),
>>> }
See also:
* the class :func:`~instruments.smu.tti.base_tti.TTI` :download:`instruments/smu/tti/base_tti <../../../src/pylab_ml/pylab_ml/smu/tti/base_tti.py>`
show the usage to create attributes and connect to a smu with one or more channels
* the class :func:`~instruments.thermostreamer.mpi_ta5k.MPI_TA5K` :download:`instruments/thermostreamer/mpi_ta5k <../../../src/pylab_ml/pylab_ml/thermostreamer/mpi_ta5k.py>`
show the usage to create attributes and connect to a thermostreamer
* the class :func:`~instruments.boards.micronas.communication.apbboard.HALAPBBoard` :
download:`../../../src/pylab_ml/pylab_ml/boards/micronas/communication/apbboard <../../../instruments/boards/micronas/communication/apbboard.py>`
show the usage to create attributes and connect to a communication board
* the class :func:`~instruments.smu.natinst.pxie41xx` use this class to call inst.methods_name
:download:`instruments/smu/natinst/pxie41xx.py <../../../src/pylab_ml/pylab_ml//smu/natinst/pxie41xx.py>`
Tip:
If your device has no read/write instance, than overwrite the method _call_instance()
Example:
>>> def _call_instance(self, function, rw, value=None):
>>> if rw == "wr":
>>> self.ch[self.channel].__setattr__(function, value) # for set attribute
>>> elif rw == "rd":
>>> value = self.ch[self.channel].__getattribute__(function) # for get attribute
>>> return (value)
Note:
Necessary Methods in the class above (if you don't overwrite the method _call_instance()):
>>> def read(self):
>>> value = self.inst.read() # your code for instance read
>>> return value
>>>
>>> def write(self,value):
>>> self.instance.write(value) # your code for instance write
>>>
"""
_attributes = {}
_ATTRERROR = {
1: "{!r}.{} always exist, couldn't create this attribute", # error
2: "{!r}.{} := {} not possible, parameter have no set!",
3: "{!r}.{} has no proberty {}, choose {}",
4: "{!r}.{} := {} outside limits, choose {}",
5: "{!r}.{}: {}",
6: "{!r} last command was {} := {}",
10: " ", # warning
20: " ", # info
30: "{!r}.{} == {} (shadow attribute)", # measure
31: "{!r}.{} := {}",
32: "{!r}.{} == {}",
}
[docs]
def __init__(self):
self._attributes = {}
self.ATTR_ERROR = "ATTRIBUTE_ERROR"
"""if something wrong with your called methode, than set the result to self.ATTR_ERROR """
self.attrLast = ""
"""last set/get attribute name."""
self.attrLastvalue = None
"""last set attribute value."""
[docs]
def createattributes(self, dictionary, parent=None, child=None, childname=''):
"""
Create attributes or methods from a dictionary.
Syntax from the dictionary see example in the class documentation.
Parameters
----------
dictionary : dict
The dictionary with the syntax:
{attribute/methode name : (Device command for read/write) , range, call functions}.
parent : create_attributes, optional
If you want to create a child, than you have to set the parent, otherwise None, by default None.
child : str, optional
The name of the child, by default None.
childname : str, optional
The name of the child, by default ''.
Returns
-------
None.
"""
if child is not None:
myparent = self if parent is None else parent
childname = child if childname == '' else childname
object.__setattr__(myparent, child, Child(myparent, childname))
parent = getattr(self, child)
childname = childname if childname == '' else childname+'.'
if self._attributes == {} or child is not None:
for attribute in dictionary.copy():
myparent = self if parent is None else parent
index = 0
splitattribute = attribute.split('.')
for partattr in splitattribute:
if hasattr(myparent, partattr):
pass
elif index < len(splitattribute)-1:
object.__setattr__(myparent, partattr, Child(self, f'{childname}{partattr}')) # create a child
myparent._attributes[partattr] = '_attrChild' # and mark it in the _attributes
else:
object.__setattr__(myparent, partattr, 'noinit')
myparent._attributes[partattr] = dictionary[attribute]
if index < len(splitattribute)-1: # has a child
myparent = getattr(myparent, partattr)
index += 1
self._attributes = dictionary
def __setattr__(self, attr, value):
"""
Set attribute.
This method will be called automatically if you set an attribute.
Checks whether the atribute is in the attributes-directory.
If so, the associated functions are called.
Parameters
----------
attr : str
Attribute name.
value : any
Value which one want to set.
Returns
-------
None.
"""
if hasattr(self, '_attributes') and attr in self._attributes:
self.attrLast = attr
self.attrLastvalue = value
orgvalue = value
fparam = self._attributes[
attr
] # search in the directory the accociated function parameter
error = False
if isinstance(fparam, tuple) and fparam[1] is None:
self._attrlogger(2, attr, value)
return
error, value = self._call_function("sb", fparam, value)
if error:
return
error, value = self._validateattributes(attr, value, fparam[1]) # check attributes
if error:
return
error, value = self._call_function("sac", fparam, value)
if error:
return
self._call_instance(
self._get_functionname(fparam, "wr"), "wr", value
) # call write function
error, result = self._call_function("sa", fparam, value)
if error:
return
if isinstance(fparam[0], tuple) and fparam[0][0] is None:
super(__class__, self).__setattr__("_" + attr, value) # no read available -> set shadow attribute
self._attrlogger(31, attr, orgvalue) # get measure info
if hasattr(self, "mqtt_enable") and self.mqtt_enable:
if isinstance(value, Enum):
self.publish_get(attr, value.name)
else:
self.publish_get(attr, orgvalue)
super(__class__, self).__setattr__(f'{attr}_cache', value)
else:
super(__class__, self).__setattr__(attr, value)
def __getattribute__(self, attr):
"""
Get attribute.
This method will be called automatically if you get an attribute.
Checks whether the atribute is in the attributes-directory.
If so, the associated functions are called.
Parameters
----------
attr : str
Attribute name.
Returns
-------
value : any
Getting value the inst.read() or the shadow attribute.
"""
if (
attr != "__class__"
and attr != "mqtt_list"
and attr != "_attributes"
and attr in self._attributes
and self._attributes[attr] != '_attrChild'
):
self.attrLast = attr
self.attrLastvalue = None
fparam = self._attributes[attr]
if isinstance(fparam, tuple) and fparam[0][0] is None:
value = super(__class__, self).__getattribute__("_" + attr)
self._attrlogger(30, attr, value)
return value
error, value = self._call_function("gb", fparam, None)
if error:
return
value = self._call_instance(
self._get_functionname(fparam, "rd"), "rd") # get the read function and make the call
error, value = self._call_function("ga", fparam, value)
if error:
return
if type(fparam[1]) is dict:
# keys = [key for key, val in fparam[1].items() if val == value]
# value = str(keys)
# print(f'{self.attrLast} = {keys}')
for key, val in fparam[1].items():
if value == val:
value = key
# else:
# print(f'{self.attrLast} = {value}')
elif type(value) is hightime.timedelta:
value = value.total_seconds()
elif fparam[1] is not None and value is not None and isinstance(fparam[1][0], int): # is range(=fparam[1]) an integer?
value = int(float(value))
elif fparam[1] is not None and value is not None and isinstance(fparam[1][0], float): # is range a float?
value = float(value)
else:
if not isinstance(value, (float, int)): # if value is an string ?
value = str2num(value)
if not isinstance(value, Enum): # is value an Enum? -> do nothing
check, enum = self._ifenum(fparam[1]) # check if range an enum?
if check:
error = True # check if value in enum
for name in enum:
if value == name.value:
value = name
error = False
if error:
self._enum_error(attr, value, enum)
error, value = self._call_function("gac", fparam, value)
if error:
return
self._attrlogger(32, attr, value)
if hasattr(self, "mqtt_enable") and self.mqtt_enable:
if isinstance(value, Enum):
self.publish_get(attr, value.name)
else:
self.publish_get(attr, value)
super(__class__, self).__setattr__(f'{attr}_cache', value)
else:
value = super(__class__, self).__getattribute__(attr)
return value
def _attrlogger(self, msgnr, *kwargs):
"""
Log the attribute access and errors.
Parameters
----------
msgnr : int
Message number to determine the log message and level.
*kwargs : any
Additional arguments to format the log message.
Returns
-------
None
"""
# if self.instName[-3:] == '[0]':
# instName_short = self.instName[:len(self.instName) - 3]
# else:
# instName_short = self.instName
msg = self._ATTRERROR[msgnr].format(self.instName, *kwargs)
if msgnr < 10:
logger.error(msg)
elif msgnr < 20:
logger.warning(msg)
elif msgnr < 30:
logger.info(msg)
elif msgnr < 40:
logger.measure(msg)
elif msgnr < 50:
logger.error("error: message not implemented !")
def _enum_error(self, function_name, val, enum):
"""
Log an error message for invalid enum values.
Parameters
----------
function_name : str
Name of the function where the error occurred.
val : any
The invalid value.
enum : Enum
The enum class.
Returns
-------
None
"""
msg = ""
for values in enum:
msg += "/" + values.name
self._attrlogger(3, function_name, val, msg[1:])
def _validateattributes(self, attr, value, validaterange=None):
"""Check if value in the validate range.
Parameters
----------
attr : str
Attribute name.
value : any
The value to be validated.
validaterange : any, optional
The range or enum to validate against. Defaults to None.
Returns
-------
error : bool
True if the value is invalid, False otherwise.
value : any
The original value or the corresponding enum member if valid.
"""
error = False
is_enum, enum = self._ifenum(validaterange) # check if validaterange an enum
if validaterange is None:
return error, value
elif isinstance(validaterange, list):
if type(validaterange[0]) is str:
if value not in validaterange:
error = True
elif value < validaterange[0] or value > validaterange[1]:
error = True
if error:
self._attrlogger(4, attr, value, validaterange)
elif type(validaterange) is dict:
if value in validaterange:
value = validaterange[value]
else:
error = True
self._attrlogger(3, attr, value, validaterange.keys()) # TODO: how can i remove the 'dict_keys' in the output?
elif is_enum:
error = True
if isinstance(value, str): # check if string in enum
for name in enum:
if value == name.name:
value = name
error = False
elif isinstance(value, enum):
error = False
else:
for name in enum:
if value == name.value:
value = name
error = False
if error:
self._enum_error(attr, value, enum)
return error, value
def _ifenum(self, value):
"""
Check if type(value)==enum.
ENUM have to be in the path of self , e.q: self.test(value='test') or self.backend.test (value='backend.test')
Parameters
----------
value : any
The value to be checked.
Returns
-------
result : bool
True if the value is an enum, False otherwise.
enum : Enum or None
The enum class if the value is an enum, None otherwise.
"""
result = False
enum = None
if value is None or isinstance(value, (list, float, int)):
return False, None
elif isinstance(value, str):
value = value.split(".")
enum = self.__getattribute__(value[0])
if len(value) > 1:
for i in range(1, len(value)):
enum = enum.__getattribute__(value[i])
result = True
# elif hasattr(self,value):
# enum=self.__getattribute__(value)
# result=True
return result, enum
def _get_functionname(self, dictline, rw):
"""
Get the function name for read/write.
Parameters
----------
dictline : any
The dictionary line to be processed.
rw : str
The read/write operation ('rd' or 'wr').
Returns
-------
result : str
The function name for the specified read/write operation.
"""
if isinstance(dictline[0], tuple):
if rw == "wr":
result = dictline[0][1]
elif rw == "rd":
result = dictline[0][0]
else:
result = dictline[0]
return result
def _call_function(self, typ, fparam, value=None):
"""
Call the associated function for the attribute if defined in the dictionary.
Parameters
----------
typ : str
The type of the function to be called.
fparam : list
The function parameters.
value : any, optional
The value to be passed to the function. Defaults to None.
Returns
-------
error : int
The error code.
result : any
The result of the function call.
"""
found = -1
result = value
error = 0
if isinstance(fparam[2], dict) and typ in fparam[2]:
functioncall = fparam[2][typ] # get the function call
found = 1
if found != -1:
function = functioncall.split("(")[0] # seperate function
values = functioncall.split("(")[1][:-1] # seperate arguments
if found != -1 and values.find("value") > -1:
if values.find(",") > -1:
logger.error('attributes.create_attributes error in {}, functioncall with more than one values ({}) are not implemented yet, please investigate'.format(values, fparam))
values = values.replace("value", str(value))
else:
values = value
if found != -1:
try:
if values is None:
result = getattr(self, function)()
else:
result = getattr(self, function)(values)
if result is None:
result = value
elif result == "ERROR":
self._attrlogger(6, self.attrLast, self.attrLastvalue)
return True, value
except Exception as e:
error = self.ATTR_ERROR
self._attrlogger(5, function, e)
return error, result
def _call_instance(self, function, rw, value=None):
"""
Read/write interface to the instance.
If your device has no read/write, then overwrite this function in your class.
See eg. instruments/instruments/smu/natinst/base_natinst.py
Parameters
----------
function : str
The function or command to be called on the instance.
rw : str
* 'wr' -> inst.write(function,value)
* 'rd' -> inst.write(function), value=inst.read()
value : any, optional
Only necessary for 'wr', the value which will be written.
Returns
-------
value : any
Only for 'rd', the value which you get from the instance
"""
if isinstance(value, Enum):
value = value.value
if rw == "wr":
self.write("{}{}".format(function, value))
elif rw == "rd":
self.write("{}".format(function))
value = self.read()
return value
[docs]
class Child(create_attributes):
""" Class for child attributes. This class is used to create child attributes, which are attributes of attributes. """
[docs]
def __init__(self, root, instName):
"""
Initialize the Child instance.
Parameters
----------
root : object
The root object to which this child belongs.
instName : str
The name of the instance.
"""
super().__init__()
self.root = root
self.instName = instName
self._attributes = {}
[docs]
def write(self, msg):
"""Write method for the child instance. This method will be called when you set a child attribute."""
self.root.publish_get(f'{self.instName}.{self.attrLast}', self.attrLastvalue)
[docs]
def read(self):
"""Read method for the child instance. This method will be called when you get a child attribute."""
cache = f'{self.attrLast}_cache'
value = getattr(self, cache) if hasattr(self, cache) else None
self.root.publish_get(f'{self.instName}.{self.attrLast}', value)
return value