Skip to content

Commit

Permalink
Deprecate Mapper system (#407)
Browse files Browse the repository at this point in the history
* Mark PlcMapper / Tag<M,T> as obsolete, and at it as an example.
* Add an alternative implementation (in examples) for code reuse that uses inheritance.
* Clean up Examples to reflect this change.
* Remove a layer of indirection from Tag / NativeTagWrapper (directly expose NativeTagWrapper as Tag)
  • Loading branch information
timyhac authored Jul 26, 2024
1 parent daab80c commit 2e13641
Show file tree
Hide file tree
Showing 60 changed files with 2,899 additions and 1,947 deletions.
86 changes: 18 additions & 68 deletions docs/libplctag.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[libplctag.NET](https://www.nuget.org/packages/libplctag/) provides wrapper packages for libplctag, with an API naturalised to .NET by adding the following features:

* Values are strongly-typed (both Atomic types and User-Defined Types).
* Values are strongly-typed
* Errors are thrown as Exceptions
* Async/Await
* Native resource cleanup
Expand All @@ -13,7 +13,7 @@

```csharp
// Example tag configuration for a global DINT tag in an Allen-Bradley CompactLogix/ControlLogix PLC
var myTag = new TagDint()
var myTag = new Tag()
{
Name = "SomeDINT",
Gateway = "10.10.10.10",
Expand All @@ -22,14 +22,16 @@ var myTag = new TagDint()
Protocol = Protocol.ab_eip
};

// Read the value from the PLC and output to console
int output = myTag.Read();
Console.WriteLine($"Original value: {output}");
// Read the value from the PLC
myTag.Read();
int originalValue = myTag.GetInt32(0);
Console.WriteLine($"Original value: {originalValue}");

// Write a new value to the PLC, then read it back, and output to console
myTag.Write(37);
output = myTag.Read();
Console.WriteLine($"Updated value: {output}");
// Write a new value to the PLC
int updatedValue = 1234;
myTag.SetInt32(0, updatedValue);
myTag.Write();
Console.WriteLine($"Updated value: {updatedValue}");
```

See the examples projects for further detail and usage:
Expand Down Expand Up @@ -57,7 +59,7 @@ Just tags.

Read more on the [libplctag wiki](https://github.com/libplctag/libplctag/wiki/API).

## `Tag`
## `libplctag.Tag`

libplctag.NET provides a wrapper for the C API naturalised for .NET.
The `Tag` class is intended to be functionally equivalent to the C API.
Expand All @@ -71,69 +73,17 @@ For example:
Some methods are presented slightly differently due to the differences in languages and language idioms.
For example, the counterpart to `Initialize(..)` is [`plc_tag_create(..)`](https://github.com/libplctag/libplctag/wiki/API#creating-a-tag-handle) and the tag attributes are specified as properties (e.g. `Tag.Path`).

### Data Accessors

## `Tag<M,T>` and Mappers
Mapping the raw tag buffer to some typed value (e.g. `int`) and vice-versa can be achieved using the built-in [Data Accessor methods](https://github.com/libplctag/libplctag/wiki/API#tag-data-accessors).
Alternatively, get a copy of the byte array with `GetBuffer(..)` and do the conversion yourself (e.g. with [`BitConverter`](https://learn.microsoft.com/en-us/dotnet/api/system.bitconverter), [`BinaryPrimities`](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.binary.binaryprimitives), [`Encoding`](https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding), or manually).

In your .NET application, you will usually need to convert the raw bytes into a .NET type.
It is possible to use `GetInt32()` and `SetInt32()` (and [others](https://github.com/libplctag/libplctag/wiki/API#tag-data-accessors)) provided by the `Tag` class to perform this conversion, and most of the time, there will only be one sensible way to interpret these bytes for a given tag.

For example, a `DINT` tag defined in a PLC is a 32bit signed integer, and this would be exposed as a little-endian encoded 4-byte array.
The natural choice for a C# type would be `int` - it would be rare to want to work with this data as a `float` or a 4-byte ASCII string for example.

To this end, libplctag.NET offers a typed tag class `Tag<M,T>` that exposes the tag value as a C# type instead of the complete set of Getter/Setter functions.
This class pairs with an [`IPlcMapper`](src/libplctag/DataTypes/IPlcMapper.cs), which encapsulates the mapping between a .NET type (e.g. `int`, `float`) and the PLC type (e.g. `DINT`, `REAL`) by calling the appropriate functions on a Tag (as well as providing other information that libplctag needs for this mapping).

```csharp
class DintPlcMapper : IPlcMapper<int>
{
public PlcType PlcType { get; set; }
public int? ElementSize => 4;
public int[] ArrayDimensions { get; set; }
public int? GetElementCount() => 1;
public int Decode(Tag tag, int offset) => tag.GetInt32(offset);
public void Encode(Tag tag, int offset, int value) => tag.SetInt32(offset, value);
}

var myTag = new Tag<DintPlcMapper, int>(){...configuration...};
myTag.Initialize();
myTag.Value = 1234;
myTag.Write();
```

In general, you will need prior knowedge of the structure of the tag data, and you may need to reverse-engineer it.
An example for reverse engineering a UDT can be found [here](../examples/CSharp%20DotNetCore/SequencePlcMapper.cs).

Because the structure of the data depends on many factors (PLC Make/model, Protocol, and even the tag Name), libplctag.NET does not provide built-in Mappers for all types.
In general, you will need prior knowedge of the binary format of the tag data, and you may need to reverse-engineer it.
The manuals provided by your device manufacturer are the best source of information on these details.

## Types

### `libplctag` namepsace
* `Tag` - A wrapper around the core libplctag library tag with an interface naturalised to .NET.
* `Tag<M,T>` - A wrapper that exposes a .NET type (generic parameter `T`) instead of Data Accessors. The data access logic is delegated to an `IPlcMapper` (generic parameter `M`).
* `ITag` - an interface that is implemented by `Tag<M,T>`.
* `Libplctag` - A static class used to access some additional features of the libplctag base library such as global debug levels and logging.
* Enum types such as `DebugLevel`.
* Supporting types such as `TagEventArgs`.

All types are shipped with XML documentation, so the full API is discoverable in your IDE.

### `libplctag.DataTypes` namespace

* [`IPlcMapper`](src/libplctag/DataTypes/IPlcMapper.cs)
* [`DintPlcMapper`](src/libplctag/DataTypes/DintPlcMapper.cs)
* [`LrealPlcMapper`](src/libplctag/DataTypes/LrealPlcMapper.cs)
* ... and so on

Of note are [TagInfoPlcMapper](src/libplctag/DataTypes/TagInfoPlcMapper.cs) and [UdtInfoMapper](src/libplctag/DataTypes/UdtInfoPlcMapper.cs), which can be used to [list the tags in a ControlLogix PLC](../examples/CSharp%20DotNetCore/ListUdtDefinitions.cs).

### `libplctag.DataTypes.Simple` namespace

In simple cases such as atomic tags (e.g.`DINT`) or arrays of atomic tags (e.g. `LREAL[x,y]`), we provide classes that pair a built-in `IPlcMapper` with the natural .NET type:
## `libplctag.LibPlcTag`

* [`TagDint`](src/libplctag/DataTypes/Simple/Definitions.cs#L21)
* [`TagLreal2D`](src/libplctag/DataTypes/Simple/Definitions.cs#L41)
* ... and so on
This is a static class used to access some utility features of the libplctag base library such as global debug levels and logging.

## libplctag.NativeImport

Expand Down
1 change: 0 additions & 1 deletion examples/CSharp DotNetCore/CSharp DotNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="RandomTestValues" Version="2.0.5.1" />
</ItemGroup>

</Project>
196 changes: 4 additions & 192 deletions examples/CSharp DotNetCore/ExampleAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

using libplctag;
using libplctag.DataTypes;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -21,7 +17,7 @@ class ExampleAsync
{
public static async Task Run()
{
var myTag = new Tag<DintPlcMapper, int>()
var myTag = new Tag()
{
Name = "PROGRAM:SomeProgram.SomeDINT",
Gateway = "10.10.10.10",
Expand All @@ -33,208 +29,24 @@ public static async Task Run()

await myTag.InitializeAsync();

myTag.Value = 3737;
myTag.SetInt32(0, 3737);

await myTag.WriteAsync();

await myTag.ReadAsync();

int myDint = myTag.Value;
int myDint = myTag.GetInt32(0);

Console.WriteLine(myDint);
}


public static void SyncAsyncComparison()
{

Console.WriteLine("This method measures the speed of synchronous vs asynchronous reads");

List<Tag<DintPlcMapper, int>> myTags;

for (int ii = 0; ii < 10; ii++)
{
myTags = Enumerable.Range(0, 10)
.Select(i => {
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[{i}]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
};
myTag.Initialize();
return myTag;
})
.ToList();

int repetitions = 100;

//Console.Write($"Running {repetitions} Read() calls...");
//var syncStopWatch = Stopwatch.StartNew();
//for (int ii = 0; ii < repetitions; ii++)
//{
// // We know that it takes less than 1000ms per read, so it will return as soon as it is finished
// myTag.Read(1000);
//}
//syncStopWatch.Stop();
//Console.WriteLine($"\ttook {(float)syncStopWatch.ElapsedMilliseconds / (float)repetitions}ms on average");


Console.Write($"Running {repetitions} ReadAsync() calls...");
var asyncStopWatch = Stopwatch.StartNew();
for (int jj = 0; jj < repetitions; jj++)
{
Task.WaitAll(
myTags[0].ReadAsync(),
myTags[1].ReadAsync(),
myTags[2].ReadAsync(),
myTags[3].ReadAsync(),
myTags[4].ReadAsync(),
myTags[5].ReadAsync(),
myTags[6].ReadAsync(),
myTags[7].ReadAsync(),
myTags[8].ReadAsync(),
myTags[9].ReadAsync()
);
//await myTag.ReadAsync();
}
asyncStopWatch.Stop();
Console.WriteLine($"\ttook {(float)asyncStopWatch.ElapsedMilliseconds / (float)repetitions}ms on average");
}


}

public static void ParallelBlockingReads()
{

Console.WriteLine("This method measures the speed of synchronous vs asynchronous reads");
var myTag = new Tag<DintPlcMapper, int>()
{
Name = "PROGRAM:SomeProgram.SomeDINT",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
};
myTag.Initialize();

int repetitions = 100;

Console.Write($"Running {repetitions} calls...");
var sw = Stopwatch.StartNew();
for (int ii = 0; ii < repetitions; ii++)
{
Task.WaitAll(
Task.Run(() => myTag.Read()),
Task.Run(() => myTag.Read())
);
}
sw.Stop();

Console.WriteLine($"\ttook {(float)sw.ElapsedMilliseconds / (float)repetitions}ms on average");

}

public static void SyncAsyncMultipleTagComparison(int repetitions = 1000)
{
Console.WriteLine("This method measures the speed of synchronous vs asynchronous reads for multiple tags simultaneously");

SyncAsyncMultipleTagComparisonSingleRun(1, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(2, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(3, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(4, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(5, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(6, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(7, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(8, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(9, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(10, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(11, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(12, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(13, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(14, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(15, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(16, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(17, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(18, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(19, repetitions);
SyncAsyncMultipleTagComparisonSingleRun(20, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(25, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(30, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(35, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(40, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(45, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(50, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(60, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(70, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(80, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(90, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(100, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(200, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(300, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(400, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(500, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(600, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(700, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(800, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(900, repetitions);
//SyncAsyncMultipleTagComparisonSingleRun(1000, repetitions);
}

private static void SyncAsyncMultipleTagComparisonSingleRun(int maxTags, int repetitions = 10)
{

Console.Write($"Running {repetitions} ReadAsync() calls on {maxTags} tags simultaneously...");

var myTags = Enumerable.Range(0, maxTags)
.Select(i => {
var myTag = new Tag<DintPlcMapper, int>()
{
Name = "PROGRAM:SomeProgram.SomeDINT",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
myTag.Initialize();
return myTag;
})
.ToList();

var asyncStopWatch = Stopwatch.StartNew();

Task.WaitAll(myTags.Select(tag =>
{
return Task.Run(async () =>
{
for (int ii = 0; ii < repetitions; ii++)
{
await tag.ReadAsync();
}
});
}).ToArray());

asyncStopWatch.Stop();
Console.WriteLine($"\ttook {(float)asyncStopWatch.ElapsedMilliseconds / (float)repetitions}ms on average");

foreach (var tag in myTags)
{
tag.Dispose();
}

}


public static void AsyncParallelCancellation(int maxTags = 20, int repetitions = 100)
{

var myTags = Enumerable.Range(0, maxTags)
.Select(i => {
var myTag = new Tag<DintPlcMapper, int>()
var myTag = new Tag()
{
Name = $"MY_DINT_ARRAY_1000[{i}]",
Gateway = "10.10.10.10",
Expand Down
Loading

0 comments on commit 2e13641

Please sign in to comment.