Join us today!

Observer Design Pat...
 
Notifications
Clear all

Observer Design Pattern

5 Posts
3 Users
7 Reactions
999 Views
twinControls
Posts: 114
Admin
Topic starter
(@twincontrols)
Member
Joined: 2 years ago

Observer pattern is one the behavioral design patterns and it defines a one-to-many relationship between objects so that if a state has changed in one object, its dependent objects are updated and notified automatically. 

For instance, if you subscribe a forum on our community, you get a notification whenever there is a new post. 

twincontrolsmembers

 

notifymembers

 

To give another real world example, let's assume that you want to move to a new city and you need to rent a place. You visit numerous apartment buildings and provide the leasing offices your email address so they can contact you if an apartment becomes available. Since there will be more people like you who's looking for a place, all of the subscribers will get notified by the leasing office. In this scenario, we can consider you and others to be the OBSERVER/SUBSCRIBER and all the leasing offices to be the SUBJECT/PUBLISHER.

 

Observer Pattern class diagram:

classdiagramObserverPattern

 

Now we are going to use this design pattern in TwinCAT 3 for our sample PLC application. Let's imagine we have a scanner station and we are scanning parts one by one. We would like to have for now two HMI screens to display the current station data and the station data statistics. 

Current station data HMI page will display the last result of scan (PASS or FAIL) and the total number of scanned parts count. 

Station data statistics HMI page will display the total pass and failed part count. You can implement other logics to display more data if you like. 

ScannerStation

 

We will have a function block called FB_StationData as a PUBLISHER and it will notify all the HMI Screen function blocks after every new scan so that the values get updated in the HMI screens. 

We will have an interface for the publisher and subscribers. FB_StationData will implement I_Publisher interface, which would have add a subscriber, remove a subscriber and notify subscribers methods. 

FB_CurrentStationDataHMIScreen as a subscriber will implement I_Subscriber and it will have an update method. 

We will define an array of I_Subscriber in FB_StationData and use the M_Update method of subscribers to notify them all whenever new data becomes available. 

The class diagram for our sample application can be seen below: 

Class Diagram observerPattern

 

The subject/publisher (FB_StationData) only knows that an observer/subscriber implements a certain interface. (I_Subscriber). It doesn't need to know anything else about the subscriber. 

By using this design, we can add new subscribers at any time as long as the array size(cMaxSubscribers) is enough. We can also replace the subscribers or remove them at runtime. 

If we decide to have a new publisher, we could easily add another publisher function block by implementing the I_Publisher. 

We can use the publisher and subscriber function blocks independently from each other because they are not tightly coupled. We are striving for loosely coupled designs so that we can handle the changes easily in the future. 

Our sample project structure:

projectStructure

 

Let's add the I_Publisher interface :

INTERFACE I_Publisher

M_AddSubscriber : 

METHOD M_AddSubscriber : BOOL
VAR_INPUT
    iSubscriber : I_Subscriber;
END_VAR

 

M_RemoveSubscriber:

METHOD M_RemoveSubscriber : BOOL
VAR_INPUT
    iSubscriber : I_Subscriber;
END_VAR

 

M_NotifySubscribers:

METHOD M_NotifySubscribers : BOOL
VAR_INPUT
END_VAR

 

 

I_Subscriber will have only M_Update method: 

INTERFACE I_Subscriber

M_Update:

METHOD M_Update : BOOL
VAR_INPUT
    stData : ST_ScannerStationData;
END_VAR

 

 

User defined scanner station data structure: ST_ScannerStationData

TYPE ST_ScannerStationData :
STRUCT
    nTotalScannedParts : INT;
    bLastScanResult : BOOL; // True- Pass, False - Fail
END_STRUCT
END_TYPE

 

 

StationParam parameter file which will have a constant for the array size of subscribers. 

{attribute 'qualified_only'}
VAR_GLOBAL CONSTANT
    cMaxSubscribers : INT := 2;
END_VAR

 

 

 FB_StationData will implement the I_Publisher interface since it is responsible for notifying all of the subscribers. It is up to you to implement how the publisher receives the data. For this application, we will have a simulation function block which will output some simulated station data. We will use this data as input to FB_StationData. 

FUNCTION_BLOCK FB_StationData IMPLEMENTS I_Publisher
VAR_INPUT
    stStationData : ST_ScannerStationData;
END_VAR
VAR_OUTPUT
END_VAR
VAR
    _stStationData : ST_ScannerStationData;
    _aSubscribers: ARRAY [1..StationParam.cMaxSubscribers] OF I_Subscriber;
END_VAR
_stStationData := stStationData;

 

M_AddSubscriber

In M_AddSubscriber method, we are checking if the subscriber is already added in the array. If it is a new subscriber, we are adding the object into the array of subscribers then sending the station data using the M_Update method of the subscriber.

METHOD M_AddSubscriber : BOOL
VAR_INPUT
    iSubscriber : I_Subscriber;
END_VAR
VAR
    nIndex : INT := 0;
END_VAR
M_AddSubscriber := FALSE;
IF (iSubscriber = 0) THEN
	RETURN;
END_IF

