libplctag / libplctag

This C library provides a portable and simple API for accessing Allen-Bradley and Modbus PLC data over Ethernet.
Mozilla Public License 2.0
712 stars 232 forks source link

Symbol ID Addressing and quote request for additions to the library #453

Open CrineTech opened 7 months ago

CrineTech commented 7 months ago

We're developing a project that makes use of your library (and the C# official wrapper). Our project can get big benefits with some libplctag library optimizations and additions. If this library have one or more official mantainers please contact me at info@crinetech.it to discuss the possibilities and the relative costs.

Symbol ID Addressing

We are having throubles when we need to access large datatypes that have one or more members configured with external access set to none. In this case, as reported in the Logix 5000 Controllers Data Access manual at page 33, is not possible to read the full datatype at once. If we try we get a permission error from the PLC.

The work-around is to read one by one the datatype elements, skipping the ones with external access set to none. This works, but, since libplctag only uses symbolic addressing, and those dataTypes have a quite long chain of names, the library is only able to read 2 tags in each request (i think that there is a limited max byte lenght for the request). This causes a big performance penality, since a single dataType have 50-80 tags to read, and there are many instance of it to read.

In the same PDF linked above are reported the info for Symbol Instance (or Symbol ID) Addressing. This is an addressing method that have been added after the initial protocol and allows to access elements by id, instead of tag. I've found only the #200 issue that mentions it, but the last update on this thread is at the beginning of 2021.

I've made some test using libplctag.NativeImport and i've been able to get all the tags id using libplctag as follow

var tags = new Tag<TagInfoPlcMapper, TagInfo[]>() {
    Gateway = "192.168.1.158",
    Path = "1,0",
    PlcType = PlcType.ControlLogix,
    Protocol = Protocol.ab_eip,
    Name = "@tags"
};
tags.Read();

Using the ID from this response, i've been able to arrange a raw request to read the value:

byte[] raw_payload = {
    0x4C,
    0x04,
    0x20,
    0x6B,
    0x25,
    0x00,
    0x5A,
    0x00,
    0x28,
    0x00,
    0x01,
    0x00
};

int test_tag = libplctag.NativeImport.plctag.plc_tag_create("protocol=ab-eip&gateway=192.168.1.158&path=1,0&plc=ControlLogix&name=@raw", 5000);
libplctag.NativeImport.plctag.plc_tag_set_size(test_tag, raw_payload.Length);
for(int i=0; i<raw_payload.Length; i++) {
    libplctag.NativeImport.plctag.plc_tag_set_uint8(test_tag, i, raw_payload[i]);
}
libplctag.NativeImport.plctag.plc_tag_write(test_tag, 5000);
libplctag.NativeImport.plctag.plc_tag_destroy(test_tag);

The above test proves that the library already have all the base features needed to test and implement this addressing type (the ability to get ids and to send raw requests with arbitrary data).

I think that the best way is to make possible to specify the id and member order path for a tag as an alternative to the symbol name. This makes requests much shorter, allows to pack more elements in a single multi-tag request (due to the shorter tag path) and reduces the time that the CPU needs to give a reply.

Obscure addressing from RSLinx

This is a capture from wireshark (remove .pdf extension) if you filter it for 'cip' packages, you can see a full initialization of RSLinx (official OPC Server from rockwell) to read a short datatype with a member with external access set to none.

It seems to use a "create" service to create a class with ID 0x2b, uses some command that appears to configure some kind of mapping inside the PLC and after that uses the 0x2b class to read values instead of the 0x6B. This can have an huge impact in reading performances. Do you know anything about this particular addressing? I've found nothinx except for this forum post, with very limited informations.

kyle-github commented 6 months ago

If I understand your ask, you want to read a UDT including protected fields. Is that correct? You also want to read more efficiently. Also correct?

I am interested in doing this but not sure what your example is doing. There is a way to get the instance ID of a tag and to read it using that using the information retrieved when listing tags, as you have found. This is documented by Rockwell. However, I am not sure how you would get the ID of a specific field within a UDT. In your request:

0x4C - Read Symbol Instance
0x04 - 4 word address length, 8 bytes
0x20 - Class
0x6B - Tag class
0x25 - Instance, 2 byte
0x00 
0x5A - Instance 0x005A
0x00

Up to this point everything is documented in Rockwell's documentation.

0x28 - Attribute/field? 1 (one) byte? 2 bytes?
0x00 - attribute index?
0x01 - attribute/field 0x0001?
0x00

