Repository: pymodbus-dev/pymodbus (Python)

Issue:
read_coils() report wrong bit order in 3.9.0
My modbus device is reporting the following coils starting at address 0x100: true, false, true, false, true, true, ... .

pymodbus 3.8.6 returned the bits in the correct order:

```
>>> client.read_coils(0x100, count=16).bits
[True, False, True, False, True, True, True, True, True, True, True, True, True, True, True, True]
```

pymodbus 3.9.0 returns the values byte-wise swapped:
```
>>> client.read_coils(0x100, count=16).bits
[True, True, True, True, True, True, True, True, True, False, True, False, True, True, True, True]
```
It seems, that pymodbus 3.9.0 misinterprets the on-wire protocol.

According to the Modbus Application Protocol 1.1b document, section 6.1, the data bytes should be interpreted one by one starting with the first byte and the bits should be interpreted starting at the LSB for each byte.

In my case, the device returns 2 data bytes: 0xf5, 0xff and so the bits should be interpreted as follows:
- starting with the first byte (0xf5), interpreting the bits starting at LSB: true, false, true, false, true, true, true, true
- then the 2nd byte (0xff), starting at LSB: true, true, ...

The modbus dissector in wireshark confirms it:

![Image](https://github.com/user-attachments/assets/2c344529-5c06-49f2-9750-3eac6d1b6b66)


It looks like that commit d47dc65a7995a19e9d004318ff7b050d8f82f2a5 has broken the bit order in 3.9.0.


Discussion:
hmm, could be a bug, I will take a look later
bug confirmed, this will be released as v3.9.1

Actually it is a feature for v4.0 where bits will be reported from LSB to MSB across bytes.



Patch:
diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py
index dd2eb3eee..9f9a7df56 100644
--- a/pymodbus/pdu/pdu.py
+++ b/pymodbus/pdu/pdu.py
@@ -164,14 +164,15 @@ def pack_bitstring(bits: list[bool], align_byte=True) -> bytes:
     bits_extra = 8 if align_byte else 16
     if (extra := len(bits) % bits_extra):
         t_bits += [False] * (bits_extra - extra)
-    for bit in reversed(t_bits):
-        packed <<= 1
-        if bit:
-            packed += 1
-        i += 1
-        if i == 8:
-            ret += struct.pack(">B", packed)
-            i = packed = 0
+    for byte_inx in range(0, len(t_bits), 8):
+        for bit in reversed(t_bits[byte_inx:byte_inx+8]):
+            packed <<= 1
+            if bit:
+                packed += 1
+            i += 1
+            if i == 8:
+                ret += struct.pack(">B", packed)
+                i = packed = 0
     return ret
 
 
@@ -183,13 +184,11 @@ def unpack_bitstring(data: bytes) -> list[bool]:
         bytes 0x05 0x81
         result = unpack_bitstring(bytes)
 
-        [True, False, False, False] +
-        [False, False, False, True] +
-        [True, False, True, False] +
-        [False, False, False, False]
+        [True, False, True, False] + [False, False, False, False]
+        [True, False, False, False] + [False, False, False, True]
     """
     res = []
-    for byte_index in range(len(data) -1, -1, -1):
+    for _, t_byte in enumerate(data):
         for bit in (1, 2, 4, 8, 16, 32, 64, 128):
-            res.append(bool(data[byte_index] & bit))
+            res.append(bool(t_byte & bit))
     return res


Test patch:
diff --git a/test/client/test_client.py b/test/client/test_client.py
index 8c99a684c..f5cb953cb 100755
--- a/test/client/test_client.py
+++ b/test/client/test_client.py
@@ -139,49 +139,49 @@ def fake_execute(_self, _no_response_expected, request):
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [True] + [False] * 15,
-                [1],  # 0x00 0x01
+                [256],  # 0x01 0x00
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [False] * 8 + [True] + [False] * 7,
-                [256],  # 0x01 0x00
+                [1],  # 0x00 0x01
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [False] * 15 + [True],
-                [32768],  # 0x80 0x00
+                [128],  # 0x00 0x80
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [True] + [False] * 14 + [True],
-                [32769],  # 0x80 0x01
+                [384],  # 0x01 0x80
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [False] * 8 + [True, False, True] + [False] * 5,
-                [1280],  # 0x05 0x00
+                [5],  # 0x00 0x05
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [True] + [False] * 7 + [True, False, True] + [False] * 5,
-                [1281],  # 0x05 0x01
+                [261],  # 0x01 0x05
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [True] + [False] * 6 + [True, True, False, True] + [False] * 5,
-                [1409], # 0x05 0x81
+                [33029], # 0x81 0x05
                 None,
             ),
             (
                 ModbusClientMixin.DATATYPE.BITS,
                 [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5,
-                [1409, 256],  # 92340480 = 0x05 0x81 0x01 0x00
+                [1, 33029],  # 92340480 = 0x00 0x01 0x81 0x05
                 None,
             ),
         ],
diff --git a/test/pdu/test_pdu.py b/test/pdu/test_pdu.py
index 243fd9300..e67c8ea61 100644
--- a/test/pdu/test_pdu.py
+++ b/test/pdu/test_pdu.py
@@ -255,21 +255,21 @@ async def test_pdu_default_datastore(self, mock_context):
     @pytest.mark.parametrize(
         ("bytestream", "bitlist"),
         [
-            (b"\x00\x01", [True] + [False] * 15),
-            (b"\x01\x00", [False] * 8 + [True] + [False] * 7),
-            (b"\x80\x00", [False] * 15 + [True]),
-            (b"\x80\x01", [True] + [False] * 14 + [True]),
-            (b"\x05\x00", [False] * 8 + [True, False, True] + [False] * 5),
-            (b"\x05\x01", [True] + [False] * 7 + [True, False, True] + [False] * 5),
-            (b"\x05\x81", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
-            (b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
+            (b"\x01\x00", [True] + [False] * 15),
+            (b"\x00\x80", [False] * 15 + [True]),
+            (b"\x00\x01", [False] * 8 + [True] + [False] * 7),
+            (b"\x01\x80", [True] + [False] * 14 + [True]),
+            (b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5),
+            (b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5),
+            (b"\x81\x05", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
+            (b"\x00\x01\x81\x05", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
 
-            (b"\x00\x01", [True]),
-            (b"\x01\x00", [False] * 8 + [True]),
-            (b"\x05\x00", [False] * 8 + [True, False, True]),
-            (b"\x05\x01", [True] + [False] * 7 + [True, False, True]),
-            (b"\x05\x81", [True] + [False] * 6 + [True, True, False, True]),
-            (b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True]),
+            (b"\x01\x00", [True]),
+            (b"\x00\x01", [False] * 8 + [True]),
+            (b"\x00\x05", [False] * 8 + [True, False, True]),
+            (b"\x01\x05", [True] + [False] * 7 + [True, False, True]),
+            (b"\x81\x05", [True] + [False] * 6 + [True, True, False, True]),
+            (b"\x00\x01\x81\x05", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True]),
         ],
     )
     def test_bit_packing(self, bytestream, bitlist):
@@ -280,9 +280,9 @@ def test_bit_packing(self, bytestream, bitlist):
         ("bytestream", "bitlist"),
         [
             (b"\x01", [True]),
-            (b"\x01\x00", [False] * 8 + [True]),
+            (b"\x00\x01", [False] * 8 + [True]),
             (b"\x05", [True, False, True]),
-            (b"\x05\x01", [True] + [False] * 7 + [True, False, True]),
+            (b"\x01\x05", [True] + [False] * 7 + [True, False, True]),
         ],
     )
     def test_bit_packing8(self, bytestream, bitlist):
@@ -293,14 +293,17 @@ def test_bit_packing8(self, bytestream, bitlist):
         ("bytestream", "bitlist"),
         [
             (b"\x01", [True] + [False] * 7),
-            (b"\x00\x01", [True] + [False] * 15),
-            (b"\x01\x00", [False] * 8 + [True] + [False] * 7),
-            (b"\x80\x00", [False] * 15 + [True]),
-            (b"\x80\x01", [True] + [False] * 14 + [True]),
-            (b"\x05\x00", [False] * 8 + [True, False, True] + [False] * 5),
-            (b"\x05\x01", [True] + [False] * 7 + [True, False, True] + [False] * 5),
-            (b"\x05\x81", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
-            (b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
+            (b"\x01\x00", [True] + [False] * 15),
+            (b"\x00\x01", [False] * 8 + [True] + [False] * 7),
+            (b"\x00\x80", [False] * 15 + [True]),
+            (b"\x01\x80", [True] + [False] * 14 + [True]),
+            (b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5),
+            (b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5),
+            (b"\x81\x05", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
+            (b"\x05\x81\x01\x00", [True, False, True] + [False] * 5 +
+                                  [True] + [False] * 6 + [True] +
+                                  [True] + [False] * 7 +
+                                  [False] * 8),
         ],
     )
     def test_bit_unpacking(self, bytestream, bitlist):