// is the subscriber already registered?
FOR nIndex := 1 TO StationParam.cMaxSubscribers DO	
	IF (_aSubscribers[nIndex] = iSubscriber) THEN
		RETURN;
	END_IF
END_FOR

// save the subscriber object into the array of subscribers and send the station data
FOR nIndex := 1 TO StationParam.cMaxSubscribers DO
	IF (_aSubscribers[nIndex] = 0) THEN
		_aSubscribers[nIndex] := iSubscriber;
		ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Publisher: Added a subscriber', '');
		_aSubscribers[nIndex].M_Update(_stStationData);
		M_AddSubscriber := TRUE;
		EXIT;					
	END_IF
END_FOR

 

M_RemoveSubscriber:

METHOD M_RemoveSubscriber : BOOL
VAR_INPUT
    iSubscriber : I_Subscriber;
END_VAR
VAR
    nIndex : INT := 0;
END_VAR
M_RemoveSubscriber := FALSE;
IF (iSubscriber = 0) THEN
	RETURN;
END_IF

FOR nIndex := 1 TO StationParam.cMaxSubscribers DO
	IF (_aSubscribers[nIndex] = iSubscriber) THEN
		_aSubscribers[nIndex] := 0;
		ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Publisher: Removed a subscriber', '');
		M_RemoveSubscriber := TRUE;	
	END_IF
END_FOR

 

M_NotifySubscribers

Notifying every subscriber using their M_Update method. 

METHOD M_NotifySubscribers : BOOL
VAR
    nIndex : INT := 0;
END_VAR
FOR nIndex := 1 TO StationParam.cMaxSubscribers DO
	IF (_aSubscribers[nIndex] <> 0) THEN
		_aSubscribers[nIndex].M_Update(stData := _stStationData);
	END_IF
END_FOR

 

 

Now we will add two subscriber function blocks : 

subscribers

 

FB_CurrentStationDataHMIScreen

This function block will display the total scanned part count and the last scan result. 

FUNCTION_BLOCK FB_CurrentStationDataHMIScreen IMPLEMENTS I_Subscriber
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    _iPublisher : I_Publisher;
    _stData : ST_ScannerStationData;

    sDisplayMessage : STRING;
    sTemp1 : STRING;
    sTemp2 : STRING;
END_VAR
sTemp1 := CONCAT('Total Scanned Part Count :', INT_TO_STRING(_stData.nTotalScannedParts));

sTemp2 := CONCAT(' Last Scan Result :',  SEL(_stData.bLastScanResult,'FAIL','PASS') );

sDisplayMessage := CONCAT(sTemp1, sTemp2);

ADSLOGSTR(ADSLOG_MSGTYPE_HINT, sDisplayMessage, '');

 

M_Update: 

METHOD M_Update : BOOL
VAR_INPUT
	stData	: ST_ScannerStationData;
END_VAR
_stData := stData;

 

 In the FB_Init method, we will pass the I_Publisher reference so that a subscriber can register itself whenever a subscriber function block is initialized. We are also storing the I_Publisher reference. We can use this to unregister ourselves if we want to. 

METHOD FB_init : BOOL
VAR_INPUT
    bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
    bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
    iPublisher : I_Publisher;
END_VAR
IF iPublisher <> 0 THEN
    _iPublisher := iPublisher;
    _iPublisher.M_AddSubscriber(iSubscriber := THIS^);
END_IF

 

We also added a getter property. 

PROPERTY P_G_Data : ST_ScannerStationData

 Get:

VAR
END_VAR

P_G_Data := _stData;

 

 

FB_StationDataStatisticsHMIScreen: 

This function block will display the total pass and fail part counts. 

FUNCTION_BLOCK FB_StationDataStatisticsHMIScreen IMPLEMENTS I_Subscriber
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    _iPublisher : I_Publisher;
   _stData : ST_ScannerStationData;

    nTotalPassPartCount : INT;

    sDisplayMessage : STRING;
    sTemp1 : STRING;
    sTemp2 : STRING;
END_VAR
//implement station data statistic logic here// 
IF _stData.bLastScanResult THEN
    nTotalPassPartCount := nTotalPassPartCount + 1;
    _stData.bLastScanResult := FALSE;
END_IF

sTemp1 := CONCAT('Total Pass Part Count :',  INT_TO_STRING( nTotalPassPartCount) );

sTemp2 := CONCAT(' Total Failed Part Count :', INT_TO_STRING( _stData.nTotalScannedParts - nTotalPassPartCount));

sDisplayMessage := CONCAT(sTemp1, sTemp2);

ADSLOGSTR(ADSLOG_MSGTYPE_HINT, sDisplayMessage, '');

 

M_Update: 

METHOD M_Update : BOOL
VAR_INPUT
	stData	: ST_ScannerStationData;
END_VAR
_stData := stData;

 

FB_init : 

METHOD FB_init : BOOL
VAR_INPUT
    bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
    bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
    iPublisher : I_Publisher;
END_VAR
IF iPublisher <> 0 THEN
    _iPublisher := iPublisher;
    _iPublisher.M_AddSubscriber(iSubscriber := THIS^);
END_IF

 

P_G_Data property:

PROPERTY P_G_Data : ST_ScannerStationData

 Get:

VAR
END_VAR

P_G_Data := _stData;

 

 

To simulate the station data, we will have a function block called FB_Simulation. Every 2s it will increase the total scanned part count and randomly generate a scan result (PASS,FAIL). It will ouput the data and a new data update flag. 

FB_Simulation

FUNCTION_BLOCK FB_Simulation
VAR_INPUT
END_VAR
VAR_OUTPUT
	stData : ST_ScannerStationData;
	bNewDataUpdate : BOOL;
END_VAR
VAR
	fbDelay				: TON;
	fbDrand				: DRAND;
END_VAR
fbDelay(IN := TRUE, PT := T#2S);
bNewDataUpdate := FALSE;
IF (fbDelay.Q) THEN	
	fbDelay(IN := FALSE);	
	fbDrand(SEED := 1);
	stData.nTotalScannedParts := stData.nTotalScannedParts + 1;
	stData.bLastScanResult := fbDrand.Num  > 0.5;
	bNewDataUpdate := TRUE;
END_IF

 

 

In the MAIN program, declare a station data and simulation function block,  and two HMI screen subscriber function blocks: 

VAR
	fbSimulation : FB_Simulation;
	
	fbStationData : FB_StationData; //Publisher
	fbCurrentStationDataHMIScreen : FB_CurrentStationDataHMIScreen(fbStationData); // Subscriber 1
	fbStationDataStatisticsHMIScreen : FB_StationDataStatisticsHMIScreen(fbStationData); // Subscriber 2
	
	bRegister : BOOL;
	bUnregister: BOOL;
END_VAR
fbSimulation(stData=> , bNewDataUpdate=> );
fbStationData(stStationData:= fbSimulation.stData); //provide data into the publisher from the simulation

//If there is an update notify the users and call subscriber FBs 
IF fbSimulation.bNewDataUpdate THEN
	fbStationData.M_NotifySubscribers();
	fbCurrentStationDataHMIScreen();
	fbStationDataStatisticsHMIScreen();
END_IF


IF bRegister THEN
	fbStationData.M_AddSubscriber(fbCurrentStationDataHMIScreen);
	fbStationData.M_AddSubscriber(fbStationDataStatisticsHMIScreen);
	bRegister := FALSE;
END_IF


IF bUnregister THEN
	fbStationData.M_RemoveSubscriber(fbCurrentStationDataHMIScreen);
	fbStationData.M_RemoveSubscriber(fbStationDataStatisticsHMIScreen);
	bUnregister := FALSE;
END_IF

 

Whenever the simulation tells us that new data is available, we are calling the notify subcribers method of the fbStationData and subscriber function blocks. 

 

Run the application and observer the message outputs. Also, set the bUnregister and bRegister to TRUE to unregister or register all the subscribers. 

result observerPattern

 

You can try adding more station data and new subscribers in order to comprehend this design better. 

In this application, we have used the PUSH method, which means all the subscribers are getting notified with all the data. You can try implementing PULL method, which means a subscriber pulls the data from the publisher whenever it needs. You may use the I_Publisher reference that we pass in the FB_Init of the subscriber function blocks to achieve this.  

 

 

Reply
4 Replies
4 Replies
runtimevictor
(@runtimevictor)
Joined: 2 years ago

Estimable Member
Posts: 156

@twincontrols ,

Hello, thank you very much for another example of Design Patterns,

the M_Update method could not be deleted and in the P_G_Data property add the setter and what if the code of the M_Update method was there??...

going to the next level with Design Patterns...

Reply
twinControls
Admin
(@twincontrols)
Joined: 2 years ago

Member
Posts: 114

@runtimevictor Hello, You're welcome! P_G_Data is there just in case if you need to get the subscribers data somewhere else. We want subscriber's data to be updated by only publishers using the M_Method and passing a subscriber reference into this method. If you add the setter, something else can set the data as well which would cause conflicts . 

Reply
 Alex
(@alex)
Joined: 2 years ago

Eminent Member
Posts: 30

@twincontrols 

Thanks for sharing this useful design patterns.

May we use it to extend a MC_TouchProbe ? In that way, as soon as a new "RecoredPosition" is recorded, MC_TouchProbe infroms  that a new data is available.

Reply
twinControls
Admin
(@twincontrols)
Joined: 2 years ago

Member
Posts: 114

@alex Hi, you are welcome!

Yes, that's possible. You can either do;

FUNCTION_BLOCK FB_MyPublisher EXTENDS MC_TouchProbe IMPLEMENTS I_Publisher
//logic here to detect new recorded position, using SUPER^.RecordedPosition maybe
IF bIsNewPositionRecorded THEN
    M_NotifySubscribers();
END_IF;

 

Or provide the MC_TouchProbeRecordedData and the notify flag to your publisher FB through inputs or properties.

Or you can inject your MC_TouchProbe function block to your Publisher FB using the FB_Init or a property. 

I would do the third method to keep the responsibilities separate and be able to use a different MC_TouchProbe function block at runtime if I want to in the future.   

It would be great if you can share your implementation with the community once you are done.

Reply
Share: