How to write a new device¶
- Parenti and A. Silenzi
Introduction¶
In order to write a new beckhoffSim device type, several
adjustments need to be done:
Create a file in the
devicesfolder, e.g.digitaloutput.pyCreate a file in the
testfolder, e.g.test_digitaloutput.pyAdd the new device name in the
beckhoffSim.pyas row_schema optionSTRING_ELEMENT(row_schema).key("type") .displayedName("Type") .description("Device Type") .options("DigitalInput, DigitalOutput, Valve, AnalogInput, AnalogOutput, PfeifferTC, StepperAxis") # <- HERE .assignmentOptional().defaultValue("DigitalInput") .commit()
Add the new import statement in
core/devicefactory.pyfrom devices.digitalinput import DigitalInput from devices.digitaloutput import DigitalOutput from devices.valve import Valve from devices.analoginput import AnalogInput from devices.analogoutput import AnalogOutput from devices.tcpfeiffer import PfeifferTCFull from devices.stepperaxis import StepperAxis # <- HERE
Update the test file
tests/test_factory.pyby adding theimportstatement, and the new function
def test_createDigitalOutput(self): c = DeviceFactory.create_device("DigitalOutput") self.assertTrue(issubclass(c, DigitalOutput), "did not create DigitalOutput class")
What to put in the test file¶
It is safe to copy and refactor an existing test file. Here below, is
the description of some of the features in test_digitaloutput.py
def setUp(self):
self.timesourceMock = create_autospec(TimeSource)
attrs = {'generate_time_delta.return_value': 0}
self.timesourceMock.configure_mock(**attrs)
self.pair = None
def tearDown(self):
pass
sets up the mock objects to perform the unit tests. Like:
def test_do_init(self):
d = DigitalOutput("val", self.timesourceMock)
This is one of the simplest tests you can make.
More complicated tests should use the process_pair method
def test_frequency(self):
d = DigitalOutput("val", self.timesourceMock)
p_f = Pair(0x02010101, 0x40000101 ,0)
p_f.add_uint32(0x3dfcd35b)
d.process_pair(p_f)
self.assertAlmostEqual(0.123450, d.get_property('AFrequency'),6)
- This is test does the following steps:
- Creates a device
- Creates a pair object with
plcId0x02010101 (not important),keyId0x40000101 (sokeyId==0x101,AFrequencywith the write bit set),deltaTime0
- adds a floating point in form of a
uint32, alternatively one can add a valuep_f.add_value_as_type(VarType.tREAL, 0.123450) - commands the device to process such pair
- verifies that the property
'AFrequency'is 0.123450
Next an analysis of a more complicated behaviour:
def test_do_onoff(self):
d = DigitalOutput("val", self.timesourceMock)
p_on = Pair(0x02010101, KeyName._key_On , 0)
p_off = Pair(0x02010101, KeyName._key_Off, 0)
d.process_pair(p_on)
self.assertEqual(0x1000, d.get_property('AState')&0x1000)
self.assertTrue(d.get_io('output'))
d.process_pair(p_off)
self.assertEqual(0, d.get_property('AState')&0x1000)
self.assertFalse(d.get_io('output'))
this test verifies that after sending the pairs with commands
_key_On and _key_Off, the proper changes happen in the device
What to put in the device file¶
There are several main parts of a device:
- the PLC properties are contained in the dictionary
self._plc_properties. The definitions should be added in thedefine_property_dictmandatory function. - the Simulated hardware input and output and the parameter are
contained in the dictionary
self._hw_ios. The definitions should be added in thedefine_property_dictmandatory function. - the behaviour of a device is defined by coding the response to a
change of the parameters in handler functions. This functions are
connected to the parameter name they react to, by means of the
dictionary
self._handlers. The mapping of handlers to the corresponding property or command is defined in thedefine_property_dictfunction. - the
initialize_propsfunction containing the default initial configuration.
Here below, is the description of some of the features in test_digitaloutput.py
PLC Properties¶
PLC properties are taken from the PLC code and specified in a
dictionary of Attribute objects called self._plc_properties
class DigitalOutput(BaseDevice):
def __init__(self, name, timesource):
super(DigitalOutput,self).__init__(name, timesource)
self._plc_properties['AFrequency'] = Attribute(name ='AFrequency', no =0x00000101,
varType=VarType.tREAL, access=Access.ExpertRW, \
unit=Unit.HERTZ, prefix=Prefix.NONE, \
description="Base frequency of PWM. 0 means constant"))
...
This was generated from the PLC code here taken from the PLC code file in the PLC framework repository
xfelPlcFramework/POUs/Devices/SD_digitalOut.TcPOU under the array specInst
specInst:ARRAY[1.._maxInst] OF ST_InstructionList:=[
(name:='AFrequency', no:=16#00000101, varType:=tREAL, access:=ExpertRW, unit:=HERTZ, prefix:=NONE),(*Base Frequency of PWM*)
(name:='AHigh', no:=16#00000102, varType:=tREAL, access:=ExpertRW, unit:=PERCENT, prefix:=NONE),(*Percentage of High time for PWM (0--100)*)
(name:='ABlinkLimit', no:=16#00000103, varType:=tINT, access:=ExpertRW, unit:=HERTZ, prefix:=NONE),(*Number of blinks to be done until off again*)
(name:='COff', no:=_key_Off, varType:=tVOID, access:=OperatorRW, unit:=OTHER, prefix:=NONE),(*Disable Output*)
(name:='COn', no:=_key_On, varType:=tVOID, access:=OperatorRW, unit:=OTHER, prefix:=NONE)];(*Enable Output*)
The _plc_properties might be generated from the script
bin/TC2Python.sh. In order to use such a script, copy the
instruction list in the snippet of code above here into a file. Next,
parse the file with the bin/TC2Python.sh filename command.
If the input code is not oddly formatted, the result will be a PEP 8
[1] snippet of code that can be used directly into the __init__
function.
HW I/Os¶
Simulated hardware input and output properties - as well as simulation
parameters that should be steered by the user - are specified in a
dictionary of SimAttribute objects called self._hw_ios
self._hw_ios['output'] = SimAttribute(name='output', varType=VarType.tBOOL, description='Digital Output'))
The logic is similar to self._plc_properties, but the hw_ios require less informations to be functional,
therefore they are initialized with SimAttribute instead of a Attribute specification.
Handlers¶
Handler functions are the beating heart of a beckhoffSim device. Each Command must be implemented in a handler, and every action that needs to happen after setting a variable needs to be coded into a handler for such function. If a PLC property handler is not implemented the following actions will be performed:
- the value is updated,
- the update is sent to the Karabo
BeckhoffSimdevice- a pair with proper update is sent to the TCP layer.
They need to be defined in the self._handlers dictionary in the
__init__ function
self._handlers['COn'] = self._con_handler
self._handlers['COff'] = self._coff_handler
self._handlers['output'] = self._output_handler
Here is the definition of one of them:
def _output_handler(self, value=None):
state = self._plc_properties['AState'].value &~StateBitsDigital.diStateBit
if value == True:
state |= StateBitsDigital.diStateBit
self._plc_properties['AState'].value = state # Internal representation
self._update_property('AState') # beckhoffSim update is triggered in karabo
self._send_property_pair('AState') # Creates a Pair[Keyname] and goes to Tx FiFo -> Tcp
self._hw_ios['output'].value = value
self._update_io('output')
the arguments have to be (self, value=None), in the case of the
hw_ios['output'] property the argument passed is the boolean
value once the business code is done (in this case a change in the
output changes a bit in the plc property
self._plc_properties['AState'].value), the value is changed and
the change is propagated to karabo through the manager object and
finally to the beckhoffSim Karabo Device.
The rule is:
- to communicate a change to the
beckhoffSimfor a given property or hw_ios use the callself._update_property(propname)andself._update_io(ioname); - to communicate a change to the TCP layer and therefore to
BeckhoffCom(might not want to always do it, e.g. epsilon) use the callself._send_property_pair(propname)
Other Mandatory Features in __init__¶
PLC Id Logic¶
This calls ensure that each new device has a unique PLC Id.
self._channel_id = self._get_channel_counter()
self._softdevice_id = self._get_softdevice_counter()
self._increment_counters()
Block Name¶
Each class has a block name, we take this from the PLC description
self._block_name = 'SD_DigitalOut'
Final Calls¶
This calls happen at the end of the __init__ contructor and are
needed to ensure that the dictionaries are properly handled through a
call from the base device.
self._generate_key_dict()
self._initialize()
self._output_manager = PwmOutput(self)
self._update_all()
In this case a threading object PwmOutput is also created.
DeviceId Function¶
# BaseDevice methods/properties overrides
@property
def device_id(self):
return 0x02010000 | ((self._softdevice_id << 8) & 0x0000ff00) | (self._channel_id & 0x000000ff)
needed to make sure that the PLCID of the soft devices are in line with the specification given by the PLC programmers (Each device class has a number)
Initialize Function¶
def _initialize(self):
self.set_property('AFWBlock', self._block_name)
self.set_property('ATerminal', 2)
self.set_property('AName', self._name)
self.set_property('AState', StateBits.enableBit)
self.set_property('AFrequency', 0.)
self.set_property('AHigh', 50.)
self.set_property('ABlinkLimit', 0)
This function initialize the values to a standard configuration. A
call to this function can also be invoked by the corresponding
beckhoffDevice to “Reset Hardware Values”
Footnotes
| [1] | https://www.python.org/dev/peps/pep-0008/ |