-
Notifications
You must be signed in to change notification settings - Fork 75
Description
A few years after the release of the official libplctag.NET wrapper, a pattern has emerged that all library consumers face a key problem;
What is the binary format of a tag buffer (a byte array)? How does it encode the tag's value?
In this ticket I will be making the argument that the Mapper system (Tag<M,T>, IPlcMapper, etc) provided by libplctag.NET has magnified this problem by obscuring/hiding it from library users.
This is to the detriment of those users, reduces the ability of the libplctag community to assist those users, and increases the burden on library authors - and therefore should be removed.
Introduction
libplctag handles the details of communicating with PLCs over Modbus or Ethernet/IP Explicit Messaging, so you can easily access the value of PLC tags.
This statement is mostly true; while it is correct that you (most of the time) do not need to understand the protocol itself, you do need to understand the device's Modbus or Ethernet/IP API, as not all devices are the same.
This is similar to using a HTTP REST API - while you do not need to understand the details of TCP/IP or even HTTP, you definitely need to know what data to send and how to interpret the response.
For example; you need to know how to configure the EthernetIP or Modbus requests by using an appropriate tag "name", path/gateway, and other attributes, and how the device will structure its response (a byte array) to encodes tag values.
For basic cases such as Atomic tag types (e.g. DINT, REAL) - this is usually straightforward and you only need to use a single Data Accessor method to extract/write the value, furthermore these are usually common across devices and for tag names.
UDTs, STRINGs, BOOLs and arrays however, are non-trivial.
In general, you will need prior knowledge of how these tags are structured.
This knowledge needs to come from device manuals or from reverse engineering efforts.
The good: it handles a common case well!
In Tag<M,T>, the M is a PlcMapper which converts between a byte array (the tag buffer) and the T .NET type and makes it possible to implement the Decode/Encode logic for a data type once, and then re-use it for 1D, 2D and 3D arrays as well as the atomic type.
For this use-case it excels.
This system can even be used for UDTs with static sizes.
The value is strongly-typed, which holds significant value in the .NET ecosystem and particularly C#.
It is entirely possible that this serves the silent majority of users perfectly - they have no complaints, and we do not hear from them.
They have used the system as it was intended and moved on with their life.
It's utility stems from being able to instantiate a strongly-typed Tag, simply by providing a Mapper class type (and most of the time this is built-in).
And even this can be hidden because we also ship "simple" classes.
The complexity cliff
Tag<M,T> makes a (somewhat implicit) promise that you as a consumer do not need to understand how PLC data is encoded in the buffer, as the PlcMapper handles that for you.
However, over time it has become clear that there are additional capabilities that PLCs offer, and that library consumers need, that the Mapper system doesn't fulfil.
At the time of development, the Mapper system was consistent with the core libplctag API, and was universally applicable (at least within the confines of libplctag.NET developers' limited exposure to other PLC vendors).
The key insight was that single values (whether they be atomic or UDTs) and arrays of values can use the same decoding logic.
The logical conclusion is that much of the setup of tags can be contained within PlcMapper classes, and Tag<M,T> merely needs to expose the Read/Write commands and events.
This abstraction falls off a "cliff" as soon as you go outside the simplest of cases.
But the promise remains - the PlcMapper is meant to do it, and it does not - therefore it is a bug in the PlcMapper or Tag<M,T>.
Ultimately, using libplctag does not preclude you (the library consumer) from understanding the device's Ethernet/IP API.
Consumers will encounter this working
- When working with some of the more complicated types (BOOL Arrays, Strings, UDTs)
- Using some advanced CIP services such as Tag Listing.
- Arrays of arrays (a.k.a. Jagged arrays)
- Dynamic Tags
- When using multiple make/models of device (Allen-Bradley encode some tags different to Omron)
- When using different tag names (e.g.
MyBoolArrayandMyBoolArray[0]return different data on Omron).
If we want to continue to make the promise that consumers do not need to understand their device's Ethernet/IP API, the only path forward for this project is to wait for complaints to arrive, and incrementally add handlers for specific cases into the mappers.
The mappers would effectively become a documentation store for this knowledge.
Its another API to learn, support, test and maintain
The good news is that consumers are not locked into using the built-in mappers.
If they are able to understand how their devices behave, they can add custom mappers.
However, this is another API that consumers must learn.
The Mapper system uses some somewhat advanced C# language features which can be tricky for early-career developers to understand and implement.
If there are features that consumers want (such as value change detection), consumers could spend a considerable amount of time trying to determine how libplctag.NET supports it (which it may not), rather than just implementing it themself.
Furthermore:
- libplctag wiki does not apply to most of it.
- There are three wrapper classes plus the mapper - objects which must be garbage collected.
- There are many cases where the returned PLC memory does not have a static size.
- It is code that the libplctag community must document, support and maintain.
It would be simpler if that layer was removed, and consumers directly interfaced with Tag - it confronts them with the complexity of the underlying API and does not hide any of the "foot-guns" (which exist whether they can see them or not).
Many consumers still add their own wrappers
A study of applications or libraries that are consuming libplctag.NET shows that most will create yet another wrapper around Tag<M,T>.
This implies that it is not the optimal level of abstraction and doesn't add much value.
Some examples:
- https://github.com/mesta1/libplctag-csharp
- https://github.com/TheFern2/clx.libplctag.NET
- https://github.com/Corsinvest/cv4ab-api-dotnet
What's the alternative?
Use an abstract base class and handle the specific data types with derived classes.
This has several advantages over the Tag<M,T>+PlcMapper system:
- Several use-cases would have been immediately enabled (e.g. hooks to add before ad after Initialize is called).
- Consumers can add their own methods such as custom setters/getters that trigger events.
The issue with this is that there is not an obvious universal interface that would serve as the base class.
As an example, should the Tag value be exposed as a .NET type such as int or should it be object - or should it be exposed at all?
What if the type was an array, and instead of exposing the entire array, the library consumer would prefer to force their application code to go through a method with an indexer (so that it can raise events or run other logic).
Summary
While Tag<M,T> has utility for a common use-case, the API it exposes is not the lowest-common denominator.
Having it is a burden on library consumers and maintainers alike.
I recommend:
- Removing
ITag,Tag<M,T>, all Mappers and Simple Tag classes, and also not supplying an alternative like an abstract base type. - Instead, moving
Tag<M,T>and a demonstration of an alternative like an abstract base mapper type to the Examples folder. - Move Tag Listing for Rockwell PLC to the Examples folder.
- Creating a separate repository within the libplctag organization for known tag buffer encodings for combinations of PlcType, Tag Name, Protocol, etc
- Clarify the promise that libplctag makes to customers - what should libplctag be capable of, and what is left up to the consumer to discover?
If this happens, the libplctag.NET would be closer to a "wrapper" in spirit, rather than what could be argued to be an opinionated framework for working with libplctag.
I would be happy to offer a separate Nuget package for the Mapper system, but not as an "official" package by the libplctag organization, instead released under a "timyhac" brand such as timyhac.libplctag.Mappers
I've created this as a Github issue to open it up for community feedback. I realize that there are many users of the Mapper system at this point and I want to take into account the impact on them.