Giter VIP home page Giter VIP logo

Comments (26)

timyhac avatar timyhac commented on September 26, 2024

I think this is a really useful feature.

Do you think we would need to find a way for this to work with different combinations of .NET types and Tag types - for example because the user might want to use an int from their code, but the Tag type is a SINT.

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

I thought about this for a possible C++ wrapper. The only problem that is causing me to rethink this is: how do we deal with UDTs?

Otherwise, what you are suggesting was almost exactly like what I thought the C++ interface would look like!

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

I looked at this a bit last year and came up with a solution that was pretty good for my use-cases.

It involves an abstract class that the user can inherit, and they just need to implement two methods to get/set the correct data. Here is what it looked like for a STRING tag (which is a not a UDT but it is a structure).

You can see that I was using the Mesta API at that time which has a controller-centric API here (as opposed to Tag-centric).

There is a whole bunch of logic here that helps it work for arrays as well.

public class TagSTRING : TagAbstract<string>
{
	public readonly int MaxLength = 82;
	public override string GetTagData(int elementIndex)
	{

		// The first two bytes of a STRING tag encode its length.
		var apparentStringLength = client.GetInt32Value(tag, elementIndex * ElementSize);
		// A STRING is a struct, and the length might be corrupted by faulty PLC programming.
		var stringLength = Math.Min(apparentStringLength, MaxLength);

		var returnString = new StringBuilder();

		for (int i = 0; i < stringLength; i++)
		{
			// There is a 2 byte padding, then each byte afterwards is an ASCII-encoded (a.k.a UTF-8) value.
			returnString.Append( Convert.ToChar(client.GetUint8Value(tag, elementIndex * ElementSize + i + 2 + 2)) );
		}

		return returnString.ToString();
	}

	public override void SetTagData(string value, int elementIndex)
	{

		foreach (var ch in value)
		{
			if((int)ch >= 256)
			{
				throw new ArgumentException("String is not UTF-8 compatible");
			}

		}

		var valueToWrite = Encoding.ASCII.GetBytes(value);

		if (valueToWrite.Length > MaxLength)
		{
			throw new ArgumentException("String length exceeds maximum for a tag of type STRING");
		}

		client.SetInt32Value(tag, elementIndex * ElementSize, valueToWrite.Length);

		for (int i = 0; i < valueToWrite.Length; i++)
		{
			client.SetUint8Value(tag, elementIndex * ElementSize + i + 2 + 2, Convert.ToByte(valueToWrite[i]));
		}
		
	}

        public TagSTRING(Client client, string name, int cacheExpiryDuration ) : base(client, name, DataType.String, 1, 1, 1, cacheExpiryDuration) { }
        public TagSTRING(Client client, string name, int arrayLength1, int cacheExpiryDuration ) : base(client, name, DataType.String, arrayLength1, 1, 1, cacheExpiryDuration) { }
        public TagSTRING(Client client, string name, int arrayLength1, int arrayLength2, int cacheExpiryDuration ) : base(client, name, DataType.String, arrayLength1, arrayLength2, 1, cacheExpiryDuration) { }
        public TagSTRING(Client client, string name, int arrayLength1, int arrayLength2, int arrayLength3, int cacheExpiryDuration ) : base(client, name, DataType.String, arrayLength1, arrayLength2, arrayLength3, cacheExpiryDuration) { }

	
}

You can also see that there is a cache duration parameter in the constructor, and the abstract class implements this caching.
It implements caching for reads, and a buffer for writes, where it waits a few milliseconds before writing just in case the application wants to quickly write a whole bunch of different values (it only writes the last one) - the logic was complex and it wasn't generic enough which is why I didn't contribute it to the package in the first place, but it could be useful for end-users I suppose?

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

You do sleep, right?

Where I was going was to be able to have an object that looked like a native array (if the tag was an array). Part of what led to the callback system was my looking at how I could hook up the C++ object to the underlying tag so that when a read completed, I could decode the raw data into the C++ object. Similarly, when a write was triggered (and this is where I started playing a bit with the dirty flag ideas for writes) then it would first encode the C++ data back into the tag buffer memory and then do the write.

What I did not like about this is that storage was essentially doubled. For PC-based applications, there is usually so little data in tags that it is not a big deal. For embedded use it it could be a problem.

The caching parameter is exactly what is already in the C library and the write delay is what I just proposed on the maintainers list :-)

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

