Detect state changes in TwinCAT router and the PLC

Requirements:

Description

When an application is actually running it is often important to query the state of TwinCAT and/or of its components (devices) ( e.g., whether the PLC is in the RUN state). So that the required state information is not always checked by polling there is the possibility of registering a notification at the TwinCAT components. These can then register with the aid of callback functions a state change to the client applications that are being addressed. In the following sample program the state of the PLC (runtime system 1) and that of the TwinCAT router on the local computer is monitored. The application can monitor only the state of the TwinCAT router on the local computer. In other words, the state of a router on a remote PC cannot be monitored in this manner. The state of the PLC can however be monitored both on the local computer and on a remote PC using the functions presented. 

With the start of the application a connection is made to the TwinCAT router on the local PC.  A notification is registered by pressing the appropriate keys, either to the TwinCAT router, or to the first runtime system of the PLC. The arriving messages (callbacks) are added to two lists. Using further keys the notifications can be canceled on the respective TwinCAT devices. With termination of the application the connection to the TwinCAT router is closed. By starting/stopping of the TwinCAT system via the taskbar the router notifications are, for example, transmitted, and by starting/stopping of the PLC the device notifications are transmitted. The registered device notifications are administered by the TwinCAT components themselves.  For this reason the device notifications must again be registered when the TwinCAT system is stopped, since the component (here the runtime system of the PLC) is removed from the memory when the TwinCAT system is stopped. 

Detect state changes in TwinCAT router and the PLC 1:

Delphi 5 program

The DLL function AdsPortOpen is called in the FormCreate event function. This function returns a port number when successful, otherwise it returns a zero. If any errors do occur, they are reported to the user through a message box. If the Ads port is opened successfully, a further DLL function is called: AdsGetLocalAddress. This returns the AMS address of the local TwinCAT PC (on which our application is running). In order to register the notifications for the first runtime system of the PLC another further AMS Address is generated. This possesses the same NetId as our application, because we wish to address the PLC on the same TwinCAT PC. By assignment of the port number 801 the first RTS is selected. The port must be closed again when closing the application. The DLL function AdsPortClose is called in the FormDestroy event function.

unit AdsDLLEventSampleForm;
interface
uses
  TcAdsDEF, TcAdsAPI, Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Math,
  StdCtrls, ComCtrls;
const WM_ADSROUTERNOTIFICATION = WM_APP + 400;
const WM_ADSDEVICENOTIFICATION = WM_APP + 401;
type
  TForm1 = class(TForm)
    StatusBar1: TStatusBar;
    GroupBox1: TGroupBox;
    RegRouterNotify: TButton;
    UnregRouterNotify: TButton;
    ListBox1: TListBox;
    GroupBox2: TGroupBox;
    AddDevNotify: TButton;
    DelDevNotify: TButton;
    ListBox2: TListBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure RegRouterNotifyClick(Sender: TObject);
    procedure UnregRouterNotifyClick(Sender: TObject);
    procedure AddDevNotifyClick(Sender: TObject);
    procedure DelDevNotifyClick(Sender: TObject);
    procedure WMAdsRouterNotification( var Message: TMessage ); message WM_ADSROUTERNOTIFICATION;
    procedure WMAdsDeviceNotification( var Message: TMessage ); message WM_ADSDEVICENOTIFICATION;
  private
    { Private declarations }
    LocalAddr     :TAmsAddr;
    ServerAddr    :TAmsAddr;
    hNotification :DWORD;
  public
    { Public declarations }
  end;
{$ALIGN OFF}
type TThreadListItem = record
     netId       : TAmsNetId;
     port        : Word;
     hNotify     : Longword;
     stamp       : _FILETIME;  {int64}
     cbSize      : Longword;
     hUser       : Longword;
     data        : Byte;    //Array[1..ANYSIZE_ARRAY] of Byte;
     end;
PThreadListItem = ^TThreadListItem;
{$ALIGN ON}
var
  Form1: TForm1;
  wndHandle : HWND;
  DevThreadList : TThreadList;
implementation
{$R *.DFM}
//////////////////////////////////////////////////////////////////////////////
procedure TForm1.FormCreate(Sender: TObject);
var AdsResult:Longint;
    ClientPort:Word;
