ADS sum command: reading or writing several variables with Delphi

System requirements:

Task

Read or write several PLC variables with TcAdsClient.ReadWrite command. This sample shows also how to get and enable several PLC variables handles with a ADS sum command.

 

Description

The singles sub commands are moved in an AdsStream (Write Data) to the PLC and executed there in a row. The result is moved with a further AdsStream (Return value + Read Data) from the PLC to the client.

Dynamic arrays can be used to assign a variable number of parameters (ADS sub commands). The Sample uses the dynamic arrays and makes the code universal reusable. Other methods are possible (array with fixed size, structure elements, etc.)

 

The following auxiliary methods are used as generic ADS sum commands to write or read the PLC variables value, to get or enable handles, etc. In these methods the AdsStream for the ADS sum command is filled  with parameters and the ADS sum command is called by the TcAdsClient.ReadWrite command.

method BlockRead(
  ig : Array of Int32;
  io : Array of Int32;
  var rLen : Array of Int32;
  var data : AdsStream
) : Array of Int32;

method BlockWrite(
  ig : Array of Int32;
  io : Array of Int32;
  wLen : Array of Int32;
  data : AdsStream
) : Array of Int32;

method BlockReadWrite(
  ig, io, wLen : Array of Int32;
  wData : AdsStream;
  var rLen : Array of Int32;
  var rData : AdsStream
) : Array of Int32;

 

The generic methods are used by four more methods:

method BlockCreateVariableHandle(names : Array Of String) : Array of Int32;
method BlockDeleteVariableHandle(handles : Array of Int32) : Array of Int32; 
method btnRead_Click(sender: System.Object; e: System.EventArgs);
method btnWrite_Click(sender: System.Object; e: System.EventArgs);

 

Delphi Prism (Embarcadero Prism XE2, Oxygen for .NET) program

namespace Sample09;

interface

uses
  System.Drawing,
  System.Collections,
  System.Collections.Generic,
  System.Windows.Forms,
  System.ComponentModel, System.IO, TwinCAT.Ads;

type
  /// <summary>
  /// Summary description for MainForm.
  /// </summary>
  MainForm = partial class(System.Windows.Forms.Form)
  private
    adsClient : TcAdsClient;
    vNames  : Array of String := ["MAIN.uintValue", "MAIN.dintValue", "MAIN.boolValue"];
    vLengths : Array of Int32 := [2,4,1];
    vHandles : Array of Int32 := [0,0,0];

    method BlockRead(ig : Array of Int32; io : Array of Int32; var rLen : Array of Int32; var data : AdsStream) : Array of Int32;
    method BlockWrite(ig : Array of Int32; io : Array of Int32; wLen : Array of Int32; data : AdsStream) : Array of Int32;
    method BlockReadWrite(ig, io, wLen : Array of Int32;  wData : AdsStream; var rLen : Array of Int32; var rData : AdsStream) : Array of Int32;

    method BlockCreateVariableHandle(names : Array Of String) : Array of Int32;
    method BlockDeleteVariableHandle(handles : Array of Int32) : Array of Int32; 

    method MainForm_Load(sender: System.Object; e: System.EventArgs);
    method MainForm_FormClosing(sender: System.Object; e: System.Windows.Forms.FormClosingEventArgs);
    method btnRead_Click(sender: System.Object; e: System.EventArgs);
    method btnWrite_Click(sender: System.Object; e: System.EventArgs);
  protected
    method Dispose(disposing: Boolean); override;
  public
    const ADSIGRP_SUMUP_READ = $0000F080;       // AdsRW  IOffs list size or 0 (=0 -> list size == WLength/3*sizeof(ULONG))                  // W: {list of IGrp, IOffs, Length}
                    // if IOffs != 0 then R: {list of results} and {list of data}
                    // if IOffs == 0 then R: only data (sum result)

    const ADSIGRP_SUMUP_WRITE = $0000F081;      // AdsRW  IOffs list size                  // W: {list of IGrp, IOffs, Length} followed by {list of data}
                    // R: {list of results}

    const ADSIGRP_SUMUP_READWRITE = $0000F082;// AdsRW  IOffs list size                  // W: {list of IGrp, IOffs, RLength, WLength} followed by {list of data}
                    // R: {list of results, RLength} followed by {list of data}
    constructor;
  end;

implementation

{$REGION Construction and Disposition}
constructor MainForm;
begin
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();
  //
  // TODO: Add any constructor code after InitializeComponent call
  //
end;

method MainForm.Dispose(disposing: Boolean);
begin
  if disposing then begin
    if assigned(components) then
      components.Dispose();
    //
    // TODO: Add custom disposition code here
    //
  end;
  inherited Dispose(disposing);
end;
{$ENDREGION}

 