Where are you getting the attribute/field number to request? Is that just the index of the field within a UDT?

I am available for some work. I do not have a lot of focus time so I try to keep such projects short duration. If this is just a slight change from symbolic instance addressing, then it should be feasible to do in a week or so, perhaps less. I prefer to charge by project rather than by hour because CIP has a number of string features and sometimes things are easy and sometimes they require a lot of research but not much code.

I also require that any code is either put back into the public version of the library immediately or with a small delay. I do not have the time to support proprietary forks of the library for any large amount of time.

I'll address the other protocol command separately.

kyle-github commented 6 months ago

RSLinx does use proprietary and undocumented CIP commands to read tags and tag values. I have not seen any documentation on the 0x2b class. While it might be possible to figure out what is needed here, it is likely to involve a lot of reverse engineering and time. I will bring this up with other library authors (specifically pylogix) to see if they have any time to investigate this.

CrineTech commented 6 months ago

If I understand your ask, you want to read a UDT including protected fields. Is that correct? You also want to read more efficiently. Also correct?

From the documentation that i've seen, reading an UDT with protected fields is not possible. This is surprising me, i was supposing to have some way to read the whole UDT at once, with protected sections skipped or always readed as 0. Since this is not possible and the RSLinx method is undocumented, i need at least to be efficent with the read of single elements inside the UDT.

I am interested in doing this but not sure what your example is doing. There is a way to get the instance ID of a tag and to read it using that using the information retrieved when listing tags, as you have found. This is documented by Rockwell. However, I am not sure how you would get the ID of a specific field within a UDT. In your request:

0x4C - Read Symbol Instance
0x04 - 4 word address length, 8 bytes
0x20 - Class
0x6B - Tag class
0x25 - Instance, 2 byte
0x00 
0x5A - Instance 0x005A
0x00

Up to this point everything is documented in Rockwell's documentation.

0x28 - Member segment header for 8-bit Element ID
0x00 - 8-bit Element ID value (element 0 in this case)
0x01 - Number of elements to read (2 bytes)
0x00

Where are you getting the attribute/field number to request? Is that just the index of the field within a UDT?

I've done some tests, see corrected byte descriptions above. The second portion is a numerical member segment part (the same as string member segment part). In tests that i've done, this was the base 0 index of the field within the UDT (or the array index for arrays).

I've tested only asking one element, it may be interesting to try asking more elements. Probably the request will fail (i think that the elements number is only for arrays), but if it is implemented also for UDT, it may be a much more permormant way to read UDT sections.

I've taken the idea from this section of the manual that i've uploaded in the original post. In the example is used to access an array, but in the tests that i've done, it always worked also for UDT. image

I am available for some work. I do not have a lot of focus time so I try to keep such projects short duration. If this is just a slight change from symbolic instance addressing, then it should be feasible to do in a week or so, perhaps less. I prefer to charge by project rather than by hour because CIP has a number of string features and sometimes things are easy and sometimes they require a lot of research but not much code.

I also require that any code is either put back into the public version of the library immediately or with a small delay. I do not have the time to support proprietary forks of the library for any large amount of time.

The only needed change is the ability to use symbol id addressing and to be able to specify numerical segment parts instead of textual ones. This new reading mode must be able to create packed requests, as it is already done for symbolic addressing. I will manage in our code the tags and UDT read on connection and the mapping of symbolic paths saved in our program to symbol id address.

Are you able to give us a quote for the above changes?

A natural expansion of this feature in my opinion will be the ability to let the library read all PLC tags and UDT definitions on connection and keep a mapping table to be able to issue more efficent symbol id addressing requests also for tags specified with symbolic addressing.

In my opinion this could be done by adding a new parameter to the connection string to enable or disable the automatic reading of tags / udt on connection and a parameter to the tag (similar to allowpacking) to allow or not the automatic conversion between symbolic addressing and symbol id addressing.

This second part however is only a suggestion, is not actually needed by us at the moment and should not be included in the quote.

CrineTech commented 6 months ago

RSLinx does use proprietary and undocumented CIP commands to read tags and tag values. I have not seen any documentation on the 0x2b class. While it might be possible to figure out what is needed here, it is likely to involve a lot of reverse engineering and time. I will bring this up with other library authors (specifically pylogix) to see if they have any time to investigate this.

