Repository: pymodbus-dev/pymodbus

Issue:
Forwarder example rewrites calls to "write_register" to "read_holding_register" (func 06 to 03)
### Versions

* Python: 3.10.6
* OS: Win11 x64
* Pymodbus: 3.0.0.dev4
* Modbus Hardware (if used): n/a

### Pymodbus Specific
* Server: definately tcp, maybe rtu/ascii - sync/async
* Client: definately tcp, maybe rtu/ascii - sync/async

### Description

When utilizing the [forwarder example](https://github.com/riptideio/pymodbus/blob/dev/examples/modbus_forwarder.py) with `ModbusTcpClient` all **writes** will be executed as **reads**.

### Code and Logs

```python
client = ModbusClient("modbus_forwarder", port=5020, timeout=10)
client.connect()

rr = client.read_holding_registers(0x90, 1, unit=247)
rr = client.write_register(0x90, 0x1234, unit=247)	# will be executed as the read above
```

Please don't remove the "read only" feature - it was helpful in a certain case. But please make it optional and documented.


Discussion:
I am not aware to have removed any “read only” parameter….where did you see that earlier?

Writes should not be reads let me test what happens. I am pretty sure this was not changed from the old example.
and please do remember that Pull Requests are welcome, that is the best way to get things changed.
As much as I'd like to help... the inner workings of `DataStore` are way over my head.
And I did not say there was a read only parameter - it just behaves like `forwarder.readonly = True`. I just meant to say such a parameter proves useful in some cases though generally it should be set to False.
you wrote "Please don't remove the "read only" feature" and I am still not aware of having removed that, I am also not aware of such a feature in the example.
Well... the example is not talking about any rewrite feature. Even I assumed it would just forward the request as is.

Then I found out it does rewrite requests which I (formerly unknowingly) made use of.

So at first someone needs to find out why and where this happens. Then it should be fixed. But with the option to keep this behavior.
I think it states it pretty clear:
```
This is a repeater or converter and  an example of just how powerful datastore is.
```
Using a "datastore" means storing the data and thus rewrite the requests.

If is true that a request forwarder would be nice to have as example, and are in my plans. Actually it is quite easy to make using the "handle=" parameter in the server.
It would be pretty "clear" if it would state it forwards in a read only fashion. Simple storing and fetching does in no way imply it will be changed in between. So could you please hint at where I have to look to make it forward **as is**?
Pull requests are welcome. And yes storing and fetching implies directly that the original request is not forwarded.
Actually the usage of DataStore is not that obvious in the forwarder example (I know it's in the imports). But from what do you derive it is modifying the requests? And please guide me to make it not modify it. Once solved I'll create a PR for the forwarder example to make this the default behavior.
line 40:
```
        store = RemoteSlaveContext(client)
    context = ModbusServerContext(slaves=store, single=True)
```
Interconnect server datastore and client datastore.
Confirmed bug, it seems to be a more general problem.

But please this a data forwarder it does NOT forward the exact calls, which would be nice to have.

Resolving patch:
diff --git a/examples/modbus_forwarder.py b/examples/modbus_forwarder.py
--- a/examples/modbus_forwarder.py
+++ b/examples/modbus_forwarder.py
@@ -1,11 +1,15 @@
 #!/usr/bin/env python3
 """Pymodbus synchronous forwarder.
 
-This is a repeater or converter and  an example of just how powerful datastore is.
+This is a repeater or converter and an example of just how powerful datastore is.
 
-It consist of a server (any comm) and a client (any comm) and basically all request
-received by the server is sent by client and all responses received by the
-client is sent back by the server.
+It consist of a server (any comm) and a client (any comm), functionality:
+
+a) server receives a read/write request from external client:
+
+    - client sends a new read/write request to target server
+    - client receives response and updates the datastore
+    - server sends new response to external client
 
 Both server and client are tcp based, but it can be easily modified to any server/client
 (see client_sync.py and server_sync.py for other communication types)
@@ -69,5 +73,7 @@ async def run_forwarder(args):
             )
         ],
     )
+    cmd_args.port = 5021
+    cmd_args.client_port = 5020
     run_args = setup_forwarder(cmd_args)
     asyncio.run(run_forwarder(run_args))
diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py
--- a/pymodbus/datastore/remote.py
+++ b/pymodbus/datastore/remote.py
@@ -49,11 +49,18 @@ def validate(self, fc_as_hex, address, count=1):
         """
         txt = f"validate[{fc_as_hex}] {address}:{count}"
         _logger.debug(txt)
-        self.result = self.__get_callbacks[self.decode(fc_as_hex)](address, count)
+        group_fx = self.decode(fc_as_hex)
+        if fc_as_hex in self._write_fc:
+            func_fc = self.__set_callbacks[f"{group_fx}{fc_as_hex}"]
+        else:
+            func_fc = self.__get_callbacks[group_fx]
+        self.result = func_fc(address, count)
         return not self.result.isError()
 
     def getValues(self, fc_as_hex, address, count=1):
         """Get values from real call in validate"""
+        if fc_as_hex in self._write_fc:
+            return [0]
         return self.__extract_result(self.decode(fc_as_hex), self.result)
 
     def setValues(self, fc_as_hex, address, values):
@@ -89,19 +96,32 @@ def __build_mapping(self):
             ),
         }
         self.__set_callbacks = {
-            "d": lambda a, v: self._client.write_coils(  # pylint: disable=unnecessary-lambda
+            "d5": lambda a, v: self._client.write_coil(  # pylint: disable=unnecessary-lambda
+                a, v, **kwargs
+            ),
+            "d15": lambda a, v: self._client.write_coils(  # pylint: disable=unnecessary-lambda
+                a, v, **kwargs
+            ),
+            "c5": lambda a, v: self._client.write_coils(  # pylint: disable=unnecessary-lambda
+                a, v, **kwargs
+            ),
+            "c15": lambda a, v: self._client.write_coils(  # pylint: disable=unnecessary-lambda
+                a, v, **kwargs
+            ),
+            "h6": lambda a, v: self._client.write_register(  # pylint: disable=unnecessary-lambda
                 a, v, **kwargs
             ),
-            "c": lambda a, v: self._client.write_coils(  # pylint: disable=unnecessary-lambda
+            "h16": lambda a, v: self._client.write_registers(  # pylint: disable=unnecessary-lambda
                 a, v, **kwargs
             ),
-            "h": lambda a, v: self._client.write_registers(  # pylint: disable=unnecessary-lambda
+            "i6": lambda a, v: self._client.write_register(  # pylint: disable=unnecessary-lambda
                 a, v, **kwargs
             ),
-            "i": lambda a, v: self._client.write_registers(  # pylint: disable=unnecessary-lambda
+            "i16": lambda a, v: self._client.write_registers(  # pylint: disable=unnecessary-lambda
                 a, v, **kwargs
             ),
         }
+        self._write_fc = (0x05, 0x06, 0x15, 0x16, 0x17)
 
     def __extract_result(self, fc_as_hex, result):
         """Extract the values out of a response.


Test patch:
diff --git a/test/test_examples.py b/test/test_examples.py
--- a/test/test_examples.py
+++ b/test/test_examples.py
@@ -12,7 +12,9 @@
 from examples.client_calls import run_async_calls, run_sync_calls
 from examples.client_payload import run_binary_payload_client
 from examples.client_sync import run_sync_client, setup_sync_client
-from examples.modbus_forwarder import run_forwarder, setup_forwarder
+from examples.modbus_forwarder import setup_forwarder
+
+# from examples.modbus_forwarder import run_forwarder, setup_forwarder
 from examples.server_async import run_async_server, setup_server
 from examples.server_payload import run_payload_server
 from examples.server_sync import run_sync_server
@@ -171,16 +173,16 @@ async def test_exp_forwarder(  # pylint: disable=unused-argument
     """Test modbus forwarder."""
     if pytest.IS_WINDOWS:
         return
-    return
 
-    pymodbus_apply_logging_config()  # pylint: disable=unreachable
+    pymodbus_apply_logging_config()
     cmd_args = Commandline
     cmd_args.comm = test_comm
     cmd_args.framer = test_framer
     cmd_args.port = test_port + test_port_offset + 1
     cmd_args.client_port = test_port + test_port_offset
     run_args = setup_forwarder(cmd_args)
-    task = asyncio.create_task(run_forwarder(run_args))
+    # task = asyncio.create_task(run_forwarder(run_args))
+    task = asyncio.create_task(run_async_server(run_args))
     await asyncio.sleep(0.1)
 
     real_client = AsyncModbusTcpClient(host="127.0.0.1", port=cmd_args.port)
@@ -191,6 +193,16 @@ async def test_exp_forwarder(  # pylint: disable=unused-argument
     assert check_client.connected
     await asyncio.sleep(0.1)
 
+    # rr = _check_call(client.read_holding_registers(1, 1, slave=SLAVE))
+    # rr = _check_call(client.read_coils(1, 1, slave=SLAVE))
+    # rr = _check_call(client.read_discrete_inputs(0, 8, slave=SLAVE))
+    # rr = _check_call(client.read_input_registers(1, 8, slave=SLAVE))
+    # --
+    # rr = _check_call(client.write_register(1, 10, slave=SLAVE))
+    # rr = _check_call(client.write_coil(0, True, slave=SLAVE))
+    # rr =_check_call(client.write_registers(1, [10] * 8, slave=SLAVE))
+    # rr = _check_call(client.write_coils(1, [True] * 21, slave=SLAVE))
+
     # Verify read values are identical
     # rr_real = await real_client.read_holding_registers(1,1,slave=1)
     # assert rr_real.registers, f"---> {rr_real}"