Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CIP IO - EDS #260

Open
thalesmaoa opened this issue Jan 27, 2023 · 10 comments
Open

CIP IO - EDS #260

thalesmaoa opened this issue Jan 27, 2023 · 10 comments
Labels
question Further information is requested

Comments

@thalesmaoa
Copy link

Hi, I'm starting with a Ethernet/IP device which I have a EDS file. After some sniffing using Wireshark, I've noticed that it uses explicit message using UDP.

After a lot of reading, I wasn't able to understand if it is possible to establish this type of connection. Are there any example on how to perform it?

@thalesmaoa thalesmaoa added the question Further information is requested label Jan 27, 2023
@thalesmaoa
Copy link
Author

I figure out how to send a generic message, but I need to open a socket to exchange information over port 2222. I'm having a lot of trouble of doing that.

plc = CIPDriver(host)
plc.open()
response = plc.generic_message(
    service=0x54,
    class_code=ClassCode.connection_manager,
    instance=b"\x01",
    #attribute=b"\x01",
    request_data=b"\x03\xfa\x00\x00\x00\x00\xc1\x61\x83\x6b\x01\x00\x05\x05\x98\xb5\x40\xeb\x01\x00\x00\x00\x10\x27\x00\x00\x10\xc8\x10\x27\x00\x00\x56\xc8\x21\x09\x34\x04\x80\x02\x0c\x00\xd7\xdc\x02\x00\x20\x04\x24\x80\x2c\x70\x2c\x64",
    data_type=REAL,
    connected=False,
    unconnected_send=False,
    route_path=False,
    )

@ottowayi
Copy link
Owner

Changing the port is easy, use the <ipaddress>:<port> syntax for you host variable. Changing the socket type will require you to subclass the driver and override the socket creation. But, you may not have to do any of that. What you're seeing in the pcaps is probably I/O traffic, do they provide any examples on how to use a MSG instruction in Logix? If so, pycomm3 should work as is. Can you post the EDS?

@thalesmaoa
Copy link
Author

Hi @ottowayi , I really appreciate your reply. You are probably right, but I didn't understand it correct.
I've attached the eds and the pcap from the PLC (can open using Wireshark). I can see that, after successful reply, it starts to send CIP IO messages.

When I try the same using pycomm3. It acknowledge, but then, it drops.

Expected behavior:

1	0.000000	10.254.1.253	10.254.1.33	TCP	74	36183 → 44818 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=1295132108 TSecr=0 WS=128
2	0.000369	10.254.1.33	10.254.1.253	TCP	60	44818 → 36183 [SYN, ACK] Seq=0 Ack=1 Win=2048 Len=0 MSS=1460
3	0.000430	10.254.1.253	10.254.1.33	TCP	54	36183 → 44818 [ACK] Seq=1 Ack=1 Win=64240 Len=0
4	0.019890	10.254.1.253	10.254.1.33	ENIP	82	Register Session (Req), Session: 0x00000000
5	0.020381	10.254.1.33	10.254.1.253	ENIP	82	Register Session (Rsp), Session: 0x05010000
6	0.020426	10.254.1.253	10.254.1.33	TCP	54	36183 → 44818 [ACK] Seq=29 Ack=29 Win=64212 Len=0
7	0.049822	10.254.1.253	10.254.1.33	CIP CM	154	Connection Manager - Forward Open (Assembly)
8	0.050886	10.254.1.33	10.254.1.253	CIP CM	164	Success: Connection Manager - Forward Open (Assembly)
9	0.051623	10.254.1.33	10.254.1.253	CIP I/O	146	Connection: ID=0x6B8361C1, SEQ=0000000000, T->O
10	0.054190	10.254.1.33	10.254.1.253	CIP I/O	146	Connection: ID=0x6B8361C1, SEQ=0000000001, T->O

What I get using pycomm3:

0.049822	10.254.1.253	10.254.1.33	CIP CM	154	Connection Manager - Forward Open (Assembly)
0.050886	10.254.1.33	10.254.1.253	CIP CM	164	Success: Connection Manager - Forward Open (Assembly)

And then it drops due to timeout.

Not sure if it is a socket problem, but I need something to establish. One example is
https://github.com/EIPStackGroup/OpENer

EDS_PCAP.zip

@ottowayi
Copy link
Owner

ottowayi commented Feb 4, 2023

Oh I see what's happening, as soon as you register a session the device creates an I/O (class 1) connection and starts sending data (that's the traffic you see on port 2222). Unfortunately pycomm3 is only designed for message/class 3 connections. I'm guessing the forward open data you're sending is telling it you want to open an I/O connection.

What data are you expecting to get from this device? If you want to read I/O data, I think it will still work using pycomm3. There isn't any built in functionality for it, like there is for reading tags, but using a generic message should be possible.

Can you try doing:

from pycomm3 import ClassCode, Services, ModuleIdentityObject, CIPDriver

with CIPDriver(host) as plc:
    response = plc.generic_message(
        class_code=ClassCode.identity_object,
        instance=b"\x01",
        service=Services.get_attributes_all,
        data_type=ModuleIdentityObject,
        connected=False,  # try with True as well
        #unconnected_send=False,  # set to True when connected is False and host has a route
                                                     # if host is just an IP, the omit all together
        name="identity",
    )

        print(response)

@thalesmaoa
Copy link
Author

Thanks for point it out. I'm still learning from ODVA doc, and yes! I can request a class 1 connection, but I need to open a socket using udp to receive the traffic. Since I can't do that, it just drop due to timeout.

The device gives me Analog Inputs and Outputs. Also it has some Digital Input/Output. I just want to integrate it to NodeRed. It won't be a problem to get an array since I now how to map the Assembly info, but I'm kind of lost here.

I tried as you suggested and I can identify the object:

identity, {'product_code': 56535, 'product_name': 'Cube67+ BN-E V2', 'product_type': 'Communications Adapter', 'revision': {'major': 2, 'minor': 6}, ...}, ModuleIdentityObject(UINT(name='vendor'), UINT(name='product_type'), UINT(name='product_code'), Revision(name='revision'), BYTES(name='status'), UDINT(name='serial'), SHORT_STRING(name='product_name')), None

I tried the assembly:

with CIPDriver(host) as plc:
    response = plc.generic_message(
        class_code=ClassCode.assembly,
        instance=b"\x01",
        service=Services.get_attributes_all,
        data_type=ModuleIdentityObject,
        connected=False,  # try with True as well
        #unconnected_send=False,  # set to True when connected is False and host has a route
                                                     # if host is just an IP, the omit all together
        name="identity",
    )

    print(response)
identity, None, ModuleIdentityObject(UINT(name='vendor'), UINT(name='product_type'), UINT(name='product_code'), Revision(name='revision'), BYTES(name='status'), UDINT(name='serial'), SHORT_STRING(name='product_name')), Service not supported

Probably because I'm using ModuleIdentifyObject.

What makes me more confused is that I must configure T->O and O->T. I can't see how is it possible with pycomm3 without request_data hex.

I'm probably missing something.

Do you see the possibility to request data using generic message?

@ottowayi
Copy link
Owner

Apologies for the late reply, but I think the error response is actually good news. It's saying the get attributes all service isn't supported, but it's a response and that means the device got the request. I'm not too familiar with the assembly object and how it works, but you could try using the get_attribute_single service, keep instance at 1, but set attribute to 3, and remove the data_type or set it to None. If this works, you'll get the data attribute back for instance 1 of the assembly. Without specifying the data type you should get back a bytes object and see how the data structured. If you can determine that you can create a type to use as data_type and have it decode it for you. Or leaving it as None you can handle the decoding yourself.

@thalesmaoa
Copy link
Author

It took some time to recover the module to keep testing. I really appreciate your help.

The device response well to pycomm3. From my first message, I can request the CIP IO. The problem is that I can't open UDP socket due to lack of knowledge.

