Home / PLC / Allen Bradley / How to communicate to an Allen Bradley Plc with C# and LibPlcTag Ethernet/IP library

How to communicate to an Allen Bradley Plc with C# and LibPlcTag Ethernet/IP library

LibPlcTag is a library that I used recently to communicate with Allen Bradley plc. It’s a C++ open source library and it can communicate with most of the Allen Bradley plcs, like Micrologix, CompactLogix, ControlLogix, SLC and Plc5.

LibPlcTag works on the Ethernet/Ip stack and is LGPL licensed (you are not forced to open source the entire project, free for commercial use).

This library is very easy to use and in this article we will see how to use a C# wrapper that I wrote time ago.

The C/C++ source code of Libplctag is hosted on Github, along with the documentation and issue tracker: https://github.com/kyle-github/libplctag

The C# wrapper is also on GitHub

Watch the video on Youtube

How to compile LibPlcTag

LibPlcTag can be compiled with CMake. You can read the documentation with all the steps on the Build.md file.

C# Wrapper

When you use a native C/C++ library in a C# project, you need a wrapper that contains the P/Invoke of the functions exposed by the C++ library.

There are several approaches that can be taken when writing a wrapper. I personally don’t like to expose C/C++ native methods, instead I usually keep all interop boilerplate inside the wrapper and expose only plain C# functions.

Just for reference, here are the wrapped functions of the C/C++ library. You can find the documentation about these functions on Libplctag Wiki.