As from previous reply, this is not our actual goal, so we're not asking a quotation for it, however, if you're intrested in researching and developing this feature, we're available to test it. We have a quite large range of Allen-Bradley PLC's to test with.

kyle-github commented 6 months ago

Thanks for the updates. This is quite interesting. I never tried to use indexing to read a UDT. I will experiment this weekend a bit to see if I can do this with the symbolic segment reads that already exist. It looks like it will be possible. It also may be possible to read a full UDT by using the element count with a value greater than 1.

Do you have access to Micro8x0 PLCs? Those have some differences in protocol from Control/CompactLogix but I do not have access to one.

kyle-github commented 6 months ago

The idea of having the tag database stored within the library is one that I have thought about for a long time. There are many advantages to this such as:

There are more advantages, but those seem to be the most important ones.

There are some problems with this though:

The Rockwell documentation shows a way to check if the tag database has changed by reading a set of attributes of some CIP objects and seeing if the content changes. This is still a problem as there is a race condition between the change to the tags in the PLC and the time that these special attributes are read. And the read of the tag database takes a long, long time if there are a lot of tags. That time blocks other tag reads (I do not have individual tag bandwidth throttling).

Everyone I have talked to (that does not work for Rockwell) thinks that there must be a way to get full type information for individual tags but no one seems to know how. This is very frustrating to all of us who have CIP/PLC libraries!

One idea I have been thinking about is this:

The idea is that if you never read @tags, then there is no translation. If you do read it and you set an attribute in it (or automatically), then the tag database is stored and used for translation. The ability to read tags automatically can be used to update the tag database from time to time. As noted above, that will almost certainly saturate the request bandwidth for a period of time and possibly result in lost data from normal tag reads.

It is very important for the goals of the library for the user (the application program) to be able to control the resource use of the library. With the above idea, a deeply embedded system would simply never read @tags.

I already have the ability to read certain tag attributes such as some flags from a tag handle. I can extend that to support direct tag type, size, dimension and element count etc. from a tag database. It might not be that good of a fit for the existing API and I have not thought through this completely.

One big caveat about symbolic instance addressing is that it appears to only be one level deep. That is, if you want to symbolically address a field in a UDT in an array of those UDTs, you can only use instance addressing on the array itself. E.g.:

UDT: myUDTType
  field1:BOOL
  field2:DINT

Tag: myUDTArray: myUDTType[10]

If I want to read myUDTArray[4].field2, I will have the following path string:

4c 0 20 6B 25 00 34 - pretend instance ID of 0x1234 12 28 04 - array index 4 91 ... symbolic segment for "field2"

Only the first part can be translated as far as I can tell. Tests by other people have not determined how to get symbolic instance addressing to go past the first level. I am not sure you can use the field indexing idea with an array like this:

4c 0 20 6B 25 00 34 - pretend instance ID of 0x1234 12 28 04 - array index 4 28 00 - field index 0?

In this case I do not see how the PLC can tell the difference between this request and a request for array element [4,0] in a two dimensional array. I will try it when I get a chance. Perhaps the PLC will look at the tag definition and understand that it is a one-dimensional array?

kyle-github commented 6 months ago

I forgot to mention another possible advantage to using symbolic instance addressing that was brought up by a user a few years ago (he wrote his own CIP stack): speed of request handling. The instance ID directly allows the PLC to find the tag in its internal database without doing a string lookup. According to that user's tests, there is a performance gain but it was not that large with his test system. Perhaps when there are many tags this would be noticeable?

CrineTech commented 6 months ago

Do you have access to Micro8x0 PLCs? Those have some differences in protocol from Control/CompactLogix but I do not have access to one.

I'm following the development part of the project. There is another local company involved wich has most of the hardware. I've asked them if they've one of those Micro8x0 PLCs, i'll let you know.

CrineTech commented 6 months ago
  • The tag database on the PLC can be changed while the program using libplctag is running. Tags can be deleted and in some cases the IDs may be changed or even reused.

A reliable and quick way to detect changes is describet at pages 51-53 of the PDF attached to the first post. The image below contains the crucial informations from page 51

image

  • The tag database takes a lot of memory when there are a lot of tags. For small embedded systems this may not be acceptable.

In my opinion you shouldn't go for the automatic way. To let developers using the library able to choose between all the possibilities i think that 4 additional parameters are needed on the connection:

There should also be some way to get from the library