Nice! Yes that would be perfect if there was a way for the class to know if was part of an array and modify the logic correspondingly, but I didn't know how to do it. I actually ended up making different classes for each type for 1D, 2D and 3D arrays as well as the single instance (e.g. TagString, TagString1D, TagString2D, TagString3D) and then using indexer getter/setters to write the values.

Maybe what I'll do is clean up the project and then post the whole thing so you can get a good feel for what it does and see whether its useful.

I suppose the main thing I was trying to contribute was that we can provide an Abstract Class which does the heavy lifting, and ask the user to fill in the nitty-gritty to do with their UDT.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

Thinking about this more now, it would be better if we could just assume that there was a one-to-one relationship between c# types and tag types.

Tag<int> means its a DINT
Tag<long> means its a LINT
Tag<uint> means its a UDINT
etc

I don't know if it is possible though - this article has a whole bunch of combinations that make me thing there is not a one-to-one relationship.

Assuming it was possible, for structures, they could provide a Data Transfer Object class and we could use reflection to figure out how to address the Tags.

Tag<MyUdt> where

public class MyUdt {
    public int MyDINT { get; set; }
    public float MyREAL { get; set; }
}

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

I like these ideas!

I really need to play a bit more to see how much boilerplate I can remove in C++. I suspect that you can remove about the same amount in C#.

I am not sure the PC-native types are going to work. There will be a mapping of some sort. For the C++ wrapper, I was thinking that I would have a bunch of typedefs:

typedef int64_t LINT;
typedef int32_t DINT;
typedef int16_t INT;
typedef int8_t SINT;

That would go into the main C++ header.

Then when you wanted to create an object:

Tag<DINT, 4> my_tag("protocol....", TIMEOUT);

Usage would be just a normal array:

for(size_t i=0; i < my_tag.length(); i++) {
    do_something(my_tag[i]);
}

That's the idea. I would like to make it work so that normal C++ operations like the foreach-style for would work.

That's one way. The other way that I was thinking used more plain data and then bound the plain data to a tag. This is less well thought through.

int32_t my_tag[10];

plc_tag_bind(my_tag, "protocol....", my_tag_decoder, my_tag_encoder, TIMEOUT);

The idea here is that my_tag is just a normal C++ array. You provide the functions my_tag_decoder and my_tag_encoder. I was really trying to see if I can make this work for C. If I can do that, then C++ is a snap. It would be easy enough to provide simple decoder and encoder functions for all the directly supported data types like int32, int64 etc. UDTs would be the responsibility of the user.

Figuring out whether the data had changed would be a bit more annoying. The binding function would need to set up a callback on the tag and when either an automatic read finished, it would call the decoder and when a write was starting, it would call the encoder and check whether anything had changed in the tag. I would need to find a way for the encoder function to be able to tell the underlying library that nothing had changed so that the write could be skipped.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

I suppose you could also ask the caller to supply a Comparer function?

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

Lot's of thoughts here:
First, UDTs - can we split those into another topic? The way I've been working with UDTs with the commercial library we currently use is really just dealing with them using the base types and building my own class around the UDT. This is because I rarely want to treat the whole UDT as a single tag. Fore example, take a built-in UDT like the ALMD (Alarm Digital). I want to poll on the InAlarm but not all the other details until I see a change. Then I want to read the other details once. Also I may want some props/methods tied to the acknowledge bits. All the business rules mixed in means that I basically have to create a C# class to reflect the UDT and access the base tags individually.

Second, datatypes. So we start almost all our code with tag discovery and then find matching tags based on name/type in the lookup after using the tag querying feature. We keep two sets of tags - one of them expanded to the base types, and the other not expanding the UDTs. We really only use the second to find base paths for objects based on type (then tack on the child path we need - _ALARM_PressureMonitor.InAlarm).

With all that being said, I think separately enumerating all of the PLC datatypes is important for embedded work, but not necessary for C#. The way I see it, so much of the world runs on REST and there's only 4 actual datatypes in JSON. Hypothetically, we could use an int64 on the C# side and convert it to the necessary lower level PLC type on read/write and I doubt it would ever be our performance bottleneck.

I guess if we rely on creating tags without querying the PLC than the actual PLC datatype needs to be known at tag creation. There's no real way around that.

I guess in general I would be in favor of having less publicly visible datatypes to support if possible - even if we're using a larger datatype than the PLC.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

@jkoplo - querying the PLC for the datatype, didn't know you could do that - that would definitely be a good way to make it work!

