Detect state changes in TwinCAT router and the PLC
Requirements:
- Delphi 5.0 or higher;
- TcAdsDLL.DLL;
- TcAdsDEF.pas and TcAdsAPI.pas, contained in the file delphi_adsdll_api_units.zip, if you want to compile the source code yourself;
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.
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 | |
Delphi 5 or higher (classic) |