2

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
WK123
  • 620
  • 7
  • 18
  • 2
    I'm pretty sure you want nothing to do with `bin`. try using something like `volts.to_bytes(4,byteorder='big')` – juanpa.arrivillaga Mar 21 '20 at 02:19
  • I did try this and it did run without any errors however the value was not correct, I posted a solution that solved my problem below. Thanks for the suggestion! – WK123 Mar 21 '20 at 18:06
  • 2
    @WK123 You probably want `byteorder='little'`, then. – chepner Mar 21 '20 at 18:20
  • Yes this seems to work, I have two functions for sorting the two different ways I will share in another solution maybe it may help someone out there who isn't using modbus library. Thank you! – WK123 Mar 21 '20 at 18:34

1 Answers1

2

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))


WK123
  • 620
  • 7
  • 18