Statusänderungen vom TwinCAT-Router und der SPS erkennen
Voraussetzungen:
- Delphi 5.0 oder höher;
- TcAdsDLL.DLL;
- TcAdsDEF.pas und TcAdsAPI.pas, enthalten in der Datei delphi_adsdll_api_units.zip, falls Sie den Quelltext selber übersetzen möchten;
Beschreibung
Zur Laufzeit einer Applikation ist es oft von Bedeutung, den Status von TwinCAT und/oder dessen Komponenten (Devices) abzufragen ( ob die SPS sich z. B. im RUN-Status befindet ). Um die benötigten Statusinformationen nicht immer pollend abzufragen gibt es die Möglichkeit, eine Mitteilung (Notification) bei den TwinCAT Komponenten anzumelden. Diese können dann mit Hilfe von Callback-Funktionen eine Statusänderung an die angemeldeten Client-Applikationen melden. In dem folgenden Beispielprogramm wird der Status der SPS (Laufzeitsystem 1) und der des TwinCAT-Routers auf dem lokalem Rechner überwacht. Die Applikation kann nur den Status des TwinCAT-Routers auf dem lokalem Rechner überwachen. D.h. der Status eines Routers auf einem Remote-PC kann auf diese Weise nicht überwacht werden. Der Status der SPS kann dagegen sowohl auf dem lokalen Rechner als auch auf einem Remote-PC mit den vorgestellten Funktionen überwacht werden.
Beim Start der Applikation wird eine Verbindung zum TwinCAT Router auf dem lokalen PC aufgebaut. Durch das Betätigen der entsprechenden Tasten wird eine Notification entweder an den TwinCAT-Router oder an das erste Laufzeitsystem der SPS angemeldet. Die ankommenden Mitteilungen (Callbacks) werden zwei Listen hinzugefügt. Über weitere Tasten können die Notifications bei den jeweiligen TwinCAT Geräten (Devices) gelöscht werden. Beim Beenden der Applikation wird die Verbindung zum TwinCAT Router geschlossen. Durch Starten/Stoppen des TwinCAT Systems über die Taskleiste werden z. B. die Router-Notifications und durch Starten/Stoppen der SPS die Device-Notifications gesendet. Die angemeldeten Device-Notifications werden von den TwinCAT Komponenten selbst verwaltet. Aus diesem Grund müssen die Device-Notifications beim Stoppen des TwinCAT Systems neu angemeldet werden, da die Komponente (hier das Laufzeitsystem der SPS) beim TwinCAT System-Stop aus dem Speicher entfernt wird.
Delphi 5 Programm
In der Event-Funktion FormCreate wird die DLL-Funktion AdsPortOpenaufgerufen. Beim Erfolg liefert diese Funktion eine Portnummer, sonst eine Null. Eventuelle Fehler werden über eine Message-Box an den Benutzer ausgegeben. Wurde der Ads-Port erfolgreich geöffnet, dann wird eine weitere DLL-Funktion aufgerufen: AdsGetLocalAddress. Diese liefert die AMS-Addresse des lokalen TwinCAT-PC's ( auf dem unsere Applikation läuft ) zurück. Um die Notifications beim ersten Laufzeitsystem der SPS anzumelden wird noch eine weitere AMS-Addresse generiert. Diese besitzt die gleiche NetId wie unsere Applikation, weil wir die SPS auf dem gleichen TwinCAT-PC ansprechen wollen. Durch Zuweisung der Portnummer 801 wird das erste LZS ausgewählt. Beim Beenden der Anwendung muß der Port wieder geschlossen werden. In der Event-Funktion FormDestroy wird dabei die DLL-Funktion AdsPortClose aufgerufen.
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;
Anmelden/Abmelden der Router-Notifications
Eine Notification beim Router wird durch den Aufruf der Funktion AdsAmsRegisterRouterNotification() angemeldet. Die Router-Notification kann immer nur bei dem Router auf dem lokalen TwinCAT-PC angemeldet werden. Als einziger Funktionsparameter wird ein Funktionszeiger auf unsere Callback-Funktion ( in unserem Fall ist es eigentlich eine Prozedur ) übergeben. Über den Funktionsaufruf AdsAmsUnRegisterRouterNotification() wird die Notification vom TwinCAT-Router gelöscht.
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;
Anmelden/Abmelden der Device-Notifications
Die Device-Notifications werden nicht an einer zentralen Stelle, sondern von der TwinCAT Komponente (Device) selbst verwaltet. Die Device-Notifications können auch bei einem Remote TwinCAT-PC angemeldet werden. Das Zielgerät wird dabei über die sogenannte AdsAms-Adresse ausgewählt. Die Notification wird durch den aufruf der DLL-Funktion AdsSyncAddDeviceNotificationReq() angemeldet. Vorher müssen die Attribute der Mitteilung festgelegt werden. Diese werden als Parameter neben dem Funktionszeiger der Callback-Routine an die DLL-Funktion übergeben. Beim Erfolg liefert die Funktion kein Fehler und ein Notification-Handle. Dieses Handle wird dann benötigt um die Notification mit der DLL-Funktion AdsSyncDelDeviceNotificationReq() zu löschen. Eine Applikation kann gleichzeitig mehrere Notifications anmelden. Jede Notification kann über ein Notification-Handle oder über ein User-Handle identifiziert werden. Diese Parameter werden immer in der Callback-Funktion an die Applikation zurückgesendet.
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;
Die Callback-Funktionen
Die Callback-Funktionen für den Router-Callback und den DeviceNotification-Callback wurden als Proceduren definiert. Es können aber genauso Funktionen sein. Da die C++ Funktionen keine Rückgabeparameter zurückliefern und in Pascal eine Funktion immer einen Parameter zurückliefern muß, wurden für die Callback-Funktionen Proceduren gewählt. Sie können in der Callback-Funktion nicht erneut eine weitere DLL-Funktion aufrufen. Diese könnte dann einen weiteren Callback auslösen usw. Um die Callback-Funktionen zu entkoppeln und mit dem Thread der Anwedung zu synchronisieren wurden PostMessage-API-Funktionen benutzt. Die Router-Callback-Daten werden an den Applikations-Thread direkt über die Message-Parameter übergeben. Die Daten der Device-Notification werden an den Applikations-Thread über eine Thread-Sichere-Liste übergeben.
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;
Sprache / IDE | Beispielprogram auspacken |
---|---|
Delphi XE2 | |
Delphi 5 oder höher (classic) |