Create connection

 On starting the application a connection to the PLC is established and variable handles are fetched via theBlockCreateVariableHandlemethod call. The names of the three PLC variables are transferred to theBlockCreateVariableHandlemethod as a dynamic array.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create form, connection and variable handles
method MainForm.MainForm_Load(sender: System.Object; e: System.EventArgs);
begin
  try
    // Connect to PLC
    adsClient := new TcAdsClient();
    adsClient.Connect(801);

    // Create variable handles 
    vHandles := BlockCreateVariableHandle(vNames);

//    Conventional method:
//    for each name in vNames index i do
//      vHandles[i] := adsClient.CreateVariableHandle(name);

  except
    On err : AdsErrorException do
      MessageBox.Show(err.Message, err.Source);
  end;
end;

 

Close connection

On terminating the application the variable handles are released and the connection to the PLC is cut. The handles to be released are transferred to the BlockDeleteVariableHandle method as a dynamic array.

/////////////////////////////////////////////////////////////////////////////////////////////////////////
// Close form and release resources
method MainForm.MainForm_FormClosing(sender: System.Object; e: System.Windows.Forms.FormClosingEventArgs);
begin
  try
    // Release variable handles
    BlockDeleteVariableHandle(vHandles);

//  Conventional method
//    for each handle in vHandles index i do
//      adsClient.DeleteVariableHandle(handle);
    // Close connection
    adsClient.Dispose()
  except
    On err : AdsErrorException do
      MessageBox.Show(err.Message, err.Source)
  end;
end;

 

Read PLC values

The PLC variable values are read by clicking on theReadbutton. ThebtnRead_Click event handling routing uses the generic BlockRead method. However, the instance of the AdsStream for the PLC data must be generated beforehand with a suitable size so that the values of all PLC variables fit into it. The parameters rLen(read lengths)and data (AdsStream) are transferred by reference. As a result, theBlockRead method can fill the AdsStream with data and change the actually returned data lengths (rLen) (e.g.: in case of error). If successful, the data are successively read from the AdsStream and displayed on the form.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Read block data (multiple PLC values)
method MainForm.btnRead_Click(sender: System.Object; e: System.EventArgs);
begin
  var ig := new Int32[vHandles.Length];
  // Calculate the size of PLC value data
  var cbRead : Int32 := 0;
  for each len in vLengths index i do
  begin
    cbRead := cbRead + len;
    ig[i] := Int32(AdsReservedIndexGroups.SymbolValueByHandle);
  end;

  var data := new AdsStream(cbRead);
  var reader := new BinaryReader(data);
  var rLen := new Int32[vLengths.Length];
  rLen := vLengths;

  // Read PLC values
var blockResult := BlockRead(ig, vHandles, var rLen, var data);
  for each adsResult in blockResult index i do
    if ( adsResult <> 0 ) then
      MessageBox.Show("Read failed! Error: " + adsResult.ToString() , "TwinCAT.Ads");

  data.Position := 0;
  tbRuint.Text := iif((blockResult[0] = 0) And (rLen[0] = 2), reader.ReadUInt16().ToString(), "0");
  tbRdint.Text := iif((blockResult[1] = 0) And (rLen[1] = 4), reader.ReadInt32().ToString(), "0");
  tbRbool.Text := iif((blockResult[2] = 0) And (rLen[2] = 1), reader.ReadBoolean().ToString(), "False");
end;

 

Write PLC values

The values are written from the application to the PLC by clicking on the Write button. The btnWrite_Click event handling routine uses the generic BlockWrite method. The instance of the AdsStream is generated before the call with a suitable size and filled with the PLC variable values to be written.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Write block data (multiple PLC values)
method MainForm.btnWrite_Click(sender: System.Object; e: System.EventArgs);
begin
  var ig := new Int32[vHandles.Length];
  // Calculate the size of PLC value data
  var cbWrite : Int32 := 0;
  for each len in vLengths index i do
  begin
    cbWrite := cbWrite + len;
    ig[i] := Int32(AdsReservedIndexGroups.SymbolValueByHandle);
  end;

  var data := new AdsStream(cbWrite);
  var writer := new BinaryWriter(data);

  writer.Write(UInt16.Parse(tbWuint.Text));
  writer.Write(Int32.Parse(tbWdint.Text));
  writer.Write(boolean.Parse(tbWbool.Text));

  // Write PLC values
 var blockResult := BlockWrite(ig, vHandles, vLengths, data);
  for each adsResult in blockResult do
    if adsResult <> 0 then
      MessageBox.Show("Write failed! Error: " + adsResult.ToString() , "TwinCAT.Ads");
end;

 

Create variable handles

