Repository: pymodbus-dev/pymodbus

Issue:
ModbusSimulatorContext String Type Fails to Encode
### Versions

* Python: 3.10.6
* OS: Linux
* Pymodbus: 3.0.2
* Modbus Hardware (if used): None

### Pymodbus Specific
* Server: tcp - async
* Client: None

### Description

I was using ModbusSimulatorContext with the settings as provided by the pymodbus documentation in this [link](https://pymodbus.readthedocs.io/en/latest/source/library/pymodbus.datastore.html#pymodbus.datastore.ModbusSimulatorContext).

All the registers were working just fine when client requested them, but the string type registers did not return anything.
After some debugging I realized that 'handle_type_string' within the ModbusSimulatorContext is setting each register with a string of 2 letters (living it as a string type).

When the 'encode' function in 'ReadRegistersResponseBase' tries to build to bytes from the registers made in ModbusSimulatorContext, it's using `struct.pack(">H", register)` over the register while the register is a string for example 'St' and it throws an exception 'required argument is not an integer'.

I managed to solve it by changing the code in ModbusSimulatorContext, see below.


### Code and Logs

Current ModbusSimulatorContext.handle_type_string function:
```python
def handle_type_string(self, registers, value, action, _entry):
    """Handle type string."""
    j = 0
    for i in range(registers[0], registers[1] + 1):
        self.registers[i].type = CELL_TYPE_STRING_NEXT
        self.registers[i].value = value[j : j + 2]
        j += 2
    self.registers[registers[0]].type = CELL_TYPE_STRING
    self.registers[registers[0]].action = action
```

Current ReadRegistersResponseBase.encode function:
```python
def encode(self):
    """Encode the response packet.

    :returns: The encoded packet
    """
    result = struct.pack(">B", len(self.registers) * 2)
    for register in self.registers:
        result += struct.pack(">H", register)
    return result
```

Suggested fix for ModbusSimulatorContext.handle_type_string function:
```python
def handle_type_string(self, registers, value, action, _entry):
    """Handle type string."""
    j = 0
    for i in range(registers[0], registers[1] + 1):
        self.registers[i].type = CELL_TYPE_STRING_NEXT
        self.registers[i].value = int.from_bytes(
            bytes(value[j : j + 2], "UTF-8"), "big"
        )
        j += 2
    self.registers[registers[0]].type = CELL_TYPE_STRING
    self.registers[registers[0]].action = action

```


Discussion:

Resolving patch:
diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py
--- a/pymodbus/datastore/simulator.py
+++ b/pymodbus/datastore/simulator.py
@@ -66,6 +66,7 @@ class Label:  # pylint: disable=too-many-instance-attributes
     type_uint32: str = "uint32"
     type_float32: str = "float32"
     type_string: str = "string"
+    uptime: str = "uptime"
     value: str = "value"
     write: str = "write"
 
@@ -123,6 +124,7 @@ def __init__(self):
                 Label.method: self.handle_type_string,
             },
         }
+        self.endianness = sys.byteorder
 
     def handle_type_bits(self, registers, reg_count, start, stop, value, action):
         """Handle type bits.
@@ -226,9 +228,11 @@ def handle_type_string(self, registers, reg_count, start, stop, value, action):
             if registers[inx].type != CELL_TYPE_NONE:
                 txt = f'ERROR Configuration invalid in section "{Label.type_string}" register {inx} already defined'
                 raise RuntimeError(txt)
-            registers[inx].value = value[i * 2 : (i + 1) * 2]
+            registers[inx].value = int.from_bytes(
+                bytes(value[i * 2 : (i + 1) * 2], "UTF-8"), "big"
+            )
             registers[inx].type = CELL_TYPE_STRING
-            registers[inx].action = action
+            registers[start].action = action
 
     def handle_setup_section(self, config, actions):
         """Load setup section"""
@@ -462,6 +466,7 @@ class ModbusSimulatorContext:
     # --------------------------------------------
     # External interfaces
     # --------------------------------------------
+    start_time = int(datetime.now().timestamp())
 
     def __init__(self, config: Dict[str, any], actions: Dict[str, Callable]) -> None:
         """Initialize."""
@@ -470,6 +475,7 @@ def __init__(self, config: Dict[str, any], actions: Dict[str, Callable]) -> None
             Label.random: self.action_random,
             Label.reset: self.action_reset,
             Label.timestamp: self.action_timestamp,
+            Label.uptime: self.action_uptime,
         }
         res = Setup().setup(config, builtin_actions, actions)
         self.registers = res[0]
@@ -617,13 +623,13 @@ def action_timestamp(cls, registers, inx, _cell):
         :meta private:
         """
         system_time = datetime.now()
