pylogix icon indicating copy to clipboard operation
pylogix copied to clipboard

How to write to UDT

Open ramezanifar opened this issue 4 years ago • 7 comments

Hi. I have few questions: Q1) Can you please provide an example how to write to a tag of type UDT having the values as an array of bytes? There is an example how to read from a timer. I used it to read from a UDT and then tried to write it back to the same tag with the value I got from read (ret.Value) but it throws an error.

Q2) In one of your examples you mention if the data type is provided, read and write become faster. If I read from a tag of type UDT, what type should I use? I tried 163= SINT and 160= STRUCT but it failed.

Q3) When reading from a UDT tag, the number of bytes returned are two more than that of UDT definition. Is this expected?

Thank you in advance

ramezanifar avatar Mar 15 '20 17:03 ramezanifar

  1. It is not currently possible to write UDT's directly.
  2. Providing the data type only works for the base data types (SINT, DINT, STRING, REAL, etc).
  3. The first 2 bytes in the response is the data type.

dmroeder avatar Mar 16 '20 03:03 dmroeder

Could you please consider this (write to UDT) as a new feature? That would be a big help. Thank you

ramezanifar avatar Mar 16 '20 03:03 ramezanifar

I have had success writing UDTs in my library. I cheat. Before the first write, I make sure I do a read. I copy the type bytes into local memory and send them back unchanged on write. The base types like DINT take two bytes for type info. But UDTs and some other types take at least 4.