I tried as you suggested but I'm getting an error:

with CIPDriver(host) as plc:
    response = plc.generic_message(
        class_code=ClassCode.assembly,
        instance=b"\x01",
        service=Services.get_attribute_single,
        data_type=None,
        connected=False,  # try with True as well
        attribute=b"\x04",
        #unconnected_send=False,  # set to True when connected is False and host has a route
                                                     # if host is just an IP, the omit all together
        name="identity",
    )

    print(response)
identity, b'', None, Destination unknown, class unsupported, instance undefined or structure element undefined (see extended status) - Extended status out of memory  (05, 00)

To be honest, I had the feeling that I don't know what I am probing, and what I should expect.

@ASolchen
Copy link

I did exactly this: https://github.com/ASolchen/pico-eip
@ottowayi is correct, it establishes a session via TCP port 44818 and does io (implicit) data back and forth on UDP port 2222. My code is buggy and I don't fully understand all of what is going on, but it does work. I used the example at https://github.com/EIPStackGroup/OpENer to capture packets and replicated it in python. It does the EIP commands "List Services", "Register Session", and "Send RR data". After that the "Scanner" (PLC) and "Adapter" (IO Device) send unsolicited to each other at the agreed upon RPI rate on UDP port 2222.
Feel free to let me know if I missed anything or can explain more about how this works.

@ASolchen
Copy link

ASolchen commented Mar 20, 2023

As a follow-up to my last comment, I would love to see this functionality added to pycomm3. The repo I have is actually the other side of the transaction. My code replicates a device ("Adapter" in CIP docs) that a PLC ("Scanner") talks to. If pycomm3 had the connected IO capability it would be the Scanner talking to a device. At least that's how I see the library; as a "Client." Pycomm3 treats the PLC as the server since it opens a port and listens. The flow of info is initiated by Pycomm3. With connected I/O the "Scanner" is the "Client," initiating communications to the "Server" (Adapter).
So I see much less effort needed to add the ability to be a scanner, since functions to initiate it are already here using Generic CIP commands, e.g. List Servers 0x0004, or Register Session 0x0065. All that needs to be added is UDP sockets to actually send and receive I/O data. From my testing it, seems to work better to spawn a new thread to handle the I/O data, but this may just because I can't seem to figure out how to handle socket timeouts.

@ASolchen
Copy link

I looked a bit at getting pycomm3 to be a scanner. The capture below shows an actual PLC to OpenEner scanner.


        O->T Network Connection Parameters: 0x4826
            0... .... .... .... = Redundant Owner: Non-Redundant (0)
            .10. .... .... .... = Connection Type: Point to Point (2)
            .... 10.. .... .... = Priority: Scheduled (2)
            .... ..0. .... .... = Connection Size Type: Fixed (0)
            .... ...0 0010 0110 = Connection Size: 38 bytes
        T->O RPI: 30.000ms
        T->O Network Connection Parameters: 0x4822
            0... .... .... .... = Redundant Owner: Non-Redundant (0)
            .10. .... .... .... = Connection Type: Point to Point (2)
            .... 10.. .... .... = Priority: Scheduled (2)
            .... ..0. .... .... = Connection Size Type: Fixed (0)
            .... ...0 0010 0010 = Connection Size: 34 bytes
        Transport Type/Trigger: 0x81, Direction: Server, Trigger: Cyclic, Class: 1
        Connection Path Size: 15 words

This is in the net_params of the _fowardopen method. Unfortunately, these seem to be hard-coded in the method:
init_net_params = 0b_0100_0010_0000_0000 # CIP Vol 1 - 3-5.5.1.1
This value would set the following:
Non-redundant, Point to Point, Low Priority, Fixed Size
If we could change this to Scheduled priority we may be able to get it connected. From the captures I have, it only needs this forwardopen command formatted correctly, and the 2 devices start the UDP packets. We would just need to start sending and receiving datagrams.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants