ljean / modbus-tk

Create Modbus app easily with Python
Other
572 stars 212 forks source link

Problem with holding registers encoded as float #93

Open ductsoup opened 6 years ago

ductsoup commented 6 years ago

In the example below I'm writing the same value to five holding registers encoded as a Modbus 32-bit float to a Modbus slave but the external master is not seeing the same value in all five registers, the first value is wrong. This happens in both Python2 and Python3 running under Raspbian Jessie.

I've been chasing this for a week and still don't have the slightest clue what's going on. Does anyone have any ideas?

TIA, Craig

#!/usr/bin/env python -u
import time
from math import pi
import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp
import struct

def mb_set_float(addr, val):
    j = struct.unpack('>HH',struct.pack('>f', val))
    slave_1.set_values('ro', addr, j)
    return val

if __name__ == "__main__":
    try:
        """ Initialize the MODBUS TCP slave """        
        server = modbus_tcp.TcpServer(address='0.0.0.0')
        server.start()
        slave_1 = server.add_slave(1)
        slave_1.add_block('ro', cst.HOLDING_REGISTERS, 40000, 20)
        print("Ready")
        while True:
            mb_set_float(40001, pi)
            mb_set_float(40003, pi)
            mb_set_float(40005, pi)
            mb_set_float(40007, pi)
            mb_set_float(40009, pi)
            time.sleep(1)
    except:
        server.stop()

This is the result from Modpoll, Industrial Automation's Ignition product shows the same.

modpoll -m tcp -t 4:float -r 40001 -c 5 -o 3 192.168.x.x
Polling slave... (Ctrl-C to stop)
[40001]: 3.140625
[40003]: 3.141593
[40005]: 3.141593
[40007]: 3.141593
[40009]: 3.141593

If I query within the Python script it reports the correct value.

i2, i1 = slave_1.get_values('ro', 40001, 2)
print(struct.unpack('<f',struct.pack('<HH',i1,i2))[0])
#3.1415927410125732
ljean commented 6 years ago

Hello,

I would say that the problem comes from Modpoll. Did you contact them?

Best luc

ductsoup commented 6 years ago

As I mentioned, our SCADA solution is confirming the odd behavior. It seems unlikely two independent MODBUS masters would have the same flaw.

ljean commented 6 years ago

Do you mean that both modpoll and ignition gets 3.140625 for register 40001?

ljean commented 6 years ago

What do you get if you read the register just as regular bytes?

ljean commented 6 years ago
>>> struct.unpack('>HH', struct.pack('>f', 3.140625))
(16457, 0)

The question is why it reads 0 in register 40002?

ductsoup commented 6 years ago

Exactly, the first register is getting lost somewhere and I have no idea why.

[40001]: 0
[40002]: 16457
[40003]: 4059
[40004]: 16457
blotsome commented 6 years ago

This is a common problem with modbus and depends on whether you are using base-0 or base-1.

Using your exact code, you are creating a code block starting at 40000, but you don't write the first register, you are writing 40001 first. Your zero is the fact that you are skipping that first register. If you look at the entire range you'll see every register you write has the 16457/4059 pairing, in that order, which can successfully decode to 3.1416 if you use a switch the bit order and start the scan one register later. Alternatively, you can start writing at 40000 instead of 40001, and I would recommend changing your > to < in your float conversion function. This will allow you to start scanning at 40001 in base-1, and use 32-bit float data type, and have 3.1416 show up. There are a number of ways to get the data to look correct and I can't say what it best for your application, but I can say this is not a bug with modbus-tk, but instead comes from your code allocating a block of registers, but not writing to the first one, compounded with base-0 vs. base-1 numbering, and bit order.

ductsoup commented 6 years ago

Thanks for the reply. A couple of observations and clarifications.

There is no standard for word/byte order or base 0/1 register addressing that I'm aware of. Prior versions of modbus-tk did not have this bad behavior. Regardless of how the address block is allocated it shouldn't matter which register is written first. Last, if the first register is half a word/byte off the subsequent four should also be wrong, and they're not.

Good information for general MODBUS troubleshooting but I'm not getting how it applies to this specific example. That said, I never claim to know everything so, if you have a corrected version of the test code I posted above could you please share it here?

blotsome commented 6 years ago

If you don't want to change the data type on your master from LSR to MSR, then you need to do what I said above. "start writing at 40000 instead of 40001, and... change your > to < in your float conversion function". What does that look like:

def mb_set_float(addr, val):
    j = struct.unpack('<HH',struct.pack('<f', val))
    slave_1.set_values('ro', addr, j)
    return val

if __name__ == "__main__":
    try:
        """ Initialize the MODBUS TCP slave """        
        server = modbus_tcp.TcpServer(address='0.0.0.0')
        server.start()
        slave_1 = server.add_slave(1)
        slave_1.add_block('ro', cst.HOLDING_REGISTERS, 40000, 20)
        print("Ready")
        while True:
            mb_set_float(40000, pi)
            mb_set_float(40002, pi)
            mb_set_float(40004, pi)
            mb_set_float(40006, pi)
            mb_set_float(40008, pi)
            time.sleep(1)
    except:
        server.stop()

You can see the disconnect (where the zero value was coming from) in that you start polling and start the block at 40000, but don't start writing until 40001, so nothing is being written to 40000. To fix that, change where you start writing. I set up modscan so that it was viewing the data exactly as you copied it above, then I changed the code as described here and values all go to 3.1416. I believe this is the fix you are looking for that doesn't require changing anything on your master. Tested your command in modpoll and also worked. However, if you don't want to change your code and can change your master, This is the modpoll command using your base code that will get it to view properly : modpoll -m tcp -t 4:float -f -r 40002 -c 5 -o 3 192.168.x.x note the -f switch and where the starting register is.

ductsoup commented 6 years ago

Tested your modification to my example this morning, it's got the same bad behavior only different. It did inspire me to think about this from a new direction and find a solution.

The word order not specified in the MODBUS standard doesn't matter so long as it's consistent. I used the default word order for our MODBUS OPC master plugin which is the same defalt for modpoll. That's not the issue here.

The reason I created the block at 40000 and didn't write to the first register is our OPC (and modpoll) expects one-based, not zero-based addressing by default. Made that adjustment and everything lit up like a Christmas tree. Note the -0 flag.

To my understanding however you implement the zero/one base addressing in modbus-tk it shouldn't matter to the master so long as it knows what to expect. Apparently it does so that's what I think is the possible bug in the modbus-tk code.

Either way, this example works as expected.

#!/usr/bin/env python -u
import time
from math import pi
import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp
import struct

def mb_set_float(addr, val):
    j = struct.unpack('<HH',struct.pack('<f', val))
    print("%02x %02x" % j)
    slave_1.set_values('ro', addr, j)
    #print("%02x %02x" % slave_1.get_values('ro', addr,2))
    return val

if __name__ == "__main__":
    try:
        """ Initialize the MODBUS TCP slave """        
        server = modbus_tcp.TcpServer(address='0.0.0.0')
        server.start()
        slave_1 = server.add_slave(1)
        slave_1.add_block('ro', cst.HOLDING_REGISTERS, 40001, 20)
        print("Ready")
        mb_set_float(40001, pi)
        mb_set_float(40003, pi+1.0)
        mb_set_float(40005, pi+2.0)
        mb_set_float(40007, pi+3.0)
        mb_set_float(40009, pi+4.0)
        mb_set_float(40011, pi+5.0)
        mb_set_float(40013, 0.0)
        while True:
            time.sleep(1)
    except:
        server.stop()

'''
modpoll -0 -1 -m tcp -t 4:hex -f -r 40001 -c 12 -o 3 192.168.5.36
modpoll 2.9 - FieldTalk(tm) Modbus(R) Polling Utility
Copyright (c) 2002-2010 proconX Pty Ltd
Visit http://www.modbusdriver.com for Modbus libraries and tools.

Protocol configuration: MODBUS/TCP
Slave configuration...: Address = 1, start reference = 40001 (PDU), count = 12
Communication.........: 192.168.5.36, port 502, t/o 3.00 s
Data type.............: 16-bit register (hex), output (holding) register table
Word swapping.........: Slave configured as big-endian float machine

Polling slave ...
[40001]: 0x0FDB
[40002]: 0x4049
[40003]: 0x87ED
[40004]: 0x4084
[40005]: 0x87ED
[40006]: 0x40A4
[40007]: 0x87ED
[40008]: 0x40C4
[40009]: 0x87ED
[40010]: 0x40E4
[40011]: 0x43F7
[40012]: 0x4102

modpoll -0 -1 -m tcp -t 4:float -r 40001 -c 8 -o 3 192.168.5.36
modpoll 2.9 - FieldTalk(tm) Modbus(R) Polling Utility
Copyright (c) 2002-2010 proconX Pty Ltd
Visit http://www.modbusdriver.com for Modbus libraries and tools.

Protocol configuration: MODBUS/TCP
Slave configuration...: Address = 1, start reference = 40001 (PDU), count = 8
Communication.........: 192.168.5.36, port 502, t/o 3.00 s
Data type.............: 32-bit float, output (holding) register table

Polling slave ...
[40001]: 3.141593
[40003]: 4.141593
[40005]: 5.141593
[40007]: 6.141593
[40009]: 7.141593
[40011]: 8.141593
[40013]: 0.000000
[40015]: 0.000000
'''
blotsome commented 6 years ago

