-
Notifications
You must be signed in to change notification settings - Fork 1k
Properly process incomplete RTU frames, improve test coverage #608
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
Conversation
sfarbotka
left a comment
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.
Please, find my comments in the code to speed up the process of code review
| :param decoder: The decoder factory implementation to use | ||
| """ | ||
| self._buffer = b'' | ||
| self._header = {'uid': 0x00, 'len': 0, 'crc': '0000'} |
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.
Other parts of the code expect that crc is bytes object
| self.populateHeader() | ||
| frame_size = self._header['len'] | ||
| data = self._buffer[:frame_size - 2] | ||
| crc = self._buffer[frame_size - 2:frame_size] |
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.
crc is already extracted from message and can be accessed as self._header['len'], no need to extract it again
| # Error response, no header len found | ||
| self.resetFrame() | ||
|
|
||
| self._buffer = self._buffer[self._header['len']:] |
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.
In this pull request self._header now always populated with len key (as in other framer classes - binary, ascii, socket, etc). That's why try/catch is not required anymore.
| self._buffer = self._buffer[self._header['len']:] | ||
| _logger.debug("Frame advanced, resetting header!!") | ||
| self._header = {} | ||
| self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'} |
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 change makes self._header content consistent with other framer classes, and resolves issues when some functions expect that this dict always contain some keys.
| "buffer - {}".format(hexlify_packets(self._buffer))) | ||
| self._buffer = b'' | ||
| self._header = {} | ||
| self._header = {'uid': 0x00, 'len': 0, 'crc': b'\x00\x00'} |
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 change makes self._header content consistent with other framer classes, and resolves issues when some functions expect that this dict always contain some keys.
| size = pdu_class.calculateRtuFrameSize(data) | ||
| self._header['len'] = size | ||
|
|
||
| if len(data) < size: |
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.
Slice operation data[size - 2:size] doesn't raise IndexError if there is not enough data in array. But other functions in this module expects that this method raises exception in case of incomplete frame. That's why this if is added
| @pytest.mark.parametrize("data", [b'', b'abcd']) | ||
| def test_advance_framer(rtu_framer, data): | ||
| rtu_framer._buffer = data | ||
| @pytest.mark.parametrize("data", [ |
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.
Useless tests (which do not test anything but only improve code coverage) are replaced with new tests.
| data, expected = data | ||
| rtu_framer._buffer = data | ||
| rtu_framer.advanceFrame() | ||
| # rtu_framer.advanceFrame() |
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 function is useless here and only adds some complexity to the code. It is removed, and some additional test cases are added to be sure that this function works in other cases
| b'\x11\x03\x06', | ||
| b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x43', | ||
| ]) | ||
| def test_rtu_populate_header_fail(rtu_framer, data): |
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.
Ensure that populateHeader() properly deals with incomplete frames
| (b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD', 16, True, False), # incorrect unit id | ||
| (b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD\x11\x03', 17, False, True), # good frame + part of next frame | ||
| ]) | ||
| def test_rtu_process_incoming_packet(rtu_framer, data): |
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.
Improve coverage and ensure that RTU framer properly deals with incomplete frames
|
Kudos, SonarCloud Quality Gate passed!
|
|
It looks like all failed tests are not related to my changes |
|
@sfarbotka Thanks for the PR. I will take a look in to this. |
|
Tested the fix using real RTU MODBUS connection, I didn't see any stack traces as before, and didn't found any regressions. |
|
@sfarbotka Thanks for the PR, this is now merged as part of PR #605. I will be closing this PR |
|
@sfarbotka This fix perfectly works in synchronous mode, but fails when asyncio used. The At best (if message is received full) the unit tries to be extracted from response and then compare with itself in Don't know a good way to fix this, just comment out the call to |
|
@olddudealex that seems to be the expected behaviour right? If there is an incomplete data and unit id is not extracted the validation should fail . With async client, it's hard to keep track of the requests and map it to the response incoming. I will take a look but if you have logs of your failing cases that could help us more with coming up with a proper solution. |
|
@dhoomakethu, sorry for long response. Below is the log file of failing unit id validation. If in the line 230 in pymodbus/framer/rtu_framer.py remove unit validation: messages will start to receive correctly. |
This change fixes errors in processing of incomplete frames. Some of them lead to the situation where the framer starts raising exception for all future frames. For example, when only first byte is added to self._buffer, self.populateHeader() adds unit ID to self._header only, and self.isFrameReady() starts raising exceptions until framer instance is re-created.
On the other hand, the code of RTU framer now is consistent with implementation of other framer (same function do the same).
Additionally, to be sure that all changes work, more test module was populated with some new tests, which also improves coverage.