Repository: pymodbus-dev/pymodbus

Issue:
Function code 0x15 fails because of incomplete receival of bytes
<!--
Please use the Pymodbus gitter channel at https://gitter.im/pymodbus_dev/Lobby or Stack Overflow(tag [pymodbus](https://stackoverflow.com/questions/tagged/pymodbus) for
support questions.

Before opening a new issue, make sure you do the following:
    * check that your issue isn't already filed: https://github.com/riptideio/pymodbus/issues
    * check the discussions forum https://github.com/riptideio/pymodbus/discussions
    * prepare a short, runnable example that reproduce the issue with the latest development version of Pymodbus
-->

### Versions

* Python:3.10.7
* OS:Win 10
* Pymodbus:3.0.2
* Modbus Hardware (if used):USB to TTL(CH340)

### Pymodbus Specific
* Server: rtu - sync
* Client: rtu - sync

### Description

What were you trying, what has happened, what went wrong, and what did you expect?

We were trying to send many bytes data to slave by function code 0x15. Finally, we found that serial received data is incomplete, especially when the baud rate is low. In function recv of pymodbus/client/serial.py, when size if not None, serial does not wait data.

### Code and Logs

```python
# code and logs here.

from pymodbus.client import ModbusSerialClient as ModbusClient
from pymodbus.file_message import FileRecord, WriteFileRecordRequest

import os
import argparse

parser = argparse.ArgumentParser(description='Firmware update on MODBus RTU.')

parser.add_argument('-f',
                    action='store',
                    type=str,
                    metavar='',
                    dest='file',
                    help='Full path and filename of the file to be downloaded. '
                         'Example: -f C:\git\Firmware_Download\my_file.bin')

args = parser.parse_args()

client = ModbusClient(method='rtu',port ='COM8', parity = 'E', baudrate=9600, timeout=5)
client.connect()

file_len = os.path.getsize(args.file)
record_len = file_len // 2
record_len = record_len if record_len < 121 else 121

file = open(args.file, 'rb')
file_data = file.read(record_len * 2)

frame = [FileRecord(reference_type=0x06, file_number=0x01, record_number=0, 
                 record_data=file_data, record_length=record_len)]
rq =  WriteFileRecordRequest(records=frame, unit=1)
print(rq.encode())
rr = client.execute(rq)

client.close()
```
When I modify the recv in this way, there is no problem. I don't know if there will be any other problems with this change. I hope you can help me review. Thank you.
```python
    def recv(self, size):
        """Read data from the underlying descriptor."""
        super().recv(size)
        if not self.socket:
            raise ConnectionException(
                self.__str__()  # pylint: disable=unnecessary-dunder-call
            )
        if size is None:
            size = self._wait_for_data()
        elif size > self._in_waiting():
            self._wait_for_data()
        result = self.socket.read(size)
        return result
```



Discussion:
Looks like you found a problem, pull requests are welcome.
This problem still seems to exist, but it can occur occasionally when transferring large files.
I was using 115200 or 9600 baud rate, and after several attempts, I found a 10ms delay in the following code. I think this delay should be set according to the baud rate, because depending on the PC performance, there may be 10ms of unscheduled execution time, resulting in incomplete packets received.
After changing the delay to 20ms, I tried 1200 baud rate without this problem. I wonder if there is a better solution. thank you

```python
    def _wait_for_data(self):
        """Wait for data."""
        size = 0
        more_data = False
        if self.params.timeout is not None and self.params.timeout:
            condition = partial(
                lambda start, timeout: (time.time() - start) <= timeout,
                timeout=self.params.timeout,
            )
        else:
            condition = partial(lambda dummy1, dummy2: True, dummy2=None)
        start = time.time()
        while condition(start):
            available = self._in_waiting()
            if (more_data and not available) or (more_data and available == size):
                break
            if available and available != size:
                more_data = True
                size = available
            time.sleep(0.01)
        return size
```
You do not mention which version you are testing with ? nor do you say which file you talk about,

It sounds like a good idea to have this delay depending on the baudrate, please submit a pull request, so we can review your suggested changes (you suggested another change originally).
Sorry for the lack of clarity. I just tried the latest version, 3.1.2, and it's the same problem I described. Modify _wait_for_data in pymodbus/client/serial.py.
Yeah I do not think we made any changes to that logic. As I wrote earlier please submit a pull request, since you believe you have a solution.

Resolving patch:
diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py
--- a/pymodbus/client/serial.py
+++ b/pymodbus/client/serial.py
@@ -336,8 +336,8 @@ def recv(self, size):
             )
         if size is None:
             size = self._wait_for_data()
-        elif size > self._in_waiting():
-            self._wait_for_data()
+        if size > self._in_waiting():
+            size = self._wait_for_data()
         result = self.socket.read(size)
         return result
 


Test patch:
diff --git a/test/test_client_sync.py b/test/test_client_sync.py
--- a/test/test_client_sync.py
+++ b/test/test_client_sync.py
@@ -388,7 +388,7 @@ def test_basic_sync_serial_client(self, mock_serial):
         self.assertEqual(0, client.send(None))
         client.state = 0
         self.assertEqual(1, client.send(b"\x00"))
-        self.assertEqual(b"\x00", client.recv(1))
+        self.assertEqual(b"", client.recv(1))
 
         # connect/disconnect
         self.assertTrue(client.connect())