begin
  GroupBox1.Caption := 'Router notifications:';
  GroupBox2.Caption := 'Device notifications:';
  StatusBar1.SimplePanel := true;
  wndHandle := Handle;
  DevThreadList := TThreadList.Create();
  ClientPort:= AdsPortOpen();
  if ClientPort = 0 then {error!}
    ShowMessage( 'AdsPortOpen() error!' )
  else  {OK}
  begin
    AdsResult:=AdsGetLocalAddress( @LocalAddr );
    if AdsResult = 0 then {OK}
    begin
      ServerAddr.netId:=LocalAddr.netId;{connect to the PLC on the local PC}
      ServerAddr.port:=801; {first RTS}
      StatusBar1.SimpleText:=Format('Client NetId:%d.%d.%d.%d.%d.%d  Client Port:%d,  Server Port:%d',[
          LocalAddr.netId.b[0], LocalAddr.netId.b[1], LocalAddr.netId.b[2],
          LocalAddr.netId.b[3], LocalAddr.netId.b[4], LocalAddr.netId.b[5],
          ClientPort, ServerAddr.port]);
    end
    else
      ShowMessageFmt('AdsGetLocalAddress() error:%d', [AdsResult]);
  end;
end;
//////////////////////////////////////////////////////////////////////////////
procedure TForm1.FormDestroy(Sender: TObject);
var AdsResult:longint;
    X : integer;
begin
  UnregRouterNotifyClick( Sender );
  DelDevNotifyClick( Sender );
  With DevThreadList.LockList do
  try
     for X := 0 to Count-1 do
     FreeMem( Items[X], sizeof(TThreadListItem) + PThreadListItem(Items[X])^.cbSize - 1 );
     Clear();
  finally
     DevThreadList.UnlockList;
     DevThreadList.Destroy;
  end;
  AdsResult := AdsPortClose();
  if AdsResult > 0 then
    ShowMessageFmt('AdsPortClose() error:%d', [AdsResult]);
end;

Register/unregister the router notifications

A notification by the router is registered by the call of the function AdsAmsRegisterRouterNotification(). The router notification can only be registered by the router on the local TwinCAT PC. As a single function parameter a function pointer is transferred to our callback function (in our case it is actually a procedure).  By means of the function call AdsAmsUnRegisterRouterNotification() the notification is unregistered from the TwinCAT router.

procedure TForm1.RegRouterNotifyClick(Sender: TObject);
var AdsResult:longint;
begin
  AdsResult:= AdsAmsRegisterRouterNotification(@AdsRouterCallback);
  if AdsResult > 0 then
    ListBox1.Items.Insert(0, Format('AdsAmsRegisterRouterNotification() error:%d', [AdsResult]));
end;
//////////////////////////////////////////////////////////////////////////////
procedure TForm1.UnregRouterNotifyClick(Sender: TObject);
var AdsResult:longint;
begin
  AdsResult:=AdsAmsUnRegisterRouterNotification();
  if AdsResult > 0 then
    ListBox1.Items.Insert(0, Format('AdsAmsUnRegisterRouterNotification() error:%d', [AdsResult]));
end;

Register/unregister the device notifications

The device notifications are not administered in a central location but by the TwinCAT component (device) itself. The device notifications can also be registered from a remote TwinCAT PC. Here the target device is selected via the so-called AdsAms address. The notification is registered by the call of the DLL function AdsSyncAddDeviceNotificationReq(). The attributes of the message must be determined beforehand. These will transfer to the DLL function as parameters alongside the function pointer for the callback routine. If successful the function supplies no error and a notification handle. This handle is then required for unregister the notification with the DLL function AdsSyncDelDeviceNotificationReq(). An application can register several notifications at the same time. Each notification can be identified via a notification handle or a user handle. These parameters are always sent back in the callback function to the application.

procedure TForm1.AddDevNotifyClick(Sender: TObject);
var AdsResult:Longint;
    huser :Longword;
    adsNotificationAttrib :TadsNotificationAttrib;
begin
  adsNotificationAttrib.cbLength       := sizeof(DWORD);
  adsNotificationAttrib.nTransMode     := ADSTRANS_SERVERONCHA;
  adsNotificationAttrib.nMaxDelay      := 0; // jede Aenderung sofort melden
  adsNotificationAttrib.nCycleTime     := 0; //
  hUser := 7;
  AdsResult:=AdsSyncAddDeviceNotificationReq( @ServerAddr,
            ADSIGRP_DEVICE_DATA,
            ADSIOFFS_DEVDATA_ADSSTATE,
            @adsNotificationAttrib,
            @AdsDeviceCallback, hUser, @hNotification  );
  if AdsResult > 0 then
    ListBox2.Items.Insert(0, Format('AdsSyncAddDeviceNotificationReq() error:%d', [AdsResult]));
end;
//////////////////////////////////////////////////////////////////////////////
procedure TForm1.DelDevNotifyClick(Sender: TObject);
var AdsResult:Longint;
begin
  AdsResult := AdsSyncDelDeviceNotificationReq( @ServerAddr, hNotification );
  if AdsResult > 0 then
    ListBox2.Items.Insert(0, Format('AdsSyncDelDeviceNotificationReq() error:%d', [AdsResult]));
