Skip to content

Commit 968564c

Browse files
authored
Bit handling LSB -> MSB across bytes. (#2634)
1 parent 4fc8ff6 commit 968564c

File tree

5 files changed

+64
-48
lines changed

5 files changed

+64
-48
lines changed

API_changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ API changes 4.0.0
88
- payload removed (replaced by "convert_to/from_registers")
99
- slave=, slaves= replaced by device_id=, device_ids=
1010
- slave request names changed to device
11+
- bit handling order is LSB (last byte) -> MSB (first byte)
1112

1213
-----------------
1314
API changes 3.9.0

doc/source/upgrade_40.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ Pymodbus 4.0 upgrade procedure
44
Pymodbus 4.0 contains a number of incompatibilities with Pymodbus 3.x, however
55
most of these are simple edits.
66

7+
Bit handling
8+
------------
9+
readCoils and other bit functions now return bit in logical order (NOT byte order)
10+
11+
Older versions had LSB -> MSB pr byte
12+
New version have LSB -> MSB across bytes.
13+
14+
Example:
15+
Hex bytes: 0x00 0x01
16+
v3.x would deliver False * 8 True False * 7
17+
V4.x deliver True False * 15
18+
19+
Hex bytes: 0x01 0x03
20+
v3.x would deliver True False * 7 True True False * 6
21+
V4.x deliver True True False * 6 True False * 7
22+
23+
724
Python 3.9
825
----------
926
Python 3.9 is reaching end of life and from october 2025 no longer receives security updates.

pymodbus/pdu/pdu.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,14 @@ def pack_bitstring(bits: list[bool], align_byte=True) -> bytes:
164164
bits_extra = 8 if align_byte else 16
165165
if (extra := len(bits) % bits_extra):
166166
t_bits += [False] * (bits_extra - extra)
167-
for byte_inx in range(0, len(t_bits), 8):
168-
for bit in reversed(t_bits[byte_inx:byte_inx+8]):
169-
packed <<= 1
170-
if bit:
171-
packed += 1
172-
i += 1
173-
if i == 8:
174-
ret += struct.pack(">B", packed)
175-
i = packed = 0
167+
for bit in reversed(t_bits):
168+
packed <<= 1
169+
if bit:
170+
packed += 1
171+
i += 1
172+
if i == 8:
173+
ret += struct.pack(">B", packed)
174+
i = packed = 0
176175
return ret
177176

178177

@@ -184,11 +183,13 @@ def unpack_bitstring(data: bytes) -> list[bool]:
184183
bytes 0x05 0x81
185184
result = unpack_bitstring(bytes)
186185
187-
[True, False, True, False] + [False, False, False, False]
188-
[True, False, False, False] + [False, False, False, True]
186+
[True, False, False, False] +
187+
[False, False, False, True] +
188+
[True, False, True, False] +
189+
[False, False, False, False]
189190
"""
190191
res = []
191-
for _, t_byte in enumerate(data):
192+
for byte_index in range(len(data) -1, -1, -1):
192193
for bit in (1, 2, 4, 8, 16, 32, 64, 128):
193-
res.append(bool(t_byte & bit))
194+
res.append(bool(data[byte_index] & bit))
194195
return res

test/client/test_client.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,49 +139,49 @@ def fake_execute(_self, _no_response_expected, request):
139139
(
140140
ModbusClientMixin.DATATYPE.BITS,
141141
[True] + [False] * 15,
142-
[256], # 0x01 0x00
142+
[1], # 0x00 0x01
143143
None,
144144
),
145145
(
146146
ModbusClientMixin.DATATYPE.BITS,
147147
[False] * 8 + [True] + [False] * 7,
148-
[1], # 0x00 0x01
148+
[256], # 0x01 0x00
149149
None,
150150
),
151151
(
152152
ModbusClientMixin.DATATYPE.BITS,
153153
[False] * 15 + [True],
154-
[128], # 0x00 0x80
154+
[32768], # 0x80 0x00
155155
None,
156156
),
157157
(
158158
ModbusClientMixin.DATATYPE.BITS,
159159
[True] + [False] * 14 + [True],
160-
[384], # 0x01 0x80
160+
[32769], # 0x80 0x01
161161
None,
162162
),
163163
(
164164
ModbusClientMixin.DATATYPE.BITS,
165165
[False] * 8 + [True, False, True] + [False] * 5,
166-
[5], # 0x00 0x05
166+
[1280], # 0x05 0x00
167167
None,
168168
),
169169
(
170170
ModbusClientMixin.DATATYPE.BITS,
171171
[True] + [False] * 7 + [True, False, True] + [False] * 5,
172-
[261], # 0x01 0x05
172+
[1281], # 0x05 0x01
173173
None,
174174
),
175175
(
176176
ModbusClientMixin.DATATYPE.BITS,
177177
[True] + [False] * 6 + [True, True, False, True] + [False] * 5,
178-
[33029], # 0x81 0x05
178+
[1409], # 0x05 0x81
179179
None,
180180
),
181181
(
182182
ModbusClientMixin.DATATYPE.BITS,
183183
[False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5,
184-
[1, 33029], # 92340480 = 0x00 0x01 0x81 0x05
184+
[1409, 256], # 92340480 = 0x05 0x81 0x01 0x00
185185
None,
186186
),
187187
],

test/pdu/test_pdu.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -255,21 +255,21 @@ async def test_pdu_default_datastore(self, mock_context):
255255
@pytest.mark.parametrize(
256256
("bytestream", "bitlist"),
257257
[
258-
(b"\x01\x00", [True] + [False] * 15),
259-
(b"\x00\x80", [False] * 15 + [True]),
260-
(b"\x00\x01", [False] * 8 + [True] + [False] * 7),
261-
(b"\x01\x80", [True] + [False] * 14 + [True]),
262-
(b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5),
263-
(b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5),
264-
(b"\x81\x05", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
265-
(b"\x00\x01\x81\x05", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
258+
(b"\x00\x01", [True] + [False] * 15),
259+
(b"\x01\x00", [False] * 8 + [True] + [False] * 7),
260+
(b"\x80\x00", [False] * 15 + [True]),
261+
(b"\x80\x01", [True] + [False] * 14 + [True]),
262+
(b"\x05\x00", [False] * 8 + [True, False, True] + [False] * 5),
263+
(b"\x05\x01", [True] + [False] * 7 + [True, False, True] + [False] * 5),
264+
(b"\x05\x81", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
265+
(b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
266266
267-
(b"\x01\x00", [True]),
268-
(b"\x00\x01", [False] * 8 + [True]),
269-
(b"\x00\x05", [False] * 8 + [True, False, True]),
270-
(b"\x01\x05", [True] + [False] * 7 + [True, False, True]),
271-
(b"\x81\x05", [True] + [False] * 6 + [True, True, False, True]),
272-
(b"\x00\x01\x81\x05", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True]),
267+
(b"\x00\x01", [True]),
268+
(b"\x01\x00", [False] * 8 + [True]),
269+
(b"\x05\x00", [False] * 8 + [True, False, True]),
270+
(b"\x05\x01", [True] + [False] * 7 + [True, False, True]),
271+
(b"\x05\x81", [True] + [False] * 6 + [True, True, False, True]),
272+
(b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True]),
273273
],
274274
)
275275
def test_bit_packing(self, bytestream, bitlist):
@@ -280,9 +280,9 @@ def test_bit_packing(self, bytestream, bitlist):
280280
("bytestream", "bitlist"),
281281
[
282282
(b"\x01", [True]),
283-
(b"\x00\x01", [False] * 8 + [True]),
283+
(b"\x01\x00", [False] * 8 + [True]),
284284
(b"\x05", [True, False, True]),
285-
(b"\x01\x05", [True] + [False] * 7 + [True, False, True]),
285+
(b"\x05\x01", [True] + [False] * 7 + [True, False, True]),
286286
],
287287
)
288288
def test_bit_packing8(self, bytestream, bitlist):
@@ -293,17 +293,14 @@ def test_bit_packing8(self, bytestream, bitlist):
293293
("bytestream", "bitlist"),
294294
[
295295
(b"\x01", [True] + [False] * 7),
296-
(b"\x01\x00", [True] + [False] * 15),
297-
(b"\x00\x01", [False] * 8 + [True] + [False] * 7),
298-
(b"\x00\x80", [False] * 15 + [True]),
299-
(b"\x01\x80", [True] + [False] * 14 + [True]),
300-
(b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5),
301-
(b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5),
302-
(b"\x81\x05", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
303-
(b"\x05\x81\x01\x00", [True, False, True] + [False] * 5 +
304-
[True] + [False] * 6 + [True] +
305-
[True] + [False] * 7 +
306-
[False] * 8),
296+
(b"\x00\x01", [True] + [False] * 15),
297+
(b"\x01\x00", [False] * 8 + [True] + [False] * 7),
298+
(b"\x80\x00", [False] * 15 + [True]),
299+
(b"\x80\x01", [True] + [False] * 14 + [True]),
300+
(b"\x05\x00", [False] * 8 + [True, False, True] + [False] * 5),
301+
(b"\x05\x01", [True] + [False] * 7 + [True, False, True] + [False] * 5),
302+
(b"\x05\x81", [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
303+
(b"\x05\x81\x01\x00", [False] * 8 + [True] + [False] * 7 + [True] + [False] * 6 + [True, True, False, True] + [False] * 5),
307304
],
308305
)
309306
def test_bit_unpacking(self, bytestream, bitlist):

0 commit comments

Comments
 (0)