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
devices
folder, e.g.digitaloutput.py
Create a file in the
test
folder, e.g.test_digitaloutput.py
Add the new device name in the
beckhoffSim.py
as 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.py
from 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.py
by adding theimport
statement, 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
plcId
0x02010101 (not important),keyId
0x40000101 (sokeyId==0x101
,AFrequency
with the write bit set),deltaTime
0
- 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_dict
mandatory 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_dict
mandatory 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_dict
function. - the
initialize_props
function 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
BeckhoffSim
device- 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
beckhoffSim
for 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/ |