With all the options described above, based on the application and resources, a developer should be able to choose between all the possibilities, from the fully automatic mode to the fully managed (for example, an application that knows the status of the machine could decide to read tags only when the machine is turned on but not running).

  • Reading the tag database from the PLC can take seconds in some cases. I have an old 1756-L55 CPU that only supports the old 500 byte packets and has a program in it that has hundreds of tags. It takes almost three seconds on an unloaded 1Gpbs network to get the tag database.

I agree that this is not an option to enable as default (see above), but if you've an high end CPU with 2 network interfaces, one dedicated to the field and the other one exclusively dedicated to the supervision, HMI etc, a few seconds of full load on the network interface that is not handling the field is not a big problem in most of the cases.

The Rockwell documentation shows a way to check if the tag database has changed by reading a set of attributes of some CIP objects and seeing if the content changes. This is still a problem as there is a race condition between the change to the tags in the PLC and the time that these special attributes are read. And the read of the tag database takes a long, long time if there are a lot of tags. That time blocks other tag reads (I do not have individual tag bandwidth throttling).

Ops, i've reached this point after writing the reply to the first quote above. I'll leave it, maybe there is some useful info.

There is not a real solution for this kind of "race condition", but if you examine the real cases, it should not be a big issue.

I suppose (but should be checked) that when a new tag is added gets an ID that was free. I don't think that Allen Bradley is swapping IDs on a running PLC. If this is the case, a new tag shouldn't be problematic. If accessed it will give error (the library will think that is not existing) untill the tag database is refreshed. Since the check for changes is a qucik test it can be executed every minute or so, giving this small timeframe, in the real word i dont think that will be noticed.

If a tag is deleted, on the first response with an error reporting a not found tag, it can be removed from the tag database to avoid subsequent readings. Is up to the application (or to the user, if it is configurable) to edit settings to adapt the application to the PLC changes (this once again is valid only if allenbradley is not changing other IDs when tags are added or deleted)

Regarding the blocking issue, i've not looked in details at how the library is implemented internally, but a quick solution can be a delay between each request needed to read the tag. This could be an optional parameter of the tag and will give the ability to slow down the reading of a huge tag (@tags or a really, really large array or UDT) to let smaller tags reads able to process.

One big caveat about symbolic instance addressing is that it appears to only be one level deep. That is, if you want to symbolically address a field in a UDT in an array of those UDTs, you can only use instance addressing on the array itself. E.g.:

UDT: myUDTType
  field1:BOOL
  field2:DINT

Tag: myUDTArray: myUDTType[10]

If I want to read myUDTArray[4].field2, I will have the following path string:

4c 0 20 6B 25 00 34 - pretend instance ID of 0x1234 12 28 04 - array index 4 91 ... symbolic segment for "field2"

Only the first part can be translated as far as I can tell. Tests by other people have not determined how to get symbolic instance addressing to go past the first level. I am not sure you can use the field indexing idea with an array like this:

4c 0 20 6B 25 00 34 - pretend instance ID of 0x1234 12 28 04 - array index 4 28 00 - field index 0?

In this case I do not see how the PLC can tell the difference between this request and a request for array element [4,0] in a two dimensional array. I will try it when I get a chance. Perhaps the PLC will look at the tag definition and understand that it is a one-dimensional array?

In the tests that i've done i was able to index different elements of an UDT, but there was no array involved. PLC have somehow to look at tag definition also to know how many bites of memory it should return (in the request there is only the element count, not the size) and to check that the externalaccess for that particular tag is not set to none, so i will not be surprised if it looks also at the definition of an array to understand how to handle a particular segment address path.

In the next week i'll be quite busy, but in the following one i can perform some more extended test if you need.

kyle-github commented 6 months ago

I have been doing some testing over the weekend and it looks like this works in that the PLC responds without error. I did additional testing on UDTs with hidden fields. These are used when there are BOOL fields, for instance. I also tested with a very large BOOL array (512 elements). There were some results I did not expect.

The test UDT I used has many BOOL fields. This originally was a test for reading hidden fields. The UDT is structured as follows:

UDT ManyBOOLFieldsUDT (ID 9a5, 8 bytes, struct handle 2c1a):
    Field 0: ZZZZZZZZZZManyBOOLFi0, offset 0, type (00c2) SINT: Signed 8-bit integer value.
    Field 1: aLongBOOLFieldName1, offset 0:0, type (00c1) BOOL: Boolean value.
    Field 2: aLongBOOLFieldName2, offset 0:1, type (00c1) BOOL: Boolean value.
    Field 3: aLongBOOLFieldName3, offset 0:2, type (00c1) BOOL: Boolean value.
    Field 4: aLongBOOLFieldName4, offset 0:3, type (00c1) BOOL: Boolean value.
    Field 5: aLongBOOLFieldName5, offset 0:4, type (00c1) BOOL: Boolean value.
    Field 6: aLongBOOLFieldName6, offset 0:5, type (00c1) BOOL: Boolean value.
    Field 7: aLongBOOLFieldName7, offset 0:6, type (00c1) BOOL: Boolean value.
    Field 8: aLongBOOLFieldName8, offset 0:7, type (00c1) BOOL: Boolean value.
    Field 9: ZZZZZZZZZZManyBOOLFi9, offset 1, type (00c2) SINT: Signed 8-bit integer value.
    Field 10: aLongBOOLFieldName9, offset 1:0, type (00c1) BOOL: Boolean value.
    Field 11: aLongBOOLFieldName10, offset 1:1, type (00c1) BOOL: Boolean value.
    Field 12: aLongBOOLFieldName11, offset 1:2, type (00c1) BOOL: Boolean value.
    Field 13: aLongBOOLFieldName12, offset 1:3, type (00c1) BOOL: Boolean value.
    Field 14: aLongBOOLFieldName13, offset 1:4, type (00c1) BOOL: Boolean value.
    Field 15: aLongBOOLFieldName14, offset 1:5, type (00c1) BOOL: Boolean value.
    Field 16: aLongBOOLFieldName15, offset 1:6, type (00c1) BOOL: Boolean value.
    Field 17: aLongBOOLFieldName16, offset 1:7, type (00c1) BOOL: Boolean value.
    Field 18: ZZZZZZZZZZManyBOOLFi18, offset 2, type (00c2) SINT: Signed 8-bit integer value.
    Field 19: aLongBOOLFieldName17, offset 2:0, type (00c1) BOOL: Boolean value.
    Field 20: aLongBOOLFieldName18, offset 2:1, type (00c1) BOOL: Boolean value.
    Field 21: aLongBOOLFieldName19, offset 2:2, type (00c1) BOOL: Boolean value.
    Field 22: aLongBOOLFieldName20, offset 2:3, type (00c1) BOOL: Boolean value.
    Field 23: aLongBOOLFieldName21, offset 2:4, type (00c1) BOOL: Boolean value.
    Field 24: aLongBOOLFieldName22, offset 2:5, type (00c1) BOOL: Boolean value.
    Field 25: aLongBOOLFieldName23, offset 2:6, type (00c1) BOOL: Boolean value.
    Field 26: aLongBOOLFieldName24, offset 2:7, type (00c1) BOOL: Boolean value.
    Field 27: ZZZZZZZZZZManyBOOLFi27, offset 3, type (00c2) SINT: Signed 8-bit integer value.
    Field 28: aLongBOOLFieldName25, offset 3:0, type (00c1) BOOL: Boolean value.
    Field 29: aLongBOOLFieldName26, offset 3:1, type (00c1) BOOL: Boolean value.
    Field 30: aLongBOOLFieldName27, offset 3:2, type (00c1) BOOL: Boolean value.
    Field 31: aLongBOOLFieldName28, offset 3:3, type (00c1) BOOL: Boolean value.
    Field 32: aLongBOOLFieldName29, offset 3:4, type (00c1) BOOL: Boolean value.
    Field 33: aLongBOOLFieldName30, offset 3:5, type (00c1) BOOL: Boolean value.
    Field 34: aLongBOOLFieldName31, offset 3:6, type (00c1) BOOL: Boolean value.
    Field 35: aLongBOOLFieldName32, offset 3:7, type (00c1) BOOL: Boolean value.
    Field 36: ZZZZZZZZZZManyBOOLFi36, offset 4, type (00c2) SINT: Signed 8-bit integer value.
    Field 37: aLongBOOLFieldName33, offset 4:0, type (00c1) BOOL: Boolean value.
    Field 38: aLongBOOLFieldName34, offset 4:1, type (00c1) BOOL: Boolean value.
    Field 39: aLongBOOLFieldName35, offset 4:2, type (00c1) BOOL: Boolean value.
    Field 40: aLongBOOLFieldName36, offset 4:3, type (00c1) BOOL: Boolean value.
    Field 41: aLongBOOLFieldName37, offset 4:4, type (00c1) BOOL: Boolean value.
    Field 42: aLongBOOLFieldName38, offset 4:5, type (00c1) BOOL: Boolean value.
    Field 43: aLongBOOLFieldName39, offset 4:6, type (00c1) BOOL: Boolean value.
    Field 44: aLongBOOLFieldName40, offset 4:7, type (00c1) BOOL: Boolean value.
    Field 45: ZZZZZZZZZZManyBOOLFi45, offset 5, type (00c2) SINT: Signed 8-bit integer value.
    Field 46: aLongBOOLFieldName41, offset 5:0, type (00c1) BOOL: Boolean value.
    Field 47: aLongBOOLFieldName42, offset 5:1, type (00c1) BOOL: Boolean value.
    Field 48: aLongBOOLFieldName43, offset 5:2, type (00c1) BOOL: Boolean value.
    Field 49: aLongBOOLFieldName44, offset 5:3, type (00c1) BOOL: Boolean value.
    Field 50: aLongBOOLFieldName45, offset 5:4, type (00c1) BOOL: Boolean value.
    Field 51: aLongBOOLFieldName46, offset 5:5, type (00c1) BOOL: Boolean value.
    Field 52: aLongBOOLFieldName47, offset 5:6, type (00c1) BOOL: Boolean value.
    Field 53: aLongBOOLFieldName48, offset 5:7, type (00c1) BOOL: Boolean value.
    Field 54: ZZZZZZZZZZManyBOOLFi54, offset 6, type (00c2) SINT: Signed 8-bit integer value.
    Field 55: aLongBOOLFieldName49, offset 6:0, type (00c1) BOOL: Boolean value.
    Field 56: aLongBOOLFieldName50, offset 6:1, type (00c1) BOOL: Boolean value.
    Field 57: aLongBOOLFieldName51, offset 6:2, type (00c1) BOOL: Boolean value.
    Field 58: aLongBOOLFieldName52, offset 6:3, type (00c1) BOOL: Boolean value.
    Field 59: aLongBOOLFieldName53, offset 6:4, type (00c1) BOOL: Boolean value.
    Field 60: aLongBOOLFieldName54, offset 6:5, type (00c1) BOOL: Boolean value.
    Field 61: aLongBOOLFieldName55, offset 6:6, type (00c1) BOOL: Boolean value.
    Field 62: aLongBOOLFieldName56, offset 6:7, type (00c1) BOOL: Boolean value.
    Field 63: ZZZZZZZZZZManyBOOLFi63, offset 7, type (00c2) SINT: Signed 8-bit integer value.
    Field 64: aLongBOOLFieldName57, offset 7:0, type (00c1) BOOL: Boolean value.
    Field 65: aLongBOOLFieldName58, offset 7:1, type (00c1) BOOL: Boolean value.
    Field 66: aLongBOOLFieldName59, offset 7:2, type (00c1) BOOL: Boolean value.
    Field 67: aLongBOOLFieldName60, offset 7:3, type (00c1) BOOL: Boolean value.

