ottowayi / pycomm3

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

Generic CIP Adapter #104

Open ASolchen opened 3 years ago

ASolchen commented 3 years ago

Forgive me if this was proposed already.

Has any thought been given to making a adapter-side version of the CIPDriver? Not only would it be great for a testing, but I could envision someone using a MSG command from a PLC to connect to a PC or Raspberry Pi as an IO adapter. Another use may be the ability for a PLC to conditionally "Push" info to a server and not required the server to continually poll for info.

Are there any large technical challenges to this or would this seem useful? I would be willing to help but am not sure where to start. I do have access to many PLCs, drives, AB programming software, etc.

ottowayi commented 3 years ago

I think it is a great idea and would love to support that. It would really help for staging systems and simulating messaging between controllers when you don't have enough available physical PLCs since the emulators can't do messaging. But, I'm no where prepared to get started on it. Right now I'm in the middle of a pretty rewrite of a lot of the internals that should make it even easier to use, so all my (little) free time is focused on that. Once I get the next release out maybe I'll try and investigate it. I agree though that it would be a nice feature to have and open up a lot more possibilities.

ASolchen commented 3 years ago

Like I said I am willing to help. I think I'm a pretty decent python programmer and do understand the CIP object model from a high level but would need to learn more of the details. Do you see that fitting in the cip_base.py as something like "class CIPAdapter:"? I will fork and start playing with this. Any ideas on how it should fit in would help.

ottowayi commented 3 years ago

Umm, I think it would be best as it's own module and I feel it should be something like CIPServer maybe? Feel free to take a shot at it, I'm always willing to accept help. If you do fork, I would work off the decoupling branch. That is where most of my work is being done right now, the new data type classes will make it easier to define/encode/decode different objects. I have a lot to do though and that branch is current broken, but if you're starting from scratch writing a server that may not affect you. You will have all my improvements though and if you were to use the current DataType or Pack/Unpack classes those are all going away and would need to be replaced anyway.

ASolchen commented 3 years ago

First off, let me know if this is the proper place to discuss details on this. I would understand if the "issues" area on github is only where one would bring up issues. Maybe discussions of details happen some other way, e.g. email, discord, etc. I haven't really worked on any open-source projects. I'm thinking it would be best to just get a stand-alone script running as a proof-of-concept and then work it into the lib. Basically open port 44818 and listen for a client. Is there somewhere in the spec pdfs that explain the overall sequence of handling the CIP connections? From the CIPDriver code it looks like the client registers a session then the server responds that session ID to verify the connection is made. After that, the client can send a generic_message and the server reads the service, class_code, instance and attribute and returns data related to that. In the case of this CIPServer code, the user would need to build some context or values to respond with. If they are invalid the response is some error. This seem correct?

ottowayi commented 3 years ago

I think this is the perfect spot for this type of thing. Discussions are new to me, but they seem like a better solution to just making question/discussion issues.

There are specs in the spec folder, the CIP_Vol1/2 and the EIP Data Access Manual (pm020). The CIP specs will be deleted soon though when I merge in develop. I'm not sure of the legality of me having them in the repo and didn't realize they were in there until recently. So I'm removing them to be safe. Those documents are large and take awhile to understand, but they're thorough.

I think you have the right idea though. It should listen for a register session command, reply with a session ID. Then certain commands require a connection, so like reading a tag would require that a forward open is done. So it should listen for a forward open request for that session. I'm not sure if it specifies anywhere which services require connections or not, but maybe to get started skip that. The new type system in the decoupling branch will make it a lot simpler to encode response objects, so you may want to work off of that branch for now. Hopefully I'll merge it soon and get a release out. I'm working on fixing the tests and documentation right now, then hopefully it'll be done and released.

au-amck commented 2 years ago

I see some time has passed since the last comment. I also think a driver acting as a generic adapter would be a great feature. It would allow PLC so send data when it's required, rather the polling the PLC looking for change. In some situations would reduce network traffic significantly. Have seen some commercial/industrial (proprietary) products that do this.

ASolchen commented 2 years ago

I agree. The use case I imagined was a PLC being able to "push" data to a server that had this adapter listening. For example, a database entry on batch completion for reporting. Like @au-amck said, there would be no traffic unless needed vs. a polling loop to continually check for a "done" tag. The way I was thinking of starting this was to replicate what a PowerFlex drive does when you read a parameter. If this is a flawed approach, let me know. The way I understand it, you need to do the following:

ASolchen commented 2 years ago

So I played around with this a bit. I used a CompactLogix I had laying around and did the cip generic_message example for getting the MAC address. I captured the traffic with Wireshark to review it. Then I made a python socket script and basically replicated what the PLC did. I created a gist on Github for this if you'd like to try it: cip_server.py I also busted out the data in functions to explain how the packets are built. I'll admit that this is a long way from a driver since it's very "rigged." It does not parse commands or sender context, etc. I think most of those functions are available in the library. I'll see if I can use some of them to actual parse things required.

au-amck commented 2 years ago

I've only been using Python for a short time and am learning more about it all the time. Thanks to all the work that @ottowayi and the original pycomm team have done to make pycomm3 such an easy library to use. I have tried pycomm and another library with variable results. So far I've had the best experience with pycomm3. I had a similar idea to @ASolchen creating a server to act as a PLC. I started going through the CIP documents quite some time back (before python) and was a little overwhelmed. I'm hoping I can contribute to pycomm3 in some small way, even if it's only testing. I'll definitely take a look at the code you have posted. I understand that the server side that we are talking about here is the opposite side of what pycomm3 is at the moment and while it would use of lot of the structures and classes already defined it's different logic that needs to be tested. If we need to move this discussion to another more appropriate section please let us know. In my opinion there are 2 different functions of the "Generic CIP Adapter". One that would receive a CIP message (same as a MSG from a PLC) that would contain tagname and values that could be passed on to program code. This would be the "simplest" CIP device. Doesn't have to provide the full PLC functionality, just got to receive and acknowledge message.

The second functionality would be to act as a device in the I/O network. I imagine this is more complicated, even as a Generic Ethernet Device you would have to deal with all the I/O and use input/output words or Datalink style to pass data.

I would be focusing on the first function before the second.

ottowayi commented 2 years ago

Yeah, what you're describing is the difference between explicit (MSG, pycomm3, etc) and implicit (I/O) messaging. Handling implicit messaging is much more complicated, so if anything I would start with being able to receive explicit messages. I'm currently working on rewrite of a lot of the internals, especially the protocol implementations. I didn't design the current implementation very well, like it really blurs the line between EIP and CIP when they're really separate protocols. Once I have that work done, making a server might be easier.

ASolchen commented 2 years ago

I agree that the CIPServer would use explicit messaging. Correct me if I'm wrong, but pycomm3 does not do implicit messages currently, even from the client side. I spoke about this a bit in the discussions area Connected CIP Adapter but from what I saw in captures is that the implicit version establishes a session and then switches to UDP sockets to pass data at an RPI. I would also say that initially the CIPServer would most likely only provide the base for responding to generic messages. When @au-amck says the MSG block of a PLC (or pycomm CIPDriver) "contain the tagname", that is really a Rockwell specific service. The user would need to extend this class to provide advanced services like reading tags by name. (initially, at least)

ASolchen commented 2 years ago

I have been making some progress on this. At first I "rigged" a python server to just mimic the data I saw a PLC give. Then broke the response packet apart to see how it's built. Then I started using the data_type encodes, services EnumMaps to get command data etc. Now I'm trying to implement the Packet classes to build this. Mainly, this is for @ottowayi . The biggest challenge I see is all of the classes are intended to be from the client side. For example, the CIPDriver creates a RegisterSessionRequestPacket class instance by just passing the protocol version as an arg. The RegisterSessionRequestPacket class uses the RegisterSessionResponsePacket to parse the response. The ResponsePackets allow you to pass raw_data in the init since the are expected to come from the other side of the connection (client requests, server responds).

I guess what I'm asking is do you think that both the Request and Response classes should be able to dynamically "decide", or be told, which side of the connection they are on. Seems like in the end, the Packet classes should have the same args and maybe a kwarg like server_mode=False or something. Thoughts?

ASolchen commented 2 years ago

I have functioning version of the CIPServer in my fork under the branch "cip_server." There are examples of both the server and client in the examples directory. I haven't used a PLC to MSG yet, but it works well with the CIPDriver.generic_message. Right now the server closes when the client unregisters the session. We could spawn a thread for each client to handle multiple clients and keep the server running. I don't have a lot of experience with socket servers and am just looking at examples in python.

I decided not to touch any of the existing classes. There is still plenty to do. I used the concept that the user would have to build a CIP_Context. This context holds CIP_Classs, the classes hold CIP_Instances, and the instances hold CIP_Attributes. Right now each attribute holds a single Tag. This was just to make it similar end to end, but this may need to change.

ottowayi commented 2 years ago

It sounds like you've made some good progress, this would be very useful functionality and I would like to continue with it. I'm not sure if this will be good or bad news, but I'm in the process of refactoring/throwing away the current Packet classes. I didn't fully appreciate the scope when I came up with them and I've found them hard to maintain. I think the biggest issue is they handle both the EtherNet/IP encapsulation and the CIP protocol, when really they are separate protocols that should be handled independently. And the subclassing has made it hard to figure out what is going on while creating a request or parsing a response because so much of that code lives way higher up in the base classes. My new approach will be to have separate EtherNet/IP and CIP request/response classes and an object library of CIP classes, attributes, services, etc. I think the work I'm doing will make implementing the server-side code easier, a lot of it is based on the DataTypes.. each piece handles encoding and decoding of itself and then passing on the remaining to the next layer down. I've been doing it all locally, so there isn't a branch for it yet, but I'll see if I can get it pushed up soon. I haven't had much time recently to work on it and was kinda hoping to be farther longer by now, but I'm hoping this will make it much easier to maintain and add features.

Edit: I've pushed it to the v2 branch, but it's pretty early and nothing in finalized. Hopefully now with me making it public, I'll have more pressure to work on it and get it finished.

ASolchen commented 2 years ago

sounds good. I've continued to play with it. I stopped using the Packet classes since the only thing I was using them for was to get the .cmd out of. I made methods on the server to handle different requests but see that will quickly get hairy and need some classes to pass off jobs to. Also, I did try to use a PLC to MSG to the server, but instead of it directly asking for the service that I entered, it first asks to list_services. This is a little different from the CIPDriver.generic_message() where it only does the request you enter. I'll need to review the CIP and Rockwell Data Access docs for more info to implement that service (and any others it may ask for later).