Search code examples
pythonpython-3.xstructieee-754pymodbus

Unpack IEEE 754 Floating Point Number


I am reading two 16 bit registers from a tcp client using the pymodbus module. The two registers make up a 32 bit IEEE 754 encoded floating point number. Currently I have the 32 bit binary value of the registers shown in the code below.

start_address = 0x1112
reg_count = 2
client = ModbusTcpClient(<IP_ADDRESS>)
response = client.read_input_registers(start_address,reg_count)
reg_1 = response.getRegister(0)<<(16 - (response.getRegister(0).bit_length())) #Get in 16 bit format
reg_2 = response.getRegister(1)<<(16 - (response.getRegister(1).bit_length())) #Get in 16 bit format 
volts = (reg_1 << 16) | reg_2 #Get the 32 bit format

The above works fine to get the encoded value the problem is decoding it. I was going to code something like in this video but I came across the 'f' format in the struct module for IEEE 754 encoding. I tried decode the 32 bit float stored in volts in the code above using the unpack method in the struct module but ran into the following errors.

val = struct.unpack('f',volts)
>>> TypeError: a bytes-like object is required, not 'int'

Ok tried convert it to a 32 bit binary string.

temp = bin(volts)
val = struct.unpack('f',temp)
>>> TypeError: a bytes-like object is required, not 'str'

Tried to covert it to a bytes like object as in this post and format in different ways.

val = struct.unpack('f',bytes(volts))
>>> TypeError: string argument without an encoding

temp = "{0:b}".format(volts)
val = struct.unpack('f',temp)
>>> ValueError: Unknown format code 'b' for object of type 'str'

val = struct.unpack('f',volts.encode())
>>> struct.error: unpack requires a buffer of 4 bytes

Where do I add this buffer and where in the documentation does it say I need this buffer with the unpack method? It does say in the documentation

The string must contain exactly the amount of data required by the format (len(string) must equal calcsize(fmt)).

The calcsize(fmt) function returns a value in bytes but the len(string) returns a value of the length of the string, no?

Any suggestions are welcome.

EDIT

There is a solution to decoding below however a better solution to obtaining the 32 bit register value from the two 16 bit register values is shown below compared to the original in the question.

start_address = 0x1112
reg_count = 2
client = ModbusTcpClient(<IP_ADDRESS>)
response = client.read_input_registers(start_address,reg_count)
reg_1 = response.getRegister(0)
reg_2 = response.getRegister(1)
# Shift reg 1 by 16 bits
reg_1s = reg_1 << 16
# OR with the reg_2
total = reg_1s | reg_2

Solution

  • I found a solution to the problem using the BinaryPayloadDecoder.fromRegisters() from the pymodbus moudule instead of the struct module. Note that this solution is specific to the modbus smart meter device I am using as the byte order and word order of the registers could change in other devices. It may still work in other devices to decode registers but I would advise to read the documentation of the device first to be sure. I left in the comments in the code below but when I refer to page 24 this is just for my device.

    from pymodbus.client.sync import ModbusTcpClient
    from pymodbus.constants import Endian
    from pymodbus.payload import BinaryPayloadDecoder
    
    start_address = 0x1112
    reg_count = 2
    client = ModbusTcpClient(<IP_ADDRESS>)
    response = client.read_input_registers(start_address,reg_count)
    # The response will contain two registers making a 32 bit floating point number
    # Use the BinaryPayloadDecoder.fromRegisters() function to decode
    # The coding scheme for a 32 bit float is IEEE 754 https://en.wikipedia.org/wiki/IEEE_754
    # The MS Bytes are stored in the first address and the LS bytes are stored in the second address,
    # this corresponds to a big endian byte order (Second parameter in function)
    # The documentation for the Modbus registers for the smart meter on page 24 says that
    # the low word is the first priority, this correspond to a little endian word order (Third parameter in function)
    decoder = BinaryPayloadDecoder.fromRegisters(response.registers, Endian.Big, wordorder=Endian.Little)
    final_val = (decoder.decode_32bit_float())
    client.close()
    
    

    EDIT Credit to juanpa-arrivillaga and chepner the problem can be solved using the struct module also with the byteorder='little'. The two functions in the code below can be used if the byteorder is little or if the byte order is big depending upon the implementation.

    import struct
    from pymodbus.client.sync import ModbusTcpClient
    
    def big_endian(response):
        reg_1 = response.getRegister(0)
        reg_2 = response.getRegister(1)
        # Shift reg 1 by 16 bits
        reg_1s = reg_1 << 16
        # OR with the reg_2
        total = reg_1s | reg_2
        return total
    
    def little_endian(response):
        reg_1 = response.getRegister(0)
        reg_2 = response.getRegister(1)
        # Shift reg 2 by 16 bits
        reg_2s = reg_2 << 16
        # OR with the reg_1
        total = reg_2s | reg_1
        return(total)
    
    start_address = 0x1112
    reg_count = 2
    client = ModbusTcpClient(<IP_ADDRESS>)
    response = client.read_input_registers(start_address,reg_count)
    
    # Little 
    little = little_endian(response)
    lit_byte = little.to_bytes(4,byteorder='little')
    print(struct.unpack('f',lit_byte))
    
    # Big 
    big = big_endian(response)
    big_byte = big.to_bytes(4,byteorder='big')
    print(struct.unpack('f',big_byte))