Statusänderungen vom TwinCAT-Router und der SPS erkennen

Voraussetzungen:

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. 

Statusänderungen vom TwinCAT-Router und der SPS erkennen 1:

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

delphixe2_api_ADS-DLL Sample04.exe

Delphi 5 oder höher (classic)

Sample04.exe