So you could potentially just supply a type as the generic type, and then our library works out how to talk to it. The only issue then would be to make sure the type coercion is checked.
For example if our user created a Tag<int> but the tag type was a LINT then it would need to throw an error if the Tag value was larger than an int could take.
So we'd either need to expose the correct C# type, or deal with type differences. Is that what checked is for?

My opinion is that we should force the consumer to deal with these differences by exposing the appropriate type but am open to doing it differently (or maybe this is where there is scope for having two implementations on top of the thin wrapper?)


I think UDTs do need to be part of this discussion, but we could do an initial implementation that only supports the built-in types using where constraints. The way you use it sounds like that its irrelevant that its part of a UDT, you can supply the full path to the members and work with them as if they were basic types - right?

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

Yes, you can query the datatype via tag listing, but only for top level tags right now! The other way is to do a read of a tag (but you need to know that the tag exists first). One of the "fun" things about the protocol is that you need to pass the datatype to the PLC if you ever do a write to a tag. So the library always starts off with a read of a tag value even if you did not trigger one explicitly. That pulls back the datatype internally. The PLC/protocol differentiates between built-in types like DINT and user or system supplied aggregate types like UDTs. Strings fall somewhere in the middle.

I think that both of you are mostly agreeing that starting with basic types like DINT, will be a good MVP (minimum viable product). UDTs are fraught with problems and complexity and should be put off until there is a clear plan, IMHO.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

@kyle-github - is there a document that shows the mapping between the datatype code and the datatype?

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

I can't remember where I found it, but if you look at the two-byte data type that comes back when you list tags (it is not printed out, but it is pulled out in the code), and you look at the lowest byte:

/* base data type byte values */
#define AB_CIP_DATA_BIT         ((uint8_t)0xC1) /* Boolean value, 1 bit */
#define AB_CIP_DATA_SINT        ((uint8_t)0xC2) /* Signed 8–bit integer value */
#define AB_CIP_DATA_INT         ((uint8_t)0xC3) /* Signed 16–bit integer value */
#define AB_CIP_DATA_DINT        ((uint8_t)0xC4) /* Signed 32–bit integer value */
#define AB_CIP_DATA_LINT        ((uint8_t)0xC5) /* Signed 64–bit integer value */
#define AB_CIP_DATA_USINT       ((uint8_t)0xC6) /* Unsigned 8–bit integer value */
#define AB_CIP_DATA_UINT        ((uint8_t)0xC7) /* Unsigned 16–bit integer value */
#define AB_CIP_DATA_UDINT       ((uint8_t)0xC8) /* Unsigned 32–bit integer value */
#define AB_CIP_DATA_ULINT       ((uint8_t)0xC9) /* Unsigned 64–bit integer value */
#define AB_CIP_DATA_REAL        ((uint8_t)0xCA) /* 32–bit floating point value, IEEE format */
#define AB_CIP_DATA_LREAL       ((uint8_t)0xCB) /* 64–bit floating point value, IEEE format */
#define AB_CIP_DATA_STIME       ((uint8_t)0xCC) /* Synchronous time value */
#define AB_CIP_DATA_DATE        ((uint8_t)0xCD) /* Date value */
#define AB_CIP_DATA_TIME_OF_DAY ((uint8_t)0xCE) /* Time of day value */
#define AB_CIP_DATA_DATE_AND_TIME ((uint8_t)0xCF) /* Date and time of day value */
#define AB_CIP_DATA_STRING      ((uint8_t)0xD0) /* Character string, 1 byte per character */
#define AB_CIP_DATA_BYTE        ((uint8_t)0xD1) /* 8-bit bit string */
#define AB_CIP_DATA_WORD        ((uint8_t)0xD2) /* 16-bit bit string */
#define AB_CIP_DATA_DWORD       ((uint8_t)0xD3) /* 32-bit bit string */
#define AB_CIP_DATA_LWORD       ((uint8_t)0xD4) /* 64-bit bit string */
#define AB_CIP_DATA_STRING2     ((uint8_t)0xD5) /* Wide char character string, 2 bytes per character */
#define AB_CIP_DATA_FTIME       ((uint8_t)0xD6) /* High resolution duration value */
#define AB_CIP_DATA_LTIME       ((uint8_t)0xD7) /* Medium resolution duration value */
#define AB_CIP_DATA_ITIME       ((uint8_t)0xD8) /* Low resolution duration value */
#define AB_CIP_DATA_STRINGN     ((uint8_t)0xD9) /* N-byte per char character string */
#define AB_CIP_DATA_SHORT_STRING ((uint8_t)0xDA) /* Counted character sting with 1 byte per character and 1 byte length indicator */
#define AB_CIP_DATA_TIME        ((uint8_t)0xDB) /* Duration in milliseconds */
#define AB_CIP_DATA_EPATH       ((uint8_t)0xDC) /* CIP path segment(s) */
#define AB_CIP_DATA_ENGUNIT     ((uint8_t)0xDD) /* Engineering units */
#define AB_CIP_DATA_STRINGI     ((uint8_t)0xDE) /* International character string (encoding?) */

