Skip to content

Commit f93b722

Browse files
daanwtbjaniversen
andauthored
Convert endianness (#2506)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
1 parent abdbeca commit f93b722

File tree

4 files changed

+72
-33
lines changed

4 files changed

+72
-33
lines changed

examples/client_async_calls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ async def async_handle_holding_registers(client):
146146
assert not rr.isError() # test that call was OK
147147
assert rr.registers == [10]
148148

149+
value_int32 = 13211
150+
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
151+
await client.write_registers(1, registers, slave=SLAVE)
152+
rr = await client.read_holding_registers(1, count=len(registers), slave=SLAVE)
153+
assert not rr.isError() # test that call was OK
154+
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
155+
assert value_int32 == value
156+
149157
_logger.info("### write read holding registers")
150158
arguments = {
151159
"read_address": 1,

examples/client_calls.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,13 @@ def handle_holding_registers(client):
135135
assert not rr.isError() # test that call was OK
136136
assert rr.registers[0] == 10
137137

138-
client.write_registers(1, [10] * 8, slave=SLAVE)
139-
rr = client.read_holding_registers(1, count=8, slave=SLAVE)
138+
value_int32 = 13211
139+
registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32)
140+
client.write_registers(1, registers, slave=SLAVE)
141+
rr = client.read_holding_registers(1, count=len(registers), slave=SLAVE)
140142
assert not rr.isError() # test that call was OK
141-
assert rr.registers == [10] * 8
143+
value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32)
144+
assert value_int32 == value
142145

