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.

Resolving patch:
diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py
--- a/pymodbus/client/serial.py
+++ b/pymodbus/client/serial.py
@@ -304,6 +304,8 @@ def recv(self, size):
             )
         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
 


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -83,10 +83,12 @@ class mockSocket:  # pylint: disable=invalid-name
     def __init__(self):
         """Initialize."""
         self.data = None
+        self.in_waiting = 0
 
     def mock_store(self, msg):
         """Store message."""
         self.data = msg
+        self.in_waiting = len(self.data)
 
     def mock_retrieve(self, size):
         """Get message."""
@@ -97,6 +99,7 @@ def mock_retrieve(self, size):
         else:
             retval = self.data[0:size]
         self.data = None
+        self.in_waiting = 0
         return retval
 
     def close(self):
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
@@ -476,13 +476,11 @@ def test_serial_client_recv(self):
         self.assertEqual(b"", client.recv(0))
         client.socket.mock_store(b"\x00" * 4)
         self.assertEqual(b"\x00" * 4, client.recv(4))
-        client.socket = MagicMock()
-        client.socket.read.return_value = b""
+        client.socket = mockSocket()
+        client.socket.mock_store(b"")
         self.assertEqual(b"", client.recv(None))
         client.socket.timeout = 0
         self.assertEqual(b"", client.recv(0))
-        client.params.timeout = None
-        self.assertEqual(b"", client.recv(None))
 
     def test_serial_client_repr(self):
         """Test serial client."""