The generic BlockReadWrite method is used to fetch the variable handles. The symbol names of the PLC variables are transferred to the BlockReadWrite method as write data in the AdsStream. The handles are received in a further AdsStream as read data. As with the BlockWrite method, the AdsStream: rData (read data) and rLen are transferred by reference here also. Only in this way can the contents of the stream be filled with data from the BlockReadWrite method. If successful, the handles are returned by the method as return parameters in the form of a dynamic array.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create multiple symbol handles
method MainForm.BlockCreateVariableHandle( names : Array Of String ) : Array of Int32;
begin
  var getHandles := new Int32[names.Length];
  var ig := new Int32[names.Length];
  var io := new Int32[names.Length];
  var rLen := new Int32[names.Length];
  var wLen := new Int32[names.Length];
  var cbWrite : Int32 := 0;
  for each name in names do
    cbWrite := cbWrite + name.Length;
  var wData := new AdsStream(cbWrite);
  var writer := new BinaryWriter(wData, System.Text.Encoding.ASCII);

  var rData := new AdsStream(getHandles.length * 4);
  var reader := new BinaryReader(rData);

  for each name in names index i do
  begin
    writer.Write(name.ToCharArray());// Copy PLC variable names to the stream
    ig[i] := Int32(AdsReservedIndexGroups.SymbolHandleByName);// indexGroup
    io[i] := 0;// indexOffset = 0
    rLen[i] := sizeOf(getHandles[i]);// readLen = 4 byte 
    wLen[i] := name.Length;// writeLen = length of plc variable name
  end;

  var blockResult := BlockReadWrite(ig, io, wLen, wData, var rLen, var rData);
  rData.Position := 0;
  for each adsResult in blockResult index i do
  begin
    if (adsResult = 0) and (rLen[i] = 4) then
      getHandles[i] := reader.ReadInt32()
    else
      MessageBox.Show("CreateVariableHandle failed! Error: " + adsResult.ToString() , "TwinCAT.Ads");
  end;

  result := getHandles;
end;

 

Realease variable handles

The method BlockDeleteVariableHandle uses the method  BlockWrite. The handles to be enabled are copied before in the AdsStream (Write-Data).

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Release multiple symbol handles
method MainForm.BlockDeleteVariableHandle(handles : Array of Int32 ): Array of Int32; 
begin
  var data := new AdsStream(handles.Length * 4);
  var writer := new BinaryWriter(data);
  var ig := new Int32[handles.Length];
  var wLen := new Int32[handles.Length];

  for each handle in handles index i do
  begin
    writer.Write(handle);
    ig[i] := Int32(AdsReservedIndexGroups.SymbolReleaseHandle);
    wLen[i] := sizeOf(handle);// = 4 byte
  end;

  var blockResult := BlockWrite(ig, handles, wLen, data);
  for each adsResult in blockResult do
    if adsResult <> 0 then
      MessageBox.Show("DeleteVariableHandle failed! Error: " + adsResult.ToString() , "TwinCAT.Ads");

  result := blockResult;
end;

 

Generic ADS read sum command

The parameter for the ADS sum command consists of:

Arrangement of the ADS sub command inwriteDataduring writing the PLC variables values:

 

 

Arrangement of return data inreadDataduring reading  the PLC variables values:

 

 

The return values of the single ADS subcommands are extracted from thereadData-Stream and returned fromtheBlockWritemethod as return value. The read data (e.g. PLC variables values) are copied into the data method parameter.

The rLen values are aligned ( in error case the data length = Null).

 

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Ads block read
method MainForm.BlockRead( ig : Array of Int32; io : Array of Int32; var rLen : Array of Int32;  var data : AdsStream) : Array of Int32;
begin
  var count := ig.Length;
  var blockResult := new Int32[count];
  try
    var writeData := new AdsStream(count * 12);
    var writer := new BinaryWriter(writeData);
    var cbData : Int32 := 0;

    for each len in rLen index i do
    begin
      writer.Write(ig[i]);
      writer.Write(io[i]);
      writer.Write(len);
      cbData := cbData + len;
    end;

    if cbData <> data.Length then
      MessageBox.Show("BlockRead()::Invalid read data length!", "TwinCAT.Ads");

    var readData := new AdsStream((count * 4) + data.Length);
    var reader := new BinaryReader(readData);

    adsClient.ReadWrite(ADSIGRP_SUMUP_READ, count, readData, writeData);

    readData.Position := 0;
    for each adsResult in blockResult index i do
      blockResult[i] := reader.ReadInt32();

    // Copy read data
    var copy := new BinaryWriter(data);
    data.Position := 0;
    for each adsResult in blockResult index i do
    begin
      if adsResult = 0 then// no error => copy data, error => don't copy data
    copy.Write(reader.ReadBytes(rLen[i]))
      else
    rLen[i] := 0;
    end;
  except
    On err : AdsErrorException do
      MessageBox.Show(err.Message, err.Source)
  end;

  result := blockResult;