-        registers[inx].value = system_time.tm_year - 1900
-        registers[inx + 1].value = system_time.tm_mon
-        registers[inx + 2].value = system_time.tm_mday
-        registers[inx + 3].value = system_time.tm_wday
-        registers[inx + 4].value = system_time.tm_hour
-        registers[inx + 5].value = system_time.tm_min
-        registers[inx + 1].value = system_time.tm_sec
+        registers[inx].value = system_time.year
+        registers[inx + 1].value = system_time.month - 1
+        registers[inx + 2].value = system_time.day
+        registers[inx + 3].value = system_time.weekday() + 1
+        registers[inx + 4].value = system_time.hour
+        registers[inx + 5].value = system_time.minute
+        registers[inx + 6].value = system_time.second
 
     @classmethod
     def action_reset(cls, _registers, _inx, _cell):
@@ -633,6 +639,28 @@ def action_reset(cls, _registers, _inx, _cell):
         """
         raise RuntimeError("RESET server")
 
+    @classmethod
+    def action_uptime(cls, registers, inx, cell):
+        """Return uptime in seconds.
+
+        :meta private:
+        """
+        value = int(datetime.now().timestamp()) - cls.start_time
+        value = 123456789
+
+        if cell.type == CELL_TYPE_BIT:
+            registers[inx].value = 0
+        elif cell.type == CELL_TYPE_FLOAT32:
+            regs = cls.build_registers_from_value(value, False)
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+        elif cell.type == CELL_TYPE_UINT16:
+            registers[inx].value = value
+        elif cell.type == CELL_TYPE_UINT32:
+            regs = cls.build_registers_from_value(value, True)
+            registers[inx].value = regs[0]
+            registers[inx + 1].value = regs[1]
+
     # --------------------------------------------
     # Internal helper methods
     # --------------------------------------------
@@ -667,21 +695,21 @@ def build_registers_from_value(cls, value, is_int):
         """Build registers from int32 or float32"""
         regs = [0, 0]
         if is_int:
-            value_bytes = int.to_bytes(value, 4, sys.byteorder)
+            value_bytes = int.to_bytes(value, 4, "big")
         else:
             value_bytes = struct.pack("f", value)
-        regs[0] = int.from_bytes(value_bytes[:2], sys.byteorder)
-        regs[1] = int.from_bytes(value_bytes[-2:], sys.byteorder)
+        regs[0] = int.from_bytes(value_bytes[:2], "big")
+        regs[1] = int.from_bytes(value_bytes[-2:], "big")
         return regs
 
     @classmethod
     def build_value_from_registers(cls, registers, is_int):
         """Build registers from int32 or float32"""
-        value_bytes = int.to_bytes(registers[0], 2, sys.byteorder) + int.to_bytes(
-            registers[1], 2, sys.byteorder
+        value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes(
+            registers[1], 2, "big"
         )
         if is_int:
-            value = int.from_bytes(value_bytes, sys.byteorder)
+            value = int.from_bytes(value_bytes, "big")
         else:
             value = struct.unpack("f", value_bytes)[0]
         return value


Test patch:
diff --git a/test/test_simulator.py b/test/test_simulator.py
--- a/test/test_simulator.py
+++ b/test/test_simulator.py
@@ -119,30 +119,38 @@ class TestSimulator:
         Cell(CELL_TYPE_UINT16, True, 5678, None),
         Cell(CELL_TYPE_UINT16, False, 14661, True),
         Cell(CELL_TYPE_UINT16, False, 14661, True),  # 20
-        Cell(CELL_TYPE_UINT32, True, 3124, None),
-        Cell(CELL_TYPE_NEXT, True, 0, None),
-        Cell(CELL_TYPE_UINT32, True, 5678, None),
-        Cell(CELL_TYPE_NEXT, True, 0, None),
-        Cell(CELL_TYPE_UINT32, True, 5678, None),
-        Cell(CELL_TYPE_NEXT, True, 0, None),
-        Cell(CELL_TYPE_UINT32, False, 17320, True),
-        Cell(CELL_TYPE_NEXT, False, 5, None),
-        Cell(CELL_TYPE_UINT32, False, 17320, True),
-        Cell(CELL_TYPE_NEXT, False, 5, None),  # 30
-        Cell(CELL_TYPE_FLOAT32, True, 17080, None),
-        Cell(CELL_TYPE_NEXT, True, 17731, None),
-        Cell(CELL_TYPE_FLOAT32, True, 29061, None),
-        Cell(CELL_TYPE_NEXT, True, 17841, None),
-        Cell(CELL_TYPE_FLOAT32, True, 29061, None),
-        Cell(CELL_TYPE_NEXT, True, 17841, None),
-        Cell(CELL_TYPE_FLOAT32, False, 29958, True),
-        Cell(CELL_TYPE_NEXT, False, 18600, None),
-        Cell(CELL_TYPE_FLOAT32, False, 29958, True),
-        Cell(CELL_TYPE_NEXT, False, 18600, None),
-        Cell(CELL_TYPE_STRING, False, "St", None),
-        Cell(CELL_TYPE_STRING, False, "r ", None),
-        Cell(CELL_TYPE_STRING, False, "St", None),
-        Cell(CELL_TYPE_STRING, False, "rx", None),  # 29 MAX before repeat
+        Cell(CELL_TYPE_UINT32, True, 0, None),
+        Cell(CELL_TYPE_NEXT, True, 3124, None),
+        Cell(CELL_TYPE_UINT32, True, 0, None),
+        Cell(CELL_TYPE_NEXT, True, 5678, None),
+        Cell(CELL_TYPE_UINT32, True, 0, None),
+        Cell(CELL_TYPE_NEXT, True, 5678, None),
+        Cell(CELL_TYPE_UINT32, False, 5, True),
+        Cell(CELL_TYPE_NEXT, False, 17320, None),
+        Cell(CELL_TYPE_UINT32, False, 5, True),
+        Cell(CELL_TYPE_NEXT, False, 17320, None),  # 30
+        Cell(CELL_TYPE_FLOAT32, True, 47170, None),
+        Cell(CELL_TYPE_NEXT, True, 17221, None),
+        Cell(CELL_TYPE_FLOAT32, True, 34161, None),
+        Cell(CELL_TYPE_NEXT, True, 45381, None),
+        Cell(CELL_TYPE_FLOAT32, True, 34161, None),
+        Cell(CELL_TYPE_NEXT, True, 45381, None),
+        Cell(CELL_TYPE_FLOAT32, False, 1653, True),
+        Cell(CELL_TYPE_NEXT, False, 43080, None),
+        Cell(CELL_TYPE_FLOAT32, False, 1653, True),
+        Cell(CELL_TYPE_NEXT, False, 43080, None),
+        Cell(
+            CELL_TYPE_STRING, False, int.from_bytes(bytes("St", "utf-8"), "big"), None
+        ),
+        Cell(
+            CELL_TYPE_STRING, False, int.from_bytes(bytes("r ", "utf-8"), "big"), None
+        ),
+        Cell(
+            CELL_TYPE_STRING, False, int.from_bytes(bytes("St", "utf-8"), "big"), None
+        ),
+        Cell(
+            CELL_TYPE_STRING, False, int.from_bytes(bytes("rx", "utf-8"), "big"), None
+        ),  # 29 MAX before repeat
     ]
 
     @classmethod