PyModbus
PyModbusDocs

Working with Data Types in PyModbus

Convert between Modbus registers and Python data types - integers, floats, strings, bits.

Data Types in PyModbus

Modbus only knows 16-bit registers and single bits. Here's how to work with real data types.

Basic Conversions

16-bit Integer (Single Register)

# Read single register as unsigned int (0-65535)
result = client.read_holding_registers(100, 1)
value = result.registers[0]

# Convert to signed int (-32768 to 32767)
if value > 32767:
    signed_value = value - 65536
else:
    signed_value = value

# Or use struct
import struct
bytes_val = struct.pack('>H', value)
signed = struct.unpack('>h', bytes_val)[0]

32-bit Integer (Two Registers)

from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
from pymodbus.constants import Endian

# Read 32-bit integer
result = client.read_holding_registers(100, 2)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
value = decoder.decode_32bit_int()  # or decode_32bit_uint() for unsigned

# Write 32-bit integer
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_32bit_int(-123456)
registers = builder.to_registers()
client.write_registers(100, registers)

Float (32-bit IEEE 754)

# Read float
result = client.read_holding_registers(100, 2)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
float_value = decoder.decode_32bit_float()

# Write float
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_32bit_float(3.14159)
registers = builder.to_registers()
client.write_registers(100, registers)

Double (64-bit Float)

# Read double (4 registers)
result = client.read_holding_registers(100, 4)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
double_value = decoder.decode_64bit_float()

# Write double
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG,
    wordorder=Endian.BIG
)
builder.add_64bit_float(3.141592653589793)
registers = builder.to_registers()
client.write_registers(100, registers)

Strings

# Read string (8 registers = 16 characters)
result = client.read_holding_registers(100, 8)
decoder = BinaryPayloadDecoder.fromRegisters(
    result.registers,
    byteorder=Endian.BIG
)
string_value = decoder.decode_string(16).decode('ascii').strip('\x00')

# Write string
builder = BinaryPayloadBuilder(
    byteorder=Endian.BIG
)
text = "HELLO WORLD"
# Pad to fixed length
text = text.ljust(16, '\x00')
builder.add_string(text)
registers = builder.to_registers()
client.write_registers(100, registers)

Bits (From Registers)

# Read register and extract bits
result = client.read_holding_registers(100, 1)
value = result.registers[0]

# Extract individual bits
bit_0 = bool(value & 0x0001)
bit_1 = bool(value & 0x0002)
bit_2 = bool(value & 0x0004)
bit_7 = bool(value & 0x0080)
bit_15 = bool(value & 0x8000)

# Extract all bits
bits = [(value >> i) & 1 for i in range(16)]
print(f"Bits: {bits}")

# Set specific bits
value = 0
value |= (1 << 0)  # Set bit 0
value |= (1 << 5)  # Set bit 5
value |= (1 << 15) # Set bit 15
client.write_register(100, value)

Endianness (Byte Order)

Different devices use different byte orders. Try all combinations:

def try_all_endianness(registers):
    """Try all byte/word order combinations."""
    from pymodbus.constants import Endian
    
    combinations = [
        (Endian.BIG, Endian.BIG, "Big-Big"),
        (Endian.BIG, Endian.LITTLE, "Big-Little"),
        (Endian.LITTLE, Endian.BIG, "Little-Big"),
        (Endian.LITTLE, Endian.LITTLE, "Little-Little"),
    ]
    
    for byte_order, word_order, name in combinations:
        decoder = BinaryPayloadDecoder.fromRegisters(
            registers,
            byteorder=byte_order,
            wordorder=word_order
        )
        value = decoder.decode_32bit_float()
        print(f"{name}: {value}")

# Read and test
result = client.read_holding_registers(100, 2)
try_all_endianness(result.registers)

Common Scaled Values

Many devices use scaled integers instead of floats:

# Temperature scaled by 10 (23.5°C = 235)
result = client.read_holding_registers(100, 1)
temperature = result.registers[0] / 10.0

# Pressure scaled by 100 (1.23 bar = 123)
result = client.read_holding_registers(101, 1)
pressure = result.registers[0] / 100.0

# Power in kilowatts (1234 = 1.234 kW)
result = client.read_holding_registers(102, 1)
power = result.registers[0] / 1000.0

# Write scaled values
temp_scaled = int(23.5 * 10)  # 235
client.write_register(100, temp_scaled)

BCD (Binary Coded Decimal)

Some devices use BCD encoding:

def bcd_to_int(bcd_value):
    """Convert BCD to integer."""
    result = 0
    multiplier = 1
    while bcd_value > 0:
        digit = bcd_value & 0x0F
        result += digit * multiplier
        multiplier *= 10
        bcd_value >>= 4
    return result

def int_to_bcd(value):
    """Convert integer to BCD."""
    result = 0
    shift = 0
    while value > 0:
        digit = value % 10
        result |= (digit << shift)
        shift += 4
        value //= 10
    return result