Here is the C code (data is a pointer to bytes that points to the first byte of the type info in the returned read response):

            /* the first byte of the response is a type byte. */
            pdebug(DEBUG_DETAIL, "type byte = %d (%x)", (int)*data, (int)*data);

            /* handle the data type part.  This can be long. */

            /* check for a simple/base type */
            if ((*data) >= AB_CIP_DATA_BIT && (*data) <= AB_CIP_DATA_STRINGI) {
                /* copy the type info for later. */
                if (tag->encoded_type_info_size == 0) {
                    tag->encoded_type_info_size = 2;
                    mem_copy(tag->encoded_type_info, data, tag->encoded_type_info_size);
                }

                /* skip the type byte and zero length byte */
                data += 2;
            } else if ((*data) == AB_CIP_DATA_ABREV_STRUCT || (*data) == AB_CIP_DATA_ABREV_ARRAY ||
                       (*data) == AB_CIP_DATA_FULL_STRUCT || (*data) == AB_CIP_DATA_FULL_ARRAY) {
                /* this is an aggregate type of some sort, the type info is variable length */
                int type_length = *(data + 1) + 2;  /*
                                                       * MAGIC
                                                       * add 2 to get the total length including
                                                       * the type byte and the length byte.
                                                       */

                /* check for extra long types */
                if (type_length > MAX_TAG_TYPE_INFO) {
                    pdebug(DEBUG_WARN, "Read data type info is too long (%d)!", type_length);
                    rc = PLCTAG_ERR_TOO_LARGE;
                    break;
                }

                /* copy the type info for later. */
                if (tag->encoded_type_info_size == 0) {
                    tag->encoded_type_info_size = type_length;
                    mem_copy(tag->encoded_type_info, data, tag->encoded_type_info_size);
                }

                data += type_length;

It is a pain. I cannot remember where I first saw this logic. Arrays and UDTs usually have a type ID that is two bytes (I think this corresponds to the type ID that you get when enumerating the tags in a PLC) but it can be a full description with many, many bytes. I have not seen that but it is possible apparently.

What you see in the packet is that a UDT will generally have 4 type bytes. The first one is an indicator that the type is a structure. The next one contains the number of bytes of the type ID. Then you get two bytes of type ID. Total of four. The first two are actually an INT with various bit fields but it works out as above. I have not checked to see if the type bytes you get when reading a tag are the same as the ones you get when listing out tags.

kyle-github avatar Mar 16 '20 04:03 kyle-github

I know we've been down this road many many times about UDTs in many issues opened in the past.

https://github.com/dmroeder/pylogix/issues/45 https://github.com/dmroeder/pylogix/issues/6

and prob many more...

@dmroeder would you suggest that the best solution for @ramezanifar right now as the code stands is to parse the xml from the L5X file for the UDTs, and then use those tags with correct data type, provided they are base data types, if another UDT is within the UDT then it becomes more problematic.

@ramezanifar the biggest problem about implementing UDT's logic, is that people will have to learn how to unpack and pack byte data as shown in the timer example, that alone can open up a big can of worms in terms of maintainability.

TheFern2 avatar Mar 16 '20 16:03 TheFern2

I will be happy to pass the byte array because I already know the structure of my UDT. I attempted to read a UDT and just send it back to the same tag but I got an error.

ramezanifar avatar Mar 16 '20 17:03 ramezanifar

I will be happy to pass the byte array because I already know the structure of my UDT. I attempted to read a UDT and just send it back to the same tag but I got an error.

Right, _writeTag method is not meant for UDTs, hence the exception. The only UDT operation in pylogix is to get UDT names, which is _getUDT

Pull Requests are welcome.

TheFern2 avatar Mar 16 '20 17:03 TheFern2

I have had success writing UDTs in my library. I cheat. Before the first write, I make sure I do a read. I copy the type bytes into local memory and send them back unchanged on write. The base types like DINT take two bytes for type info. But UDTs and some other types take at least 4.

Here is the C code (data is a pointer to bytes that points to the first byte of the type info in the returned read response):

            /* the first byte of the response is a type byte. */
            pdebug(DEBUG_DETAIL, "type byte = %d (%x)", (int)*data, (int)*data);

            /* handle the data type part.  This can be long. */

            /* check for a simple/base type */
            if ((*data) >= AB_CIP_DATA_BIT && (*data) <= AB_CIP_DATA_STRINGI) {
                /* copy the type info for later. */
                if (tag->encoded_type_info_size == 0) {
                    tag->encoded_type_info_size = 2;
                    mem_copy(tag->encoded_type_info, data, tag->encoded_type_info_size);
                }

                /* skip the type byte and zero length byte */
                data += 2;
            } else if ((*data) == AB_CIP_DATA_ABREV_STRUCT || (*data) == AB_CIP_DATA_ABREV_ARRAY ||
                       (*data) == AB_CIP_DATA_FULL_STRUCT || (*data) == AB_CIP_DATA_FULL_ARRAY) {
                /* this is an aggregate type of some sort, the type info is variable length */
                int type_length = *(data + 1) + 2;  /*
                                                       * MAGIC
                                                       * add 2 to get the total length including
                                                       * the type byte and the length byte.
                                                       */

                /* check for extra long types */
                if (type_length > MAX_TAG_TYPE_INFO) {
                    pdebug(DEBUG_WARN, "Read data type info is too long (%d)!", type_length);
                    rc = PLCTAG_ERR_TOO_LARGE;
                    break;
                }

                /* copy the type info for later. */
                if (tag->encoded_type_info_size == 0) {
                    tag->encoded_type_info_size = type_length;
                    mem_copy(tag->encoded_type_info, data, tag->encoded_type_info_size);
                }

                data += type_length;

It is a pain. I cannot remember where I first saw this logic. Arrays and UDTs usually have a type ID that is two bytes (I think this corresponds to the type ID that you get when enumerating the tags in a PLC) but it can be a full description with many, many bytes. I have not seen that but it is possible apparently.

What you see in the packet is that a UDT will generally have 4 type bytes. The first one is an indicator that the type is a structure. The next one contains the number of bytes of the type ID. Then you get two bytes of type ID. Total of four. The first two are actually an INT with various bit fields but it works out as above. I have not checked to see if the type bytes you get when reading a tag are the same as the ones you get when listing out tags.

Thank you for the sample. I created a UDT that has one DINT with value one. Reading the tag:

tag = 'Program:Main.test'      
ret = comm.Read(tag)      
print(ret) 

Result: Program:Main.test b'\xaaz\x01\x00\x00\x00' Success So as pointed out by you all it has two bytes for the data type. I converted the result to a list and wrote it into the tag

Value = ['\xaa','z','\x01','\x00','\x00','\x00']  
ret = comm.Write(tag,Value)  
print(ret)

Result: Program:Main.test ['ª', 'z', '\x01', '\x00', '\x00', '\x00'] Unknown error 255

So PLC did not like it!

ramezanifar avatar Mar 23 '20 00:03 ramezanifar