========================= How to write a new device ========================= A. 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 option .. code-block:: python STRING_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`` .. code-block:: python 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 the ``import`` statement, and the new function .. code-block:: python 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`` .. code-block:: python 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: .. code-block:: python 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 .. code-block:: python 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: .. code-block:: python 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 the ``define_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 the ``define_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 the ``define_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`` .. code-block:: python 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`` .. code-block:: none 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 [#f1]_ 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`` .. code-block:: python 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 .. code-block:: python 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: .. code-block:: python 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. .. code-block:: python 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 .. code-block:: python 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. .. code-block:: python 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 ----------------- .. code-block:: python # 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 ------------------- .. code-block:: python 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" .. rubric:: Footnotes .. [#f1] https://www.python.org/dev/peps/pep-0008/