end;

The callback functions

The callback functions for the router callback and the device notification callback have been defined as procedures. They could equally well be defined as functions. Since however the C++ functions do not supply any return parameters and in Pascal a function must always supply a parameter, procedures were selected for the callback functions. In other words, they cannot again call a further DLL function in the callback function. This could then trigger another callback and so on. To decouple the callback functions and synchronize them with the application thread, PostMessage API functions were used. The router callback data is passed to the application thread directly via the message parameters. The device notification data is passed to the application thread via a thread safe list.

Procedure AdsDeviceCallback(     pAddr:PAmsAddr; pNotification:PAdsNotificationHeader;   hUser:Longword ); stdcall;
var pItem : PThreadListItem;
begin
    pItem := Nil;
    try
       GetMem( pItem, sizeof(TThreadListItem) + pNotification^.cbSampleSize - 1 );
       pItem^.netId := pAddr^.netId;
       pItem^.port := pAddr^.port;
       pItem^.hNotify := pNotification^.hNotification;
       pItem^.stamp := pNotification^.nTimeStamp;
       pItem^.cbSize := pNotification^.cbSampleSize;
       pItem^.hUser := hUser;
       //copy the notification data
       Move( pNotification^.data, pItem^.data, pNotification^.cbSampleSize);
    finally
    with DevThreadList.LockList do
    try
       Add( pItem );
    finally
       DevThreadList.UnlockList;
       PostMessage(wndHandle, WM_ADSDEVICENOTIFICATION, 0, 0);
    end;
    end;
end;
//////////////////////////////////////////////////////////////////////////////
procedure AdsRouterCallback( nEvent:Longint ); stdcall;
begin
  PostMessage(wndHandle, WM_ADSROUTERNOTIFICATION, nEvent, 0);
end;

 

procedure TForm1.WMAdsRouterNotification( var Message: TMessage );
var tmpString:String;
begin
  case Message.WParam of
    AMSEVENT_ROUTERSTOP:
      tmpString:='Router STOP!';
    AMSEVENT_ROUTERSTART:
      tmpString:='Router START!';
    AMSEVENT_ROUTERREMOVED:
      tmpString:='Router REMOVED!';
  else
      tmpString:=Format('Unknown state:%d', [Message.WParam]);
  end;
  ListBox1.Items.Insert(0, Format('%s %s', [TimeToStr(time), tmpString]));
  inherited;
end;
//////////////////////////////////////////////////////////////////////////////
procedure TForm1.WMAdsDeviceNotification( var Message: TMessage );
var pItem           : PThreadListItem;
    X               : integer;
    FileTime        : _FILETIME;
    SystemTime, LocalTime   : _SYSTEMTIME;
    TimeZoneInformation     : _TIME_ZONE_INFORMATION;
    DateTime        : TDateTime;
    adsState        : Smallint;
    sState          : String;
    cbData          : Longword;
begin
    with DevThreadList.LockList do
    try
       for X := 0 to Count-1 do
       begin
       pItem := Items[X];
       {convert file time to local system time}
       FileTime:=pItem^.stamp;
       FileTimeToSystemTime(FileTime, SystemTime);
       GetTimeZoneInformation(TimeZoneInformation);
       SystemTimeToTzSpecificLocalTime(@TimeZoneInformation, SystemTime, LocalTime);
       DateTime:=SystemTimeToDateTime(LocalTime);
       cbData :=Min(pItem^.cbSize, sizeof(adsState));
       System.Move( pItem^.data, adsState, cbData );
       case adsState of
        ADSSTATE_STOP:  sState := 'STOP';
        ADSSTATE_RUN:  sState := 'RUN';
       else
        sState := Format('Other: %d', [adsState]);
       end;
       ListBox2.Items.Add(Format( '%s %d.%d.%d.%d.%d.%d[%d], hNot:0x%x, size:%d, hUser:0x%x, State:%s',
                     [TimeToStr(DateTime), pItem^.netId.b[0], pItem^.netId.b[1], pItem^.netId.b[2],
                     pItem^.netId.b[3], pItem^.netId.b[4], pItem^.netId.b[5], pItem^.port,
                     pItem^.hNotify, pItem^.cbSize, pItem^.hUser,
                     sState ]));
       FreeMem( pItem, sizeof(TThreadListItem) + pItem^.cbSize - 1 ); //free item memory
       end;
       Clear();
    finally
       DevThreadList.UnlockList;
    end;
    inherited;
end;

Language / IDE

Unpack sample program

Delphi XE2

delphixe2_api_ADS-DLL Sample04.exe

Delphi 5 or higher (classic)

Sample04.exe