Repository: pymodbus-dev/pymodbus

Issue:
Single byte error response handling
### Versions

- Python: 3.11
- OS: Ubuntu 22.04
- Pymodbus: 3.6.2
- Modbus Hardware (if used): Sungrow Inverter

### Pymodbus Specific

- Client: tcp async

### Description

#### Current State
When I query my sungrow inverter with a register I'm not supposed to be querying, I'm gettting a malformed package and the connection is dopped.

Wireshark:
![image](https://github.com/pymodbus-dev/pymodbus/assets/5074553/41de8660-c269-49cb-a792-bfd1a85b500a)

TCP Payload: 00 02 00 00 00 02 01 84 02

#### Request
This line of code assumes that `data` has more than a single element. However, `data` in my case is only b'\x84'.
The response is being assigned correctly: Exception Response(132, 4, IllegalFunction)
But in the marked line I'm getting an pymodbus.logging:General exception: index out of range

See https://github.com/pymodbus-dev/pymodbus/blob/6913e9c829009f3704f75d7cb7fff019c27c6770/pymodbus/factory.py#L351 

I can fix this locally by changing
```python
        response.decode(data[1:])
```
to
```python
        if len(data) > 1:
            response.decode(data[1:])
```
but I really have no idea what I'm doing here.

However that change allows me to handle the error super nicely on the response object:
```python
        if rr.isError():
            if isinstance(rr, pymodbus.pdu.ExceptionResponse):
                if rr.exception_code == pymodbus.pdu.ModbusExceptions.IllegalFunction:
                    print("unsupported register")
```


### Code and Logs

```python
--------------------
2024-01-09 17:10:45,355 DEBUG logging:103 send: 0x0 0xd 0x0 0x0 0x0 0x6 0x1 0x4 0x13 0x90 0x0 0x2
2024-01-09 17:10:45,355 DEBUG logging:103 Adding transaction 13
2024-01-09 17:10:45,366 DEBUG logging:103 recv: 0x0 0xd 0x0 0x0 0x0 0x2 0x1 0x84 0x2 old_data:  addr=None
2024-01-09 17:10:45,366 DEBUG logging:103 Processing: 0x0 0xd 0x0 0x0 0x0 0x2 0x1 0x84 0x2
2024-01-09 17:10:45,366 DEBUG logging:103 Factory Response[132]
*************** data:  b'\x84'
*************** response:  Exception Response(132, 4, IllegalFunction)
2024-01-09 17:10:45,366 ERROR logging:115 General exception: index out of range
2024-01-09 17:10:45,366 DEBUG logging:103 Resetting frame - Current Frame in buffer - 0x0 0xd 0x0 0x0 0x0 0x2 0x1 0x84 0x2
Fatal error: protocol.data_received() call failed.
protocol: <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7f2f1aac8e90>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib/python3.11/asyncio/selector_events.py", line 1009, in _read_ready__data_received
    self._protocol.data_received(data)
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/transport/transport.py", line 312, in data_received
    self.datagram_received(data, None)
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/transport/transport.py", line 346, in datagram_received
    cut = self.callback_data(self.recv_buffer, addr=addr)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/client/base.py", line 186, in callback_data
    self.framer.processIncomingPacket(data, self._handle_response, slave=0)
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/framer/base.py", line 139, in processIncomingPacket
    self.frameProcessIncomingPacket(single, callback, slave, **kwargs)
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py", line 147, in frameProcessIncomingPacket
    self._process(callback, tid)
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py", line 158, in _process
    raise ModbusIOException("Unable to decode request")
pymodbus.exceptions.ModbusIOException: Modbus Error: [Input/Output] Unable to decode request
2024-01-09 17:10:45,367 DEBUG logging:103 Connection lost comm due to Modbus Error: [Input/Output] Unable to decode request
2024-01-09 17:10:45,367 DEBUG logging:103 Getting transaction 13
2024-01-09 17:10:45,368 DEBUG logging:103 Wait comm 100.0 ms before reconnecting.
Traceback (most recent call last):
  File "/home/alex/homeassistant-sungrow/custom_components/sungrow/core/modbus.py", line 325, in _read_range
    rr = await self._client.read_input_registers(address_start - 1, count=address_count, slave=self._slave)  # type: ignore
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alex/.local/lib/python3.11/site-packages/pymodbus/client/base.py", line 167, in async_execute
    resp = await asyncio.wait_for(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/tasks.py", line 479, in wait_for
    return fut.result()
           ^^^^^^^^^^^^
pymodbus.exceptions.ConnectionException: Modbus Error: [Connection] Connection lost during request
```



Discussion:
Let's start by the basic, the mdap header (first 7 bytes) says the data length is 2, and you receive 84 02.... why do you think you only receive 84 ?

A modbus package is minimum 2 byte (funcion code and data byte).
But anyhow if the code change solve your problem, then please submit a pull request so other can benefit from it.

I am still wondering why wireshark says it is a malformed mode tcp-packet, that seems to be another problem.
@janiversen Unfortunately I have no idea about modbus, but I followed the data a little following your 2 byte hint.

The second byte is truncated here (error = False): https://github.com/pymodbus-dev/pymodbus/blob/6913e9c829009f3704f75d7cb7fff019c27c6770/pymodbus/framer/socket_framer.py#L144


So a totally different code change that fixes **my** problem is deleting the `- 1` from those two lines:
https://github.com/pymodbus-dev/pymodbus/blob/6913e9c829009f3704f75d7cb7fff019c27c6770/pymodbus/framer/socket_framer.py#L99
https://github.com/pymodbus-dev/pymodbus/blob/6913e9c829009f3704f75d7cb7fff019c27c6770/pymodbus/framer/socket_framer.py#L80

By doing that, I get an `IllegalAddress` instead of an `IllegalFunction` which does sound even better. But I have no idea what deleting the `- 1` breaks.
I will take a closer look later this week, I can easily simulate the wireshark packet.
P.S. I mentioned the data is only 0x84. That's what I get looking at `data` in `_helper`. That's because in the debug log above `self._buffer` is printed. However, as you see in the code above, `self._buffer` is not being used! It's using the value from `getFrame()`.

Resolving patch:
diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py
--- a/pymodbus/framer/socket_framer.py
+++ b/pymodbus/framer/socket_framer.py
@@ -96,7 +96,7 @@ def getFrame(self):
 
         :returns: The next full frame buffer
         """
-        length = self._hsize + self._header["len"] - 1
+        length = self._hsize + self._header["len"]
         return self._buffer[self._hsize : length]
 
     # ----------------------------------------------------------------------- #


Test patch:
diff --git a/test/test_framers.py b/test/test_framers.py
--- a/test/test_framers.py
+++ b/test/test_framers.py
@@ -341,7 +341,6 @@ def test_recv_packet(rtu_framer):
     rtu_framer.client = client
     assert rtu_framer.recvPacket(len(message)) == message
 
-
 def test_process(rtu_framer):
     """Test process."""
     rtu_framer._buffer = TEST_MESSAGE  # pylint: disable=protected-access
@@ -387,3 +386,19 @@ def _handle_response(_reply):
             assert not response_ok, "Response should not be accepted"
         framer.processIncomingPacket(part2, _handle_response, slave=0)
         assert response_ok, "Response is valid, but not accepted"
+
+
+def test_recv_socket_exception_packet():
+    """Test receive packet."""
+    response_ok = False
+
+    def _handle_response(_reply):
+        """Handle response."""
+        nonlocal response_ok
+        response_ok = True
+
+    message = bytearray(b"\x00\x02\x00\x00\x00\x02\x01\x84\x02")
+    response_ok = False
+    framer = ModbusSocketFramer(ClientDecoder())
+    framer.processIncomingPacket(message, _handle_response, slave=0)
+    assert response_ok, "Response is valid, but not accepted"