143146
_logger.info("### write read holding registers")
144147
arguments = {

pymodbus/client/mixin.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import struct
55
from abc import abstractmethod
66
from enum import Enum
7-
from typing import Generic, TypeVar
7+
from typing import Generic, Literal, TypeVar, cast
88

99
import pymodbus.pdu.bit_message as pdu_bit
1010
import pymodbus.pdu.diag_message as pdu_diag
@@ -691,65 +691,83 @@ class DATATYPE(Enum):
691691
FLOAT32 = ("f", 2)
692692
FLOAT64 = ("d", 4)
693693
STRING = ("s", 0)
694-
BITS = "bits"
694+
BITS = ("bits", 0)
695695

696696
@classmethod
697697
def convert_from_registers(
698-
cls, registers: list[int], data_type: DATATYPE
699-
) -> int | float | str | list[bool]:
698+
cls, registers: list[int], data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
699+
) -> int | float | str | list[bool] | list[int] | list[float]:
700700
"""Convert registers to int/float/str.
701701
702702
:param registers: list of registers received from e.g. read_holding_registers()
703703
:param data_type: data type to convert to
704-
:returns: int, float, str or list[bool] depending on "data_type"
705-
:raises ModbusException: when size of registers is not 1, 2 or 4
706-
"""
707-
byte_list = bytearray()
708-
for x in registers:
709-
byte_list.extend(int.to_bytes(x, 2, "big"))
710-
if data_type == cls.DATATYPE.STRING:
711-
# remove trailing null bytes
712-
trailing_nulls_begin = len(byte_list)
713-
while trailing_nulls_begin > 0 and not byte_list[trailing_nulls_begin - 1]:
714-
trailing_nulls_begin -= 1
715-
byte_list = byte_list[:trailing_nulls_begin]
716-
717-
return byte_list.decode("utf-8")
718-
if data_type == cls.DATATYPE.BITS:
704+
:param word_order: "big"/"little" order of words/registers
705+
:returns: scalar or array of "data_type"
706+
:raises ModbusException: when size of registers is not a multiple of data_type
707+
"""
708+
if not (data_len := data_type.value[1]):
709+
byte_list = bytearray()
710+
if word_order == "little":
711+
registers.reverse()
712+
for x in registers:
713+
byte_list.extend(int.to_bytes(x, 2, "big"))
714+
if data_type == cls.DATATYPE.STRING:
715+
trailing_nulls_begin = len(byte_list)
716+
while trailing_nulls_begin > 0 and not byte_list[trailing_nulls_begin - 1]:
717+
trailing_nulls_begin -= 1
718+
byte_list = byte_list[:trailing_nulls_begin]
719+
return byte_list.decode("utf-8")
719720
return unpack_bitstring(byte_list)
720-
if len(registers) != data_type.value[1]:
721+
if (reg_len := len(registers)) % data_len:
721722
raise ModbusException(
722-
f"Illegal size ({len(registers)}) of register array, cannot convert!"
723+
f"Registers illegal size ({len(registers)}) expected multiple of {data_len}!"
723724
)
724-
return struct.unpack(f">{data_type.value[0]}", byte_list)[0]
725+
726+
result = []
727+
for i in range(0, reg_len, data_len):
728+
regs = registers[i:i+data_len]
729+
if word_order == "little":
730+
regs.reverse()
731+
byte_list = bytearray()
732+
for x in regs:
733+
byte_list.extend(int.to_bytes(x, 2, "big"))
734+
result.append(struct.unpack(f">{data_type.value[0]}", byte_list)[0])
735+
return result if len(result) != 1 else result[0]
725736

726737
@classmethod
727738
def convert_to_registers(
728-
cls, value: int | float | str | list[bool], data_type: DATATYPE
739+
cls, value: int | float | str | list[bool] | list[int] | list[float] , data_type: DATATYPE, word_order: Literal["big", "little"] = "big"
729740
) -> list[int]:
730741
"""Convert int/float/str to registers (16/32/64 bit).
731742
732743
:param value: value to be converted
733-
:param data_type: data type to be encoded as registers
744+
:param data_type: data type to convert from
745+
:param word_order: "big"/"little" order of words/registers
734746
:returns: List of registers, can be used directly in e.g. write_registers()
735747
:raises TypeError: when there is a mismatch between data_type and value
736748
"""
737749
if data_type == cls.DATATYPE.BITS:
738750
if not isinstance(value, list):
739-
raise TypeError(f"Value should be string but is {type(value)}.")
751+
raise TypeError(f"Value should be list of bool but is {type(value)}.")
740752
if (missing := len(value) % 16):
741753
value = value + [False] * (16 - missing)
742-
byte_list = pack_bitstring(value)
754+
byte_list = pack_bitstring(cast(list[bool], value))
743755
elif data_type == cls.DATATYPE.STRING:
744756
if not isinstance(value, str):
745757
raise TypeError(f"Value should be string but is {type(value)}.")
746758
byte_list = value.encode()
747759
if len(byte_list) % 2:
748760
byte_list += b"\x00"
749761
else:
750-
byte_list = struct.pack(f">{data_type.value[0]}", value)
762+
if not isinstance(value, list):
763+
value = cast(list[int], [value])
764+
byte_list = bytearray()
765+
for v in value:
766+
byte_list.extend(struct.pack(f">{data_type.value[0]}", v))
751767
regs = [
752768
int.from_bytes(byte_list[x : x + 2], "big")
753769
for x in range(0, len(byte_list), 2)
754770
]
771+
if word_order == "little":
772+
regs.reverse()
755773
return regs

test/client/test_client.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def fake_execute(_self, _no_response_expected, request):
9595
getattr(ModbusClientMixin(), method)(**arglist[arg])
9696
assert isinstance(pdu_to_call, pdu_request)
9797

98+
@pytest.mark.parametrize(("word_order"), ["big", "little", None])
9899
@pytest.mark.parametrize(
99100
("datatype", "value", "registers"),
100101
[
@@ -156,11 +157,16 @@ def fake_execute(_self, _no_response_expected, request):
156157
),
157158
],
158159
)
159-
def test_client_mixin_convert(self, datatype, registers, value):
160+
def test_client_mixin_convert(self, datatype, word_order, registers, value):
160161
"""Test converter methods."""
161-
regs = ModbusClientMixin.convert_to_registers(value, datatype)
162+
if word_order == "little":
163+
x = registers.copy()
164+
x.reverse()
165+
registers = x
166+
kwargs = {"word_order": word_order} if word_order else {}
167+
regs = ModbusClientMixin.convert_to_registers(value, datatype, **kwargs)
162168
assert regs == registers
163-
result = ModbusClientMixin.convert_from_registers(registers, datatype)
169+
result = ModbusClientMixin.convert_from_registers(registers, datatype, **kwargs)
164170
if datatype == ModbusClientMixin.DATATYPE.FLOAT32:
165171
result = round(result, 6)
166172
if datatype == ModbusClientMixin.DATATYPE.BITS:
@@ -174,6 +180,7 @@ def test_client_mixin_convert(self, datatype, registers, value):
174180
(ModbusClientMixin.DATATYPE.STRING, "0123", [b'\x30\x31', b'\x32\x33']),
175181
(ModbusClientMixin.DATATYPE.UINT16, 258, [b'\x01\x02']),
176182
(ModbusClientMixin.DATATYPE.INT16, -32510, [b'\x81\x02']),
183+
(ModbusClientMixin.DATATYPE.INT16, [-32510, 258], [b'\x81\x02', b'\x01\x02']),
177184
(ModbusClientMixin.DATATYPE.UINT32, 16909060, [b'\x01\x02', b'\x03\x04']),
178185
(ModbusClientMixin.DATATYPE.INT32, -2130574588, [b'\x81\x02', b'\x03\x04']),
179186
(
@@ -209,6 +216,9 @@ def test_client_mixin_convert_fail(self):
209216
with pytest.raises(ModbusException):
210217
ModbusClientMixin.convert_from_registers([123], ModbusClientMixin.DATATYPE.FLOAT64)
211218

219+
with pytest.raises(TypeError):
220+
ModbusClientMixin.convert_to_registers(bool, ModbusClientMixin.DATATYPE.BITS)
221+
212222

213223
class TestClientBase:
214224
"""Test client code."""

0 commit comments

Comments
 (0)