/* aggregate data type byte values */
#define AB_CIP_DATA_ABREV_STRUCT    ((uint8_t)0xA0) /* Data is an abbreviated struct type, i.e. a CRC of the actual type descriptor */
#define AB_CIP_DATA_ABREV_ARRAY     ((uint8_t)0xA1) /* Data is an abbreviated array type. The limits are left off */
#define AB_CIP_DATA_FULL_STRUCT     ((uint8_t)0xA2) /* Data is a struct type descriptor */
#define AB_CIP_DATA_FULL_ARRAY      ((uint8_t)0xA3) /* Data is an array type descriptor */

This is from src/protocols/ab/defs.h in the C library source. I put together this list from various sources over the years.

I hope that helps.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

@kyle-github - It seems if I use a string for my tag that specifies a 0 size element, it can still read and write!

var myTag = new Tag(IPAddress.Parse("10.10.10.10"), "1,0", CpuType.Logix, 0, "PROGRAM:SomeProgram.SomeDINT");

which results in this:

protocol=ab_eip&gateway=10.10.10.10&path=1,0&cpu=logix&elem_size=0&elem_count=1&name=PROGRAM:SomeProgram.SomeDINT&use_connected_msg=1

Why does this work? Can I create my tags with an elem_size of zero and then query the size after an initial read - if I even need the elem_size? Is elem_size really only needed for arrays?

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

For logix, I ignore the size and only use the count. Then I pull in the tag based on the count and generate the size by dividing the tag size in bytes by the element count.

If you query elem_size with plc_tag_get_int_attribute(), you will get the right value.

I need to update the docs.

It is currently required for PCCC PLCs but even there I should have enough information from the data file type and element count to determine the size. I just have not bothered.

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

I created a branch generic-typed-tags and took an initial shot at this. It's not ready for PR, but it might be worth peaking at.
For now, I only implemented one datatype (DINT) for testing purposes. We'd need at least all the base types before I'd feel good pulling it. I think it's also worth a discussion if this belongs in a separate package/project or stays as part of the wrapper.

With all that said, it seems to work with my initial testing and it came out pretty darn clean.

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

Looks pretty good - I'm excited for this!

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

The Get/Set bit in the base library appear to be of type int - is this intentional?

LIB_EXPORT int plc_tag_get_bit(int32_t tag, int offset_bit);
LIB_EXPORT int plc_tag_set_bit(int32_t tag, int offset_bit, int val);

from libplctag.net.

timyhac avatar timyhac commented on September 26, 2024

I don't think C has bools, only ints.

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

It does have bools, but MSVC only supported partial C99 compliance up until fairly recently.

Also, the setter routine returns a status and they can be negative.

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

Well I learned something today about C. 👍

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

Heh :-)

C bools are usually implemented as integers with values of 0 or 1. Due to C's implicit promotion rules, anything smaller than an int (in bits) is promoted to an int within an expression. So bools, even if they are implemented as single bytes, are promoted to ints in expressions.

My understanding is that C++'s bools are closer to what people thing of when they think of bools, but I have not looked closely.

from libplctag.net.

kyle-github avatar kyle-github commented on September 26, 2024

BTW, here is how to decode the type word (16-bit):

Screenshot from 2020-07-15 08-26-09

There has to be a way to show an image inline here, right?

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

@kyle-github - thanks! I was just looking at the list_tags output and getting a bit confused by some of the values.

I've implemented and tested 0xC1 through 0xCB in your list. Basically all the Int/Uint/Reals. I PRed that and I'll start a new branch for arrays of the basic types plus strings.

Here's what the C# looks like for strongly typed tags:

IGenericTag<bool> boolTag = new GenericTag<PlcTypeBOOL, bool>(gateway, Path, CpuType.Logix, "TestBOOL", timeout);
boolTag.Value = false;
boolTag.Write(100);
boolTag.Read(100);
bool readBack = boolTag.Value;

from libplctag.net.

jkoplo avatar jkoplo commented on September 26, 2024

Calling this done (for base types). Implementing arrays has been moved to issue #51.

from libplctag.net.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.