How to write a new device

  1. Parenti and A. Silenzi

Introduction

In order to write a new beckhoffSim device type, several adjustments need to be done:

  1. Create a file in the devices folder, e.g. digitaloutput.py

  2. Create a file in the test folder, e.g. test_digitaloutput.py

  3. Add the new device name in the beckhoffSim.py as row_schema option

    STRING_ELEMENT(row_schema).key("type")
        .displayedName("Type")
        .description("Device Type")
        .options("DigitalInput, DigitalOutput, Valve, AnalogInput, AnalogOutput, PfeifferTC, StepperAxis") # <- HERE
        .assignmentOptional().defaultValue("DigitalInput")
        .commit()
    
  4. 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
    
  5. Update the test file tests/test_factory.py by adding the import 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 (so keyId==0x101, AFrequency with the write bit set),
    • deltaTime 0
  • adds a floating point in form of a uint32, alternatively one can add a value p_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:

  1. the PLC properties are contained in the dictionary self._plc_properties. The definitions should be added in the define_property_dict mandatory function.
  2. the Simulated hardware input and output and the parameter are contained in the dictionary self._hw_ios. The definitions should be added in the define_property_dict mandatory function.
  3. 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 the define_property_dict function.
  4. 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 call self._update_property(propname) and self._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 call self._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/