ottowayi / pycomm3

A Python Ethernet/IP library for communicating with Allen-Bradley PLCs.
MIT License
394 stars 88 forks source link

Help Implementing Vendor Specific Methods #201

Closed JL00001 closed 2 years ago

JL00001 commented 2 years ago

Hello again, I am looking for help in using a Vendor Specific CIP Method; Namely Rockwell PowerFlex 525's Get_Attributes_Scattered and Set_Attributes_Scattered. As described in this manual on page 81, and with additional information on page 150

https://literature.rockwellautomation.com/idc/groups/literature/documents/um/520com-um003_-en-e.pdf

I am already very familiar with the Get_Attribute_Single and Set_Attribute_Single; but I am unsure how switching from single to scattered will change my code. I believe I will need to use another datatype; but am unsure how to proceed. The manual describes sending a array (Scattered_Read_Request) and getting a array in response (Scattered_Read_Response); but how to do that in pycomm3 is a little bit beyond me.

with pycomm3.CIPDriver(self.Drive_Path) as drive:
                param = drive.generic_message(
                    service=b'\x32'.
                    class_code=b'\x93',
                    instance=0,
                    attribute=0,
                    connected=False,
                    unconnected_send=True,
                    route_path=True,
                    data_type=pycomm3.cip.data_types.INT
                )

I must thank you for this great library. This bit of code and pandas and a excel spreadsheet has save literally hundreds of man hours setting VFD parameters

ottowayi commented 2 years ago

Sure, here is an example I got working. It creates a custom struct type so you don't have to worry about encoding and decoding the data yourself.

from pycomm3 import CIPDriver, Struct, UINT, configure_default_logger, LOG_VERBOSE

configure_default_logger(level=LOG_VERBOSE)

# Since the data is structured as a list of parameter number and value/pad, 
# we can make our own struct type for each element in the array.
ParamItem = Struct(UINT('num'), UINT('value'))

read_params = [  # list of param number, param value tuples.  value = 0 for reading
    (1, 0),
    (2, 0),
    (3, 0),
]

with CIPDriver('10.10.128.4') as pf525:
    result = pf525.generic_message(
        service=0x32,
        class_code=0x93,
        instance=0,

        # Now encode the request data as an array of ParamItems
        # using the length of the source data
        request_data=ParamItem[len(read_params)].encode(read_params),

       # decode the response as an array of ParamItems
        data_type=ParamItem[len(read_params)],

    )

print(result.value)

Output:

[{'num': 1, 'value': 0}, {'num': 2, 'value': 0}, {'num': 3, 'value': 0}]

Couple notes:
1 - If you choose to make it unconnected, then you will need to include unconnected_send=True if you have a route path (ie. not just an IP address for the path). This will become automatic in the next version. I also was getting service not supported when I tried this, but I didn't spend much time investigating it. 2 - You can make any type an array by adding [<len>] to it. Instead of using the len of the param list we could have done [None] to make it of undetermined size, but I think None is kind of confusing. In the next version ... will also work for undefined length.
3 - The read_params list is a list of tuples, the struct type can take a sequence and iterate over it while encoding. Another option is to use a list of dicts matching ParamItem like [ {'num': , 'value': 0}, ...]. If you were working with struct tags in a PLC like UDTs, AOIs, etc then only the dict format is accepted (due to the more complicated structure, especially around BOOLs being mapped to hidden attributes).

Hope this helps!

JL00001 commented 2 years ago

Worked like a charm. Once I realized struct was was not the python struct but the pycomm3.struct. And with minimal tweaking, I got the Set_Attributes_Scattered to work as well with the code posted below. I don't need help with it; just incase anyone else was looking for that code.

import pycomm3
ParamItem = pycomm3.Struct(pycomm3.UINT("parameter"), pycomm3.UINT('value'))
 write_params = [
            (496, 130),
            (498, 1090),
            (500, 1526)]
with pycomm3.CIPDriver("172.16.41.17") as pf525:
  result = pf525.generic_message(
      service=0x34,
      class_code=0x93,
      instance=0,
      request_data=ParamItem[len(write_params)].encode(write_params),
      data_type=ParamItem[len(write_params)],
  )
print(result.value)

What is the significance if theses options in the generic_message? I am new to CIP messages and with your library have been learning a lot more, but have been self taught through all of this

connected=False,
unconnected_send=True,
ottowayi commented 2 years ago

It's still kind of confusing to me, but I'll try and give you a quick summary. So we're using Explicit Messaging, which is point to point like a MSG instruction in a PLC or an HMI/SCADA connection, basically everything but I/O. There are two types of messages, connected (long term, continuous) or unconnected (short term, one-off, infrequent). Connected messages first require you to setup the connection using a Forward Open, this sets up the routing, allocates resources, etc. Then each message only needs to contain the connection id it's using to make it to the target. You can send messages without first establishing a connection by using the Unconnected Message Manager (UCMM). In order to route messages thru the UCMM you wrap the message in an UnconnectedSend service. Basically each hop along the path will pop off their node from the route and forward it along, so when it reaches it's target it appears as a normal message. When I originally created the generic_message method I didn't fully understand how that worked, so in my next version the unconnected_send flag will be going away and that functionality will become automatic. If you'll be keeping the driver instance around for awhile and infrequently sending messages, the connection may time out and be closed by the controller, leading to a failed request. In that case you will probably want to use unconnected unless you'll be sending requests periodically to keep the connection open. If your driver is not going to be staying around for long (like the example above using a with block), then it doesn't matter much. There is a little extra overhead in setting up the connection, but that probably won't really affect you much and it makes your code simpler.

JL00001 commented 2 years ago

Ok, thanks for the info. For what I am using pycomm3 for; reading and writing parameters on PF525 drives; unconnected is the way to go. The Drives and our PLC already have a inexplicit connection and starting a new inexplicit connection between my computer and the Drive wont work.

Thanks for the info again and have a Happy Holidays.