TwinCAT TcAdsDLL: Delphi examples

Detect status changes in TwinCAT router and the PLC

Requirements:

Description

When an application is actually running it is often important to interrogate the status of TwinCAT and/or of its components (devices) ( e.g., whether the PLC is in the RUN state). So that the required status information is not always checked by polling there is the possibility of registering a message (notification) at the TwinCAT components. These can then register with the aid of callback functions a status change to the client applications that are being addressed. In the following example program the status of the PLC (run time system 1) and that of the TwinCAT router on the local computer is monitored. The application can monitor only the status of the TwinCAT router on the local computer. In other words, the status of a router on a remote PC cannot be monitored in this manner. The status 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 run-time system of the PLC. The arriving messages (callbacks) are added to two lists. Using further keys the notifications can be cancelled 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 task bar 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 run time system of the PLC) is removed from the memory when the TwinCAT system is stopped.

 

TcAdsDll Delphi Sample04: Detect status 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 run time 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 the application finishes. 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;

Registration/cancelling of 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 cancelled 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;

Registration/cancelling of 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 cancelling of the notification with the DLL function AdsSyncDelDeviceNotificationReq(). The 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.  The function calls of the DLL should be decoupled with the aid of the API functions (e.g. threads). In other words, they cannot again call a further DLL function in the callback function. This could then trigger a further call-back and so on.

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 the sample program
Delphi XE2 Sample04.exe
Delphi 5 or higher (classic) Sample04.exe