OOP in TwinCAT3 – Asynchronous Notifications

Notes:

This blog post is a follow-on to a previous post on implementing domain events (found here). I have replace all references to “events” with “notifications” to avoid confusion with the TwinCAT Eventlogger3 events.

The classes described can be found in the open source library “tcl_BaseClasses” at
https://github.com/RedRockControls/tcl_BaseClasses

This repository also contains an example project.

Introduction

The previous post described a synchronous mechanism for sending notifications from one POU to another. The synchronous behavior means that when a notification is raised during the execution of the POU, the code in the notification handlers of the client POUs executes there and then, in the context of the current POU (i.e in the same task).

Where all POUs share the same context (i.e are called by the same task), there is no risk of inconsistency in the state of the client POUs, as there is no concurrent access. However, if we need to use notifications to pass messages between POUs executed by different task, there is a risk of data inconsistency caused by this concurrent access. The mechanism described below provides a way of removing this risk by enabling a notification raised in one context to be handled in another.

As a side benefit, this solution is much simpler to use. It only requires a single class for notifications and a single class for notification handlers: no sub-classing is required to define different types of notification, just separate instances of these classes.

Recap – Domain Events

The notification mechanism we are discussing is an implementation of Domain Events. It allows the consistent update of POUs of a program based on things that have happened in other unrelated POUs.

The mechanism is based on the publisher/subscriber model:

  • Subscribers register with a publisher.
  • The publisher raises a notification when something interesting happens.
  • Each subscriber receives the notifications, and updates accordingly.

The main benefits are:
1. It allows decoupling of classes as the publisher class needs to know nothing about the subscriber classes.
2. It allows one-to-many communication as multiple client objects can subscribe to a single publishing object.
3. It allow many-to-one communication as a single client object can subscribe to the same notification raised by multiple publisher objects.
4. It allows many-to-many communication – for example many objects can receive a Reset message that is raised by multiple sources (different controls stations or HMIs, etc)

Overview – A Simple Example

The notification represents something interesting happening. Here, a notification is used to indicate that a radioactive nucleus has decayed:

This is handled by a Geiger counter to count the decays:

The notification is an instance of T_Notification. Here it is declared in a global list, but it could be a local variable of the publishing class:

Instances of the publisher and subscriber classes are declared in a global object list:

And the notification handler is linked to the notification during initialisation:

The result being that when the atom decays, the Geiger counter increments.

Notifications

A notification is an instance of the T_Notification class.

Each notification holds a list of notification handlers. Notification handlers are added to this list during program initialization.

Notifications can be declared either as local variables of a publishing object, or in a global list of notifications. A global list of notifications produces a more readable program, at the expense of making POUs harder to test (as it introduces a dependency on global variables).

Notifications have an arguments property that can be used to attach values to the notification by calling methods such as Add_Byte, Add_UDINT, etc. before raising the notification.

The notification’s Raise method iterates through the list of handlers, copying the arguments and calling the raise method on each handler.

Notification Handlers

A notification handler is an instance of the T_NotificationHandler class, declared as a local variable of a subscribing object.

Notification handlers also have an arguments property that is used by the subscriber to read any values that were attached to the notification by calling methods such as read_Byte, Read_UDINT, etc.

Notification handlers implement an IsRaised method. The subscribing object checks the state of the notification by calling this method and checking its return value – if true, it can then query the notification arguments and update its state.

Synchronisation between tasks

The notification object is only accessed by the publishing object, so there is no concurrent access to the notification.

The notification handler object is accessed by the notification object and by the subscribing object, so access to local variables must be synchronized, here using an instance of the FB_IecCriticalSection function block as follows:

When the notification is raised, the RaiseNotification method calls the Enter method of the critical section object. It then writes the notification arguments (if any) to a private copy and sets the Raised state of the notification handler. It then calls the Leave method of the critical section object

While the notification is handled, the IsRaised method calls the Enter method of the critical section object. It then checks the Raised state of the notification handler. If it is raised, it writes the notification arguments from the private copy to a public copy that can be accessed by the subscriber object. In either case, it then clears the raised state of the notification handler.

Notification Arguments

Notification arguments are stored using instances of a memory stream class T_Stream. This class has methods to write data to the stream sequentially (Add_BYTE, Add_UDINT, etc), and to read data from the stream sequentially (Read_BYTE, Read_UDINT, etc). The T_Stream class is initialized with a default length (8 bytes), but will automatically resize as data is added.

Here we modify the previous example by adding a string value and a LREAL value as notification arguments:

Additionally, a pair of methods (SetNextArgTypeId and GetNextArgTypeId) are implements so the type of each argument can be written to the stream before writing it, and read from the stream before reading it to ensure the type read by the subscriber matches the type written by the publisher:

Summary

Notifications and handlers are easy to create, and the benefits of decoupling events from their effects can be significant for large projects.

Additionally, they open up the possibility of writing library classes that raise custom notifications that can be handled in client code without introducing dependencies between the library code and the client code.

One further benefit of this design is that it provides a convenient way of exchanging data between tasks, as it takes care of the data synchronization as part of its implementation.

Last updated 17/08/2021