# Read BCD value
result = client.read_holding_registers(100, 1)
bcd = result.registers[0]
value = bcd_to_int(bcd)
print(f"BCD {bcd:04X} = {value}")

# Write BCD value
bcd = int_to_bcd(1234)
client.write_register(100, bcd)

Custom Data Structures

from dataclasses import dataclass
from typing import List

@dataclass
class MotorData:
    speed: float      # RPM
    current: float    # Amps
    temperature: float # Celsius
    status: int       # Status bits
    
    @classmethod
    def from_registers(cls, registers: List[int]):
        """Create from Modbus registers."""
        decoder = BinaryPayloadDecoder.fromRegisters(
            registers,
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        
        return cls(
            speed=decoder.decode_32bit_float(),
            current=decoder.decode_32bit_float(),
            temperature=decoder.decode_32bit_float(),
            status=decoder.decode_16bit_uint()
        )
    
    def to_registers(self) -> List[int]:
        """Convert to Modbus registers."""
        builder = BinaryPayloadBuilder(
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        
        builder.add_32bit_float(self.speed)
        builder.add_32bit_float(self.current)
        builder.add_32bit_float(self.temperature)
        builder.add_16bit_uint(self.status)
        
        return builder.to_registers()

# Read motor data (7 registers total)
result = client.read_holding_registers(100, 7)
motor = MotorData.from_registers(result.registers)
print(f"Motor: Speed={motor.speed} RPM, Current={motor.current} A")

# Write motor data
motor = MotorData(speed=1500.0, current=2.5, temperature=45.0, status=1)
client.write_registers(100, motor.to_registers())

Time and Date

from datetime import datetime

def read_datetime(client, address):
    """Read date/time from 3 registers (BCD format)."""
    result = client.read_holding_registers(address, 3)
    
    # Register 0: Year (BCD)
    # Register 1: Month (high byte) and Day (low byte)
    # Register 2: Hour (high byte) and Minute (low byte)
    
    year = bcd_to_int(result.registers[0]) + 2000
    month = bcd_to_int(result.registers[1] >> 8)
    day = bcd_to_int(result.registers[1] & 0xFF)
    hour = bcd_to_int(result.registers[2] >> 8)
    minute = bcd_to_int(result.registers[2] & 0xFF)
    
    return datetime(year, month, day, hour, minute)

def write_datetime(client, address, dt):
    """Write date/time to 3 registers (BCD format)."""
    year_bcd = int_to_bcd(dt.year - 2000)
    month_bcd = int_to_bcd(dt.month)
    day_bcd = int_to_bcd(dt.day)
    hour_bcd = int_to_bcd(dt.hour)
    minute_bcd = int_to_bcd(dt.minute)
    
    registers = [
        year_bcd,
        (month_bcd << 8) | day_bcd,
        (hour_bcd << 8) | minute_bcd
    ]
    
    client.write_registers(address, registers)

# Example usage
dt = read_datetime(client, 100)
print(f"Device time: {dt}")

write_datetime(client, 100, datetime.now())

Array Handling

def read_float_array(client, address, count):
    """Read array of floats."""
    # Each float is 2 registers
    result = client.read_holding_registers(address, count * 2)
    
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    
    values = []
    for _ in range(count):
        values.append(decoder.decode_32bit_float())
    
    return values

def write_float_array(client, address, values):
    """Write array of floats."""
    builder = BinaryPayloadBuilder(
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    
    for value in values:
        builder.add_32bit_float(value)
    
    client.write_registers(address, builder.to_registers())

# Read 10 temperature values
temperatures = read_float_array(client, 100, 10)
print(f"Temperatures: {temperatures}")

# Write setpoints
setpoints = [20.0, 21.5, 22.0, 23.5, 25.0]
write_float_array(client, 200, setpoints)

Quick Reference

# All available decoders
decoder.decode_8bit_int()
decoder.decode_8bit_uint()
decoder.decode_16bit_int()
decoder.decode_16bit_uint()
decoder.decode_32bit_int()
decoder.decode_32bit_uint()
decoder.decode_64bit_int()
decoder.decode_64bit_uint()
decoder.decode_32bit_float()
decoder.decode_64bit_float()
decoder.decode_string(size)
decoder.decode_bits()

# All available builders
builder.add_8bit_int(value)
builder.add_8bit_uint(value)
builder.add_16bit_int(value)
builder.add_16bit_uint(value)
builder.add_32bit_int(value)
builder.add_32bit_uint(value)
builder.add_64bit_int(value)
builder.add_64bit_uint(value)
builder.add_32bit_float(value)
builder.add_64bit_float(value)
builder.add_string(value)
builder.add_bits(values)

Always check device documentation for data type, scaling, and byte order. When in doubt, try all combinations.

Next Steps

How is this guide?