end;

 

Generic ADS write sum command

The parameter of the ADS write sum command consists of:

TheBlockWrite method is used in two cases ( in this sample):

Thecontent of data is copied into writeData.

 

Arrangement of the ADS sub command inwriteDataduring writing the PLC variables values:

 

 

Arrangement of return data inreadDataduring reading the PLC variables values:

 

 

The return values of the single ADS subcommands are extracted from thereadData-Stream and returned fromtheBlockWritemethod as return value.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Ads block write
method MainForm.BlockWrite(ig : Array of Int32; io : Array of Int32; wLen : Array of Int32; data : AdsStream) : Array of Int32;
begin
  var count := ig.Length;
  var blockResult := new Int32[count];
  try
    var writeData := new AdsStream((count * 12 ) + data.Length);
    var writer := new BinaryWriter(writeData);
    var cbData : Int32 := 0;

    for each len in wLen index i do
    begin
      writer.Write(ig[i]);
      writer.Write(io[i]);
      writer.Write(len);
      cbData := cbData + len;
    end;

    if cbData <> data.Length then
      MessageBox.Show("BlockWrite()::Invalid write data length!", "TwinCAT.Ads");

    writer.Write(data.ToArray());

    var readData := new AdsStream(count * 4);
    var reader := new BinaryReader(readData);

    adsClient.ReadWrite(ADSIGRP_SUMUP_WRITE, count, readData, writeData);
    readData.Position := 0;
    for each adsResult in blockResult index i do
      blockResult[i] := reader.ReadInt32();

  except
    On err : AdsErrorException do
      MessageBox.Show(err.Message, err.Source)
  end;

  result := blockResult;
end;

 

Generic ADS read/write sum command

The BlockReadWrite method is a combination ofBlockRead  andBlockWritemethod. The parameter of the ADS readwrite sum command consists of:

The BlockReadWrite method is used in this example when fetching the handles. In this case the wData method parameter contains the strings with the PLC symbol names. The symbol names are copied into writeData before the call. The handles are located in readData after the call and are copied from there into the rData method parameter. The rLen values are also adapted (in case of error the data length = zero). The return values of the individual ADS sub-commands are extracted from the readData stream and returned by the BlockReadWrite method as a return value.

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Ads block read/write
method MainForm.BlockReadWrite( ig, io, wLen : Array of Int32;  wData : AdsStream;
                  var rLen : Array of Int32; var rData : AdsStream) : Array of Int32;
begin
  var count := ig.Length;
  var blockResult := new Int32[count];
  try
    var writeData := new AdsStream((count * 16) + wData.Length);// 16 = sizeof(indexGroup + indexOffset + readLen + writeLen)
    var writer := new BinaryWriter(writeData);
    var cbWData : Int32 := 0;
    var cbRData : Int32 := 0;

    for each len in wLen index i do
    begin
      writer.Write(ig[i]);
      writer.Write(io[i]);
      writer.Write(rLen[i]);
      writer.Write(wLen[i]);
      cbWData := cbWData + wLen[i];
      cbRData := cbRData + rLen[i];
    end;

    if cbRData <> rData.Length then
      MessageBox.Show("BlockReadWrite()::Invalid read data length!", "TwinCAT.Ads");

    if cbWData <> wData.Length then
      MessageBox.Show("BlockReadWrite()::Invalid write data length!", "TwinCAT.Ads");

    writer.Write(wData.ToArray());

    var readData := new AdsStream((count * 8) + rData.Length);
    var reader := new BinaryReader(readData);

    adsClient.ReadWrite(ADSIGRP_SUMUP_READWRITE, count, readData, writeData);

    readData.Position := 0;
    for each adsResult in blockResult index i do
    begin
      blockResult[i] := reader.ReadInt32();
      rLen[i] := reader.ReadInt32();
    end;

    // Copy data
    var copy := new BinaryWriter(rData);
    rData.Position := 0;
    for each adsResult in blockResult index i do
    begin
      if adsResult = 0 then// no error => copy data, error => don't copy data
    copy.Write(reader.ReadBytes(rLen[i]));
    end;

  except
    On err : AdsErrorException do
      MessageBox.Show(err.Message, err.Source)
  end;

  result := blockResult;
end;

end.

 

PLC program

PROGRAM MAIN
VAR
    uintValue : UDINT := 54321;
    boolValue : BOOL := TRUE;
    dintValue : DINT := -12345678;
END_VAR
;

 

Download

Language / IDE

Unpack the sample program

Delphi Prism (Embarcadero Prism XE2, Oxygen for .NET)

Sample09.exe