Hey, you are changing your polling parameters on me, don't blame my code. :P If you run the command you have in your original post modpoll -m tcp -t 4:float -r 40001 -c 5 -o 3 192.168.x.x while running the code in my last post, you will get:

>modpoll -m tcp -t 4:float -r 40001 -c 5 -o 3 127.0.0.1
modpoll 3.4 - FieldTalk(tm) Modbus(R) Master Simulator
Copyright (c) 2002-2013 proconX Pty Ltd
Visit http://www.modbusdriver.com for Modbus libraries and tools.

Protocol configuration: MODBUS/TCP
Slave configuration...: address = 1, start reference = 40001, count = 5
Communication.........: 127.0.0.1, port 502, t/o 3.00 s, poll rate 1000 ms
Data type.............: 32-bit float, output (holding) register table

-- Polling slave... (Ctrl-C to stop)
[40001]: 3.141593
[40003]: 3.141593
[40005]: 3.141593
[40007]: 3.141593
[40009]: 3.141593

Which is I believe what you were looking for. If you run the commands you added in your last post such as the -0 switch, then yes, my code returns just zeros. However, that wasn't the command you originally posted so I apologize I didn't take that into account. I'm glad I got you on the right track, and that it's working for you now. Enjoy!

ductsoup commented 6 years ago

True, sorry I didn't mention that but that's how I spotted it. Try -c 8 instead. Reversing the endianness just moved the problem from the first to the last register. If you write different values and switch to -t 4:hex it really jumps out.

harishmohan4884 commented 4 years ago

Hi, I'm using modbus-tk slave simulator for my project. I'm using the above code mentioned in the comment "blotsome commented on Jul 12, 2018". My code is working for values up to 125.5. If I give 126.5, the value gets truncate and when I fetch from other interfaces like web, SNMP the value is showing as 126.

Please help me with this. Thanks in Advance float.txt

Harish

blotsome commented 4 years ago

I tried 126.5 and the value in your txt file 130.5 and got this:

Protocol configuration: MODBUS/TCP
Slave configuration...: address = 1, start reference = 286, count = 5
Communication.........: 127.0.0.1, port 502, t/o 3.00 s, poll rate 1000 ms
Data type.............: 32-bit float, output (holding) register table

-- Polling slave... (Ctrl-C to stop)
[286]: 126.500000
[288]: 0.000000
[290]: 0.000000
[292]: 0.000000
[294]: 0.000000

and

-- Polling slave... (Ctrl-C to stop)
[286]: 130.500000
[288]: 0.000000
[290]: 0.000000
[292]: 0.000000
[294]: 0.000000

so I updated your code to set values in both the ranges you say work and the ranges you say don't work:

while True:
            mb_set_float(285, 124.5)
            mb_set_float(287, 125.5)
            mb_set_float(289, 126.5)
            mb_set_float(291, 127.5)
            mb_set_float(293, 128.5)

And scanned again:

Protocol configuration: MODBUS/TCP
Slave configuration...: address = 1, start reference = 286, count = 10
Communication.........: 127.0.0.1, port 502, t/o 3.00 s, poll rate 1000 ms
Data type.............: 32-bit float, output (holding) register table

-- Polling slave... (Ctrl-C to stop)
[286]: 124.500000
[288]: 125.500000
[290]: 126.500000
[292]: 127.500000
[294]: 128.500000

Just as we expect, so I dove down deeper, and looked at each register:

Protocol configuration: MODBUS/TCP
Slave configuration...: address = 1, start reference = 286, count = 10
Communication.........: 127.0.0.1, port 502, t/o 3.00 s, poll rate 1000 ms
Data type.............: 16-bit register, output (holding) register table

-- Polling slave... (Ctrl-C to stop)
[286]: 0
[287]: 17145
[288]: 0
[289]: 17147
[290]: 0
[291]: 17149
[292]: 0
[293]: 17151
[294]: -32768
[295]: 17152

We do see that for the values you say work, the first 16-bit register is not being used, but when we get higher up, it is being used. But where that start doesn't correlate to where you say you are seeing problems. My intuition is that there is nothing wrong with the python code here, but something wrong with how you are reading the data. Can you explain what you are doing to "fetch" these values, or how you use SNMP to fetch values?