The UDT uses 8 bytes of space. Only the hidden fields use any space. The other fields use single bits within the hidden fields.

The symbolic instance addressing works, but the fields accessed include the hidden fields. With instance addressing, it appears that this UDT has 68 fields. With symbolic segment (named) addressing the UDT appears to have 60 fields.

I tested a large BOOL array. The results I got were very odd. The array was 512 BOOLs. Rockwell PLCs store BOOL arrays as arrays of 32-bit integers with type 0xD3. Indexing these using symbolic segment addressing requires you to use the index of the underlying 32-bit backing integer, not the index of the BOOL element. I.e. element 33 uses index 1 (second backing 32-bit integer).

These results are confusing and annoying. The PLC will respond with different amounts of data and expose different fields depending on how you access the tag. E.G. With instance addressing, the individual BOOL elements in the BOOL array can be addressed by the index in the tag. With symbolic segment addressing the index in the tag indexes the underlying backing 32-bit 0xD3 integers.

The UDT also behaves differently. With the use of names, the hidden fields remain hidden. With the use of indexes, the hidden fields are exposed, but so are the regular BOOL fields.

A major impact of this difference is that if I want to do automatic tag name translation from named tags to instance addressed tags, the data returned will change in some cases. So translation is NOT transparent.

kyle-github commented 5 months ago

Any updates on this?