-
Notifications
You must be signed in to change notification settings - Fork 953
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
Support providing framer type Enum or class #2484
base: dev
Are you sure you want to change the base?
Conversation
@@ -223,8 +223,8 @@ def __init__( | |||
self.handle_local_echo = False | |||
if isinstance(identity, ModbusDeviceIdentification): | |||
self.control.Identity.update(identity) | |||
|
|||
self.framer = FRAMER_NAME_TO_CLASS[framer] | |||
# Support mapping of FramerType to a Framer class, or a Framer class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only the server, if we are to include this feature again, at least it must also support client and sync client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I think I've generalized this to all servers and clients. I couldn't figure out where in the tests to be able to add an "evil" framer, though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you look at the test directory, it has a sub directory called framer, which is where framer testing goes. Please do not change existing tests, but add tests.
FramerBase is an internal class which we do not want public, because having it public makes it a lot harder to change, please see my bullet list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did look at it, but didn't find any tests of framers in a full I/O cycle. I'll look again.
Agreed; I don't think FramerBase should be public -- users can derive "evil" framers from any of the existing public FramerXxx classes.
I am not convinced, allowing custom framers is a good way, because the framers are pretty complicated and a lot more tightly knitted into transport and transaction, than it was before. The framer functionality is defined in the modbus standard, so what would a custom framers implement ? Why do you need to replace the framer, and not just use the byte stream/ pdu trace points that is provided. The trace points allows you to intercept and modify non-standard byte stream or even the pdu contents. A custom framer would need a number of items:
If you implement the above items, then it would become a feature in pymodbus. |
Your idea of "evil" framers is not a bad idea, the simulator does that using the trace points. |
OK, I've extended support for creating custom (eg. "evil") FramerBase classes to the client API. Here's an example of an evil FramerBase class. After using FRAMER_NAME_TO_CLASS[framer] with a FramerType to receive one of the support FramerBase classes, eg. FramerRtu, we can implement a Framer with a random delay:
or that truncates a response:
|
39d8096
to
af69355
Compare
Your examples are good, but you can do exactly the same with the trace methods, and then it will work for tcp/tls/udp and serial, whereas you framer is limited to one type. I will have a look at your code, once it passes CI, assuming your update includes the points I raised. |
To help with ci errors, check_ci.sh is your friend, as it runs the same tests as ci. |
I hope you are aware that using time.sleep() in an async program blocks everything, which probably is not what wanted. |
Ya, I suspect you're right about using the "trace" methods, but I couldn't follow the simulator well enough to easily translate my code -- the "evil" framer seems simpler. Also -- since I use the base class returned by the FramerType lookup - my "evil" framer does extend the appropriate RTU/TLS/TCP or Serial base class. |
How can it extend all the framer types using the base class, framerRTU adds different bytes to the stream than framerTCP does, this means your framer implements something different. Please also be aware the framer class is not part of the recv/send structure but merely a utility class to convert a pdu to/from a byte stream. your class probably works in sunny day situations but most likely not in a number of problem sets. Anyhow we will not expose FramerBase, and based on your example a custom base class needs to guarantee that a user supplied class do not endanger the async work flow. There are at least one example of how to use the tracer methods (all 4). |
I can get a public FramerXxx class using some method (eg. the FramerType Enum and FRAMER_NAME_TO...). Then, I can dynamically create a class (above) based on that dynamically selected FramerXxx base class. Here's another example. Remember, 'framer' in this class definition was dynamically selected, and could be any valid FramerXxx base class:
For the limited scope of this change, I think it provides a valuable set of capabilities to protocol implementers to use pymodbus... |
Yes you can, but you are using internal methods/classes that we do not want to be part of the public API. Nearly anything can be done, when you overwrite/extend internals, and that is your choice to do so, but making it officially part of the API, makes it difficult to maintain because it only can be changed in x.y releases. I have outlined earlier what is needed for custom framers to come back in the API. And just for the record your example can be done with the 2 tracepdu methods. |
Your comment in the example is not correct, the exception is logged, and depending on the recv/send direction a retry or a close is initiated. You are mixing with internal classes so you need to look at how they are used. |
Yes, this code was taking from a system implemented using early pymodbus 1.x; at the time, that was what I experienced. I had to monkey-patch a multitude of pymodbus internals to get Modus Serial RTU over RS-485 to work. Thankfully, most of those issues have now been addressed -- the remaining one was how to implement my coterie of "evil" framers. Rather than try to re-architect them in terms of tracepdu methods, I was trying to get them to work, and the threshold to restoring the previous custom framer class functionality seemed quite low. Custom Framers are always going to be based on modifying "internal" implementations, and I don't think writing a custom framer without the expectation of reading the implementation of existing framers is likely. Do you? In my 20+ years of Python, I have seldom ended up using a Python module without having to read the source code, and (often) fix/patch it. I rarely find that the documentation matches the implementation, so never depend on it, and seldom even read it -- why not just go to the definitive "source" of truth? So, while I would like to retain this custom framer functionality as it was in the 1.x version branch, I know that this is your module, not mine! You should do what you feel is best for your future maintainability! I am burdened with maintaining some very complex python modules (cpppo, python-slip39), and have made some expensive decisions to publish functionality that is burdensome to maintain... |
Please close the PR if you do not to complete it. |
Please be aware that the framer structure have changed a lot since pymodbus v.1, and today it is a utility class not directly in the the send/recv line and finally v.1 did not have async in its core, so simply reusing a v.1 framer is going to cause a lot of problems (like e,g, your time.sleep). |
Yes, timing-related "evil" framers will be impossible to implement cleanly due to asyncio, but structural framers will be a natural and simple implementation. There are probably a multitude of partially-broken Modbus devices out there that require slight modifications or extensions to the standard wire protocol in order to function, where a custom Framer would be the natural solution. I'm trying to decide on my approach -- I need to deliver some functionality next week, so I may just fork the project and issue my own version on PyPI... Alternatively, if I implement a minimal example and test case for a structural "evil" or enhanced-protocol Framer, are you saying you may merge the functionality? |
Why are they impossible to implement we do it in the tracers, without problems, you just cannot use time.sleep because it blocks everything. The custom framer is really the wrong approach, mostly because framer is now a utility class so e.g. delay will not work correctly. did you investigate the tracer, they give you all you need. The simulator allows to delay packet, break packet into multiple send with delay, send malicious bytes before/after the packet, which we have used in several projects to help with bad modbus implementations, the code as you say the truth and easy to grasp and twist to your needs. Custom framer will not be accepted without the list I wrote, simply because it does not solve e.g. delay and its far too complicated (if you want your server to actually work). you are of course free to fork your own pymodbus, please remember to follow the foss rules, which means fork not copy, and a reference to the original pymodbus, otherwise you might run into problems with pypi. |
Thinking about it, why do you need your own version of pymodbus, you can simply use the standard Pymodbus and overwrite the framer object with your own object....that is far less maintenance burden for you. I often take the approach to test out new features, mainly because it makes a clear break, between the product pymodbus and my experiment pymodbus. |
pymodbus/server/async_io.py
Outdated
@@ -166,9 +166,10 @@ async def server_async_execute(self, request, *addr): | |||
response = await request.update_datastore(context) | |||
|
|||
except NoSuchSlaveException: | |||
Log.error("requested slave does not exist: {}", request.slave_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You remove one line and add the same information in 2 lines, that does not make sense ?
the app knows if it has requested slaves to be ignored.
Secondly this change seems unrelated to the title of this PR.
67e3fc5
to
87535cd
Compare
Historically, one could create new Framer classes and supply them. For example, to implement "evil" framers, to test sending various errors in Modbus.
This fix maintains support for supplying a FramerType Enum, and restores the ability to pass a Framer class.