BeckhoffSim Implementation Spec

Karabo Team

Introduction

BeckhoffSim is a TCP server that simulates a Beckhoff PLC on TCP protocol level. It is used to test BeckhoffCom and BeckhoffDevices without the need to have actual PLC hardware up and running.

BeckhoffSim is a Python Karabo device. It is composed of several building blocks, which are implemented as separate classes: the Manager, the TcpServer, a TimeSource, and the various soft devices inheriting from BaseDevice (e.g. DigitalInput).

The Manager maintains a set of soft devices, generates the self description, and decodes/encodes TCP messages. The TcpServer opens a TCP port, to which a BeckhoffCom instance can connect, and sends and receives messages. The TimeSource generates train ID and timestamp information. Finally, the soft devices simulate the behaviour of their PLC counterpart.

The association between the building blocks in unidirectional, i.e. that method calls are defined only in one direction. Information flow into the opposite direction is realized by events. Blocks interested in a specific event can subscribe to it by passing in a method, which is called if the event is fired.

Manager

The manager implements the core logic of the BeckhoffSim device. Main task is to maintain a set of simulated soft devices and handle the message transfer between these devices and the TCP server. Each soft device has a set of properties and hardware I/Os, which are exposed by the manager as expected parameters of the BeckhoffSim device. Soft device commands are not exposed, so that they can only be executed via the TCP server.

The manager also generates the self description when a BeckhoffCom connects to the TCP server, and sends the heartbeat in regular intervals.

The manager is implemented in the class Manager from module core.manager. It has several dependencies, namely

  • Framework
  • TCP server
  • Time source

The dependencies are injected as constructor arguments. Using dependencies is essential for unit testing, since then and only then they can be mocked[#]_. Dependencies are usually defined as interfaces[#]_. Since Python does not need a formal interface contract as e.g. Java or C#, the interface and it’s implementation are usually not separated into different classes.

[1]A mock replaces a concrete class with an object that has the same methods and properties, but no behaviour by itself. Mocks can be configured by specifying return values, and one can make assertions about how they have been used.
[2]Python does not have the notion of an interface like Java. Therefore an interface is implemented as an abstract base class (see also abc).

Method Interface

The Manager class provides the following public methods:

initialize()

subscribe to events from the TcpServer and by starting the PLC uptime counter

shutdown()

stops the TcpServer and properly shuts down the PLC uptime counter

start(port)

starts a TCP server on the given port. If no port is given, the default 1234 is used.

stop()

stops the TCP server

add_device(type, name)

adds a soft device of the specified type with the specified name to the list of simluated soft devices, also injects exp. parameters for the device into the schema of BeckhoffSim

set_property(device_name, prop_name, value)

sets a named PLC property on the specified device

set_io(device_name, io_name, value)

sets a named hardware I/O property on the specified device

Features

This section describes the features, provided by the Manager class, in more detail.

Adding Devices

Simulated soft devices are created using a factory. The factory method returns a device class depending on the specified type. The device type is a string that is also used in the related expected parameter of BeckhoffSim. The instance of the device class is stored into a dictionary using the device name as the key.

Once the device is added to the dictionary, the Manager queries the list of properties and hardware I/Os from the device class instance. It then calls Framework methods to inject the properties and I/Os as expected parameters into the schema of BeckhoffSim.

Note: PLC commands are intentionally not injected as expected parameters of BeckhoffSim.

Networking

When Manager.start() is called, the TCP server is started, and when Manager.stop() is called or if the Manager is destructed, the TCP server is stopped.

When the TCP server is running, it fires events to indicate the occurence of a certain condition. The Manager therfore subscribes to the following events from the TCP server:

error event
is fired in case of errors on the TCP socket
connect event
is fired, when a client has connected to the TCP server
disconnect event
is fired, when a client has disconnected from the TCP server

In case of a connect event, the Manager starts a thread to generate heartbeat messages each 10s. It also generates the self description message from it’s set of soft devices, and passes it to the TCP server for sending the message to the network.

In case of a disconnect event, the Manager stops the heartbeat thread.

Time Source

The Time Source generates following time-dependent information that is used in the messages:

  • Time Stamp. Current time in seconds since the epoch as an integer value.
  • Time Fraction. Fractional part of the time as an integer with 100ns as a unit.
  • Train ID. A unique ID of the current XFEL shot.
  • Delta Time. The time difference between the start of message generation and
arrival of a response from a device. An integer value with 100ns as a unit.

Method Interface

A TimeSource class provides the following public methods:

initialize()

starts a thread generating a new Train ID every 100ms

shutdown()

stops the thread generating a new Train ID every 100ms

get_time_stamp()

returns the Time Stamp

get_time_frac()

returns the Time Fraction

get_train_id()

returns the Train ID

generate_time_delta():

increases the current Time Delta by 1ms +- 0.1ms and returns this value

reset_time_delta()

sets the current Time Delta to 0, the function should be called at the beginning of message generation

TCP Server

The TCP Server opens a TCP socket and binds it to a port. It then starts to listen on that socket for client messages. If needed, it sends server messages on that socket.

Messages are simply stream of bytes. There is no further processing of the message itself within the TCP server.

The TCP server is implemented in the class TcpServer from module core.TcpServer. It has no further dependencies on other classes.

Method Interface

The TcpServer class provides the following public methods:

start(port)

starts a TCP server on the given port

stop()

stops the TCP server

send(message)

sends a message over the network

subscribe_errorEvent(handler)

subscribes to the error event by adding the specified handler

subscribe_connectEvent(handler)

subscribes to the connect event by adding the specified handler

subscribe_disconnectEvent(handler)

subscribes to the disconnect event by adding the specified handler

Features

The features implemented within the TcpServer class are described in more detail in the following sections.

Events

The TcpServer defines a set of events, which are fired asynchronously:

connect event
is fired, when a client has connected to the TCP server
disconnect event
is fired, when a client has disconnected from the TCP server
error event
is fired in case of errors on the TCP socket

Socket Thread

The socket related code is run in a separate thread. Data exchange with this thread is via 2 queues: an input queue for data received on the socket, and an output queue for data to be sent on the socket.

The main loop of the thread listens on the socket for client data. The listen operation times out after 1ms to inspect the output queue. If the output queue is not empty, the messages therein are sent over the socket. Then the loop starts over again.

The loop terminates, when TcpServer.stop() is called. This also terminates the socket thread.

Receiving

When messages from connected clients are received, the receive event is fired containing the message as the event argument.

Sending

If the TcpServer.send(...) method is called, the message is pushed onto the output queue, if a TCP connection has been established. If there is no TCP connection, then the error event is fired.

Devices

Devices simulate the behaviour of PLC soft devices. They have the same properties than their PLC counterpart, but in addition they possibly also have hardware inputs and outputs. Properties and I/Os are annotated with meta information, such as name and value type.

PLC properties and hardware I/Os are implemented as Property class from module devices.property. The annotated meta information is a named tuple BeckhoffKey for PLC properties, and PropertySpec for I/O properties.

Devices are implemented as classes derived from a base class BaseDevice from module devices.base. For each PLC soft device, there is an associated simulated device. Currently, the following devices are supported:

  • DigitalInput

Methods and Properties

A device class provides the following public methods:

subscribe_update_property_event(handler)

subscribes to the update event for PLC properties by adding the specified handler

subscribe_update_io_event(handler)

subscribes to the update event for hardware I/Os by adding the specified handler

append_class_self_description(message)

appends the class description to an existing message

set_property(name, value)

sets the named PLC property to the specified value

get_property(name)

returns the value of the named property

set_io(name, value)

sets the named hardware I/O to the specified value

get_io(name)

returns the value of the named hardware I/O

A device class provides the following public properties:

Features

The features implemented within the device class are described in more detail in the following sections.

PLC Properties

The device provides a set of PLC properties and commands, which are identical to the ones in the corresponding PLC soft device. Each one has a unique name, which usually starts with capital ‘C’ for commands, and with capital ‘A’ for properties. The supported types are listed in the following table:

type description
tBOOL boolean
tBYTE 8 bit unsigned integer
tSINT 8 bit signed integer
tWORD 16 bit unsigned integer
tINT 16 bit signed integer
tDWORD 32 bit unsigned integer
tDINT 32 bit signed integer
tULINT 64 bit unsigned integer
tLINT 64 bit signed integer
tREAL 32 bit floating point (float)
tLREAL 64 bit floating point (double)
tSTRING ASCII byte array
tVOID command
tMULTI array of 32 bit unsigned integer

The distinction between commands (something that can be executed) and properties (something that is associated with a value) is done solely based on whether the variable type is tVOID or not.

The meta information for PLC commands and properties are stored as named tuple BeckhoffKey and contain name, id, type, access, unit, and prefix.

PLC properties and commands are stored in a dictionary, where the name is the key and the value is an instance of a Property class. The Property class consists of the meta information and a place to store the value. The dictionary is accessable from outside via Property.properties.

The set_property() method is used to modify the value of a PLC property. The name, passed in as an argument, is used to look up in the self._properties dictionary to reference the Property instance in order to write to it’s value. Additional specific device behaviour needs to be implemented afterwards.

The get_property() method is used to return the actual value of a PLC property. The name, passed in as an argument, is used to look up in the self._properties dictionary to reference the Property instance in order to read it’s value.

Hardware I/O

The device provides hardware inputs and outputs of the corresponding PLC soft device. Each one has a unique name, and can be of one of the types, as mentioned with PLC properties, except tSTRING, tVOID and tMULTI.

The meta information for Hardware I/Os are stored as named tuple PropertySpec and contain name, and type.

Hardware I/Os are stored in a dictionary, where the name is the key and the value is an instance of a Property class. The Property class consists of the meta information and a place to store the value. The dictionary is accessable from outside via Property.ios.

The set_io() method is used to modify the value of a hardware input. The name, passed in as an argument, is used to look up in the self._hw_ios dictionary to reference the Property instance in order to write to it’s value. Additional specific device behaviour needs to be implemented afterwards.

The get_io() method is used to return the actual value of a hardware input or output. The name, passed in as an argument, is used to look up in the self._hw_ios dictionary to reference the Property instance in order to read it’s value.

Messages

A message contains key value pairs prepended with a short header. The message header contains timestamp and train ID. Each key value pair consists of two numbers, one to identify the soft device instance, and one to identify the property or command on that instance, followed by 0 to 20 values.

Before sending messages over the network, they need to be converted into byte streams. Received byte streams need to be converted back into messages.

Messages are implemented in the Message and the Pair classes from module core.message.

Method Interface

The Pair class provides the following public methods:

add_uint32(pair)

adds an integer value to the pair (maximal 32 bit)

add_string(pair)

adds a string value to the pair (maximal 80 characters, including terminating ‘0’)

to_bytes()

returns the pair as a byte stream

The Message class provides the following public methods:

add_pair(pair)

adds the specified pair to the message

to_bytes()

returns the message as a byte stream

Features

The features implemented within the Message and Pair classes are described in more detail in the following sections.

Converting to Byte Stream

In order to send messages over the network, they must be converted into a stream of bytes. The format of the byte stream is defined by [FT2015]. Conversion is handled by the Message.to_bytes() method, which returns a byte stream representation of the message.

[FT2015]T. Freyermuth, J. Tolkiehn, “European XFEL PLC Karabo Interface Description,” internal note IN-2015-09-30, European XFEL, 2015

FiFo

The FiFo class basically mocks the PLC behavior inside the beckhoffSim project. Totally, two FiFo instances are required to establish the communication. One TX fifo (transmitter) for the message parsing between the Manager and the TcpServer and an additional RX (receiver) fifo which is filled with messages coming from the TcpServer. The messages are subsequently dispatched in the Manager.

Technically, the FiFo class uses queue objects for thread safe communication. The maxsize member variable has been set to infinity. Instead, for size handling a private member variable _max_bytecount is been set during initialization. The private member variable _current_bytecount is used to observe the current size of the queue. After each method the current bytecount is updated.

Method Interface

The FiFo class provides the following public methods:

try_put(item)

Safe method to put an item into the fifo. The bytecount of the item is checked before.

try_get()

Safe method to return an item from the fifo. Only returns an item if the bytecount of the fifo is greater than zero.

put(item)

Puts an item into the fifo. If the bytecount of the item together with the current bytecount of the fifo exceeds the max bytecount and OverFlowException is raised.

get()

Returns an item from the fifo. If the current bytecount inside the fifo is equal to zero, an EmptyBufferException is raised.

clear()

Atomically locks the fifo and clears the fifo list of items.

__len__()

Overwritten built-in function to provide the current bytecount inside the fifo.

BeckhoffSim

BeckhoffSim is the top level class. It has a lightweight implementation by simply passing down all method calls to the Manager. In addition, it implements the Framework interface, which is used by the Manager to interact with the Karabo framework, for example to inject expected parameters.

State Machine

The state machine of BeckhoffSim is fairly simple. It only has a Started, Stopped and Error state. The state reflects the state of the TCP server. If the TCP server is running, then BeckhoffSim is in Started state, when the TCP server is not running, then BeckhoffSim is in Stopped state. If an error occurs during the start of the TCP server, or while the TCP server is running, BeckhoffSim changes into the Error state.

Instantiation

BeckhoffSim has an expected parameter to configure simulated soft devices. It can only be set before instantiation. Is is a TABLE_ELEMENT where the device type and instance name can be specified.

When BeckhofSim is instantiated, it goes through the list of configured soft devices, calls Manager.add_device() for each of them, and then Manager.initialize().

Finally, BeckhoffSim changes into the Stopped state, and the expected parameters for each device are injected below the Devices node[#]_. If however, an exception was thrown while calling one of the Manager methods, BeckhoffSim changes into the Error state.

[3]Due to a bug in Karabo framework (see #10897), users need to activate Apply all once to have value updates on the newly injected parameters.

Shutdown

When BeckhoffSim is shut down (or killed), it simply shuts down the Manager by calling Manager.shutdown().

Reconfiguration

If expected parameters of the configured soft devices are changed, the preReconfigure() hook is called. Within the hook, the changed device properties and I/Os are extracted and the manager methods set_io() and set_property() are called respectively.

Commands

BeckhoffSim provides 2 commands:

Start
starts the TCP server at the configured port
Stop
stops the TCP server

The port for the TCP server is configured using an expected parameter. It can be changed at runtime, but the TCP server needs to be restarted in order to take effect.

Appendix

File Structure

The code is organized into packages:

core
contains modules defining the manager, TCP Server, and classes associated with the manager
devices
contains modules to define types, enums, base device, and soft devices
tests
contains the unit tests

Coding Style

The BeckhoffSim code follows the guidelines as proposed by PEP 8 Style Guide available under PythonStyleGuide.

Unit Tests

Most part of BeckhoffSim is unit tested. Unit tests are also run as part of Continuous Integration (CI). In order to get test reporting on the CI server, XML runner is required. Since it is not yet part of the Karabo Python library[#]_, you need to install it into your user account by running

[4]see bug report #10818
pip install --user unittest-xml-reporting

Afterwards, it might be necessary to add the user site-packages directory to PYTHONPATH, which is

::
~/.local/lib/python3.4/site-packages

under Linux, and

::
%APPDATA%PythonPython34site-packages

under Windows [1].

[5]%APPDATA% resolves to \\win.desy.de\home\<user>\Application Data.