[DllImport("plctag.dll", EntryPoint = "plc_tag_create", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr plc_tag_create([MarshalAs(UnmanagedType.LPStr)] string lpString);

[DllImport("plctag.dll", EntryPoint = "plc_tag_destroy", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_destroy(IntPtr tag);

[DllImport("plctag.dll", EntryPoint = "plc_tag_status", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_status(IntPtr tag);

[DllImport("plctag.dll", EntryPoint = "plc_tag_decode_error", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr plc_tag_decode_error(int error);

[DllImport("plctag.dll", EntryPoint = "plc_tag_read", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_read(IntPtr tag, int timeout);

[DllImport("plctag.dll", EntryPoint = "plc_tag_write", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_write(IntPtr tag, int timeout);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_uint16", CallingConvention = CallingConvention.Cdecl)]
static extern ushort plc_tag_get_uint16(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_int16", CallingConvention = CallingConvention.Cdecl)]
static extern short plc_tag_get_int16(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_uint16", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_uint16(IntPtr tag, int offset, ushort val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_int16", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_int16(IntPtr tag, int offset, short val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_uint8", CallingConvention = CallingConvention.Cdecl)]
static extern byte plc_tag_get_uint8(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_int8", CallingConvention = CallingConvention.Cdecl)]
static extern sbyte plc_tag_get_int8(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_uint8", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_uint8(IntPtr tag, int offset, byte val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_int8", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_int8(IntPtr tag, int offset, sbyte val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_float32", CallingConvention = CallingConvention.Cdecl)]
static extern float plc_tag_get_float32(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_float32", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_float32(IntPtr tag, int offset, float val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_uint32", CallingConvention = CallingConvention.Cdecl)]
static extern uint plc_tag_get_uint32(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_get_int32", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_get_int32(IntPtr tag, int offset);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_uint32", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_uint32(IntPtr tag, int offset, uint val);

[DllImport("plctag.dll", EntryPoint = "plc_tag_set_int32", CallingConvention = CallingConvention.Cdecl)]
static extern int plc_tag_set_int32(IntPtr tag, int offset, int val);

How to use the wrapper

To use the wrapper you have to add the project manually to your sources. Just copy-paste the project in your solution folder, Right-click on Solution -> add existing project -> select libplctag.csproj file. Then add a reference.

Create a Tag

Libplctag is built around the concept of tags. A tag is a memory region on the plc that you can read/write. To create a Tag you have to specify the name (that is the initial address), element size (in bytes) and number of consecutive elements that you want to read/write.

  • Name: name of the tag in the plc, like N7:0, F8:10, I0:0.2, etc…
  • ElementSize: size of a single element of the tag. For example a float in F8 is 4 bytes, a numeric in N7 is 2 bytes, etc…
  • ElementCount: number of consecutive elements that we want to read

To create a Tag you can use two constructors.
One for PLC5/SLC/MicroLogix, where you don’t have to specify the path.
The second one is for CompactLogix and ControlLogix plc, where you have to specify the path.

/// <summary>
/// Creates a tag for PLC5 / SLC / MicroLogix processor types (you don't need 
/// to specify the path)
/// </summary>
public Tag(string ipAddress, CpuType cpuType, string name, int elementSize, 
    int elementCount, int debugLevel = 0)	

/// <summary>
/// Creates a tag. If the CPU type is LGX, the path has to be specified.
/// </summary>
public Tag(string ipAddress, string path, CpuType cpuType, string name, 
    int elementSize, int elementCount, int debugLevel = 0)

Here is an example on how to create a Tag for a MicroLogix plc. In this case we read the tag B3:0, 1 element:

// creates a tag to read B3:0, 1 item, from SLC ip address 192.168.0.100
var tag = new Tag("192.168.0.100", CpuType.SLC, "B3:0", DataType.Int16, 1);

Here is another example on how to create a tag for a ControlLogix/CompactLogix plc. In this case we read the first byte of a DINT array named TestDINTArray. As you see we have to specify the path.

// creates a tag to read B3:0, 1 item, from LGX ip address 192.168.0.100
var tag = new Tag("10.206.1.39", "1, 0", CpuType.LGX, "TestDINTArray[0]", 
    DataType.Int32, 1);

In case of CompactLogix/ControlLogix tags, you can also specify tags that are in different networks, like DH+, and on different plcs.
Let’s suppose we want to read a tag from a ControlLogix named TestDINTArray – the first 10 elements.
Then we want to read the tag F8:0 – first 4 elements – from a PLC5 connected to the ControlLogix with a DH+ network.
For the second tag we have to specify the nodes where we find the DH+ slot and the PLC5.
Here is how we create the two tags:

// creates a tag to read TestDINTArray, 10 item, from controllogix
var tag1 = new Tag("10.206.1.39", "1, 0", CpuType.LGX, "TestDINTArray[0]", 
    DataType.Int32, 10);

// creates a tag to read F8:0, the first 4 consecutive elements, from PLC5 via DH+
// see https://github.com/kyle-github/libplctag/blob/master/src/examples/simple_dual.c
var tag2 = new Tag("10.206.1.39", "1,2,A:27:1", CpuType.PLC5, "F8:0", 
    DataType.Float32, 4);

Read tags

To read and write tags you have to create an instance of the plc client.

var client = new Libplctag();

Then tags has to be added to the client. After adding a tag, you have to call GetStatus to verify that the tag exists and the status code is ok.
In some cases the library may return “pending”. In that case you have to put a retry mechanism that call GetStatus again until the result is ok.

// create the tag (We want to read TestDINTArray[0], TestDINTArray[1], 
// TestDINTArray[2],TestDINTArray[3])
var tag = new Tag("10.206.1.39", "1, 0", CpuType.LGX, "TestDINTArray[0]", 
    DataType.Int32, 4);

// add the tag
client.AddTag(tag);

// check that the tag has been added, if it returns pending we have to retry
while (client.GetStatus(tag) == Libplctag.PLCTAG_STATUS_PENDING)
{
    Thread.Sleep(100);
}

// if the status is not ok, we have to handle the error
if (client.GetStatus(tag) != Libplctag.PLCTAG_STATUS_OK)
{
    Console.WriteLine($"Error setting up tag internal state. Error
        {client.DecodeError(client.GetStatus(tag))}\n");
    return;
}

After adding the tags and verifying that the status is ok, you can use the tags to read or write.

Read is done by calling the ReadTag method. After the read, you have to check that the status code returned by the ReadTag is ok. Then you have to convert the result to the format that you are interested in.

// Execute the read
var result = client.ReadTag(tag, DataTimeout);

// Check the read operation result
if (result != Libplctag.PLCTAG_STATUS_OK)
{
    Console.WriteLine($"ERROR: Unable to read the data! Got error code {result}: {client.DecodeError(result)}\n" );
    return;
}
// Convert the data
var TestDintArray0 = client.GetInt32Value(tag, 0 * tag.ElementSize); // multiply with tag.ElementSize to keep indexes consistant with the indexes on the plc
var TestDintArray1 = client.GetInt32Value(tag, 1 * tag.ElementSize);
var TestDintArray2 = client.GetInt32Value(tag, 2 * tag.ElementSize);
var TestDintArray3 = client.GetInt32Value(tag, 3 * tag.ElementSize);

// print to console
Console.WriteLine("TestDintArray0: " + TestDintArray0);
Console.WriteLine("TestDintArray1: " + TestDintArray1);
Console.WriteLine("TestDintArray2: " + TestDintArray2);
Console.WriteLine("TestDintArray3: " + TestDintArray3);

Write tags

Same as for Read tags, a tag has to be added and checked with GetStatus before it becomes possible to write it.

To write a tag we use the method WriteTag, but first we have to assign the value of the tag, converted of course.
Here is the example on how to write the first 4 elements of TestDINTArray.

// set values on the tag buffer
client.SetInt32Value(tag, 0 * tag.ElementSize, 10); // write 10 on TestDINTArray[0]
client.SetInt32Value(tag, 1 * tag.ElementSize, 20); // write 20 on TestDINTArray[1]
client.SetInt32Value(tag, 2 * tag.ElementSize, 30); // write 30 on TestDINTArray[2]
client.SetInt32Value(tag, 3 * tag.ElementSize, 40); // write 40 on TestDINTArray[3]
	
// write the values
result = client.WriteTag(tag, DataTimeout);

// check the result
if (result != Libplctag.PLCTAG_STATUS_OK)
{
    LogError($"ERROR: Unable to read the data! Got error code {rc}: {client.DecodeError(result)}\n" );
    return;
}

Close and cleanup resources

The Libplctag implements IDisposable, so you must call Dispose() before closing your program.
Dispose will remove all the tags and clear the dictionary. Internally in the C++ library, all tags pointers are destroyed and sockets are released.

Eventually if you want to remove a tag during the runtime you can use the method RemoveTag() to dispose it.

Value conversion

Here are the methods to convert the values contained in a tag.

public ushort GetUint16Value(Tag tag, int offset);
public void SetUint16Value(Tag tag, int offset, ushort value);

public short GetInt16Value(Tag tag, int offset);
public void SetInt16Value(Tag tag, int offset, short value);

public byte GetUint8Value(Tag tag, int offset);
public void SetUint8Value(Tag tag, int offset, byte value);

public sbyte GetInt8Value(Tag tag, int offset);
public void SetInt8Value(Tag tag, int offset, sbyte value);

public float GetFloat32Value(Tag tag, int offset);
public void SetFloat32Value(Tag tag, int offset, float value);

public uint GetUint32Value(Tag tag, int offset);
public void SetUint32Value(Tag tag, int offset, uint value);

public int GetInt32Value(Tag tag, int offset);
public void SetInt32Value(Tag tag, int offset, int value);

Decoding error codes

LibPlcTag returns several error codes. Error codes can be translated to a string by using DecodeError();
For example, this code prints the error code number and the description:

var result = ...
if (result != Libplctag.PLCTAG_STATUS_OK)
{
	string error = client.DecodeError(result);
    Console.WriteLine($"ERROR: Unable to read the data! Got error code" + result +": " + error);
    return;
}

Limitations and gotcha

You might have some problems with this library especially when you try to request more than the limit of bytes allowed in a single request (80 bytes if I remember well).
In that case the library just fails to read/write and returns you an error code.
This means that you have to check and decode the error code and understand what it means.
Also you have to execute multiple reads/writes depending on the limits of the processor.

Source code and examples

The repository with the wrapper and examples is on GitHub: https://github.com/mesta1/libplctag-csharp
The C++ library can be found also on GitHub: https://github.com/kyle-github/libplctag, along with the official examples in C++.

Getting help and documentation

If you need help with LibPlcTag, there is an official Google Group where you can find the developer and other users that can help you. Here is the link: https://groups.google.com/forum/#!forum/libplctag
The official LibPlcTag Wiki is at this addres: https://github.com/kyle-github/libplctag/wiki
And be sure to read the long description on the ReadMe file: https://github.com/kyle-github/libplctag/blob/master/README.md

One comment

  1. Hello I was wondering, I was using rslogix 5000, studio 5000 emulate, and rslinx classic gateway. I have in emulate a softlogix driver configured to run an emulated plc on a virtual back plane. I can set up an OPC topic and copy the link into excel to read and write values. I tried to use the control logix set up to possibly get comunicaiton with MS visual studio working. my goal is to use 3d models in unity3d to interact based on tag values from logixs, and write to them as the operator interacts with model. following the video I dont get past creating a tag. i am using my computers IP address with the path 1,2 as the emulated plc is in slot 2. any pointers would be very helpful

Leave a Reply

Share13
Tweet
Share
+1