3

I have a setup with 2 SDM120 kWh energy meters daisy chained on the same serial port (in the future I want to add a SDM630). I found "Using multiple instruments" in the MinimalModbus communication. I succeed in reading registers on the SDM120 on address 1, but I get an error on reading address 2. The error: minimalmodbus.NoResponseError: No communication with the instrument (no answer).

I can work around it by adding time.sleep(0.1), but I would think that RS485 allows to immediately read the registers of a second address after the first one is completed. I also tried lower values, but eg. time.sleep(0.01) also gave a NoResponseError.

I personally thought the setting instrument.serial.timeout = 1 already would have had the desired effect, but apparently I really need the time.sleep. Is the time.sleep(0.1) the correct way of doing? If so: how can I know the lowest value, so that I don't have a NoResponseError? Trial and error? Could my script be optimized? Especially when timing is important, eg. to avoid injection in the grid (pv diverter, ...). Thanks in advance!

The script:

#!/usr/bin/env python3
import minimalmodbus
import time

instrumentA = minimalmodbus.Instrument('/dev/ttyUSB0', 1, debug = True)  # port name, slave address (in decimal)
instrumentA.serial.baudrate = 9600
instrumentA.serial.timeout  = 1          # seconds
instrumentA.serial.bytesize = 8
instrumentA.serial.parity   = minimalmodbus.serial.PARITY_NONE
instrumentA.serial.stopbits = 1
instrumentA.mode = minimalmodbus.MODE_RTU

instrumentB = minimalmodbus.Instrument('/dev/ttyUSB0', 2, debug = True)
instrumentB.mode = minimalmodbus.MODE_RTU

print ("====== SDM120 instrumentA on addres 1 ======")
print (instrumentA)
P = instrumentA.read_float(12, 4, 2)
print ("Active Power in Watts:", P)

#time.sleep(0.1)  #workaround to avoid NoResponseError 

print ("====== SDM120 instrumentB on addres 2 ======")
print (instrumentB)
P = instrumentB.read_float(12, 4, 2)
print ("Active Power in Watts:", P)

Output without the time.sleep(0.1):

MinimalModbus debug mode. Create serial port /dev/ttyUSB0
MinimalModbus debug mode. Serial port /dev/ttyUSB0 already exists
====== SDM120 instrumentA on addres 1 ======
minimalmodbus.Instrument<id=0x7f36e3dc0df0, address=1, mode=rtu, close_port_after_each_call=False, precalculate_read_size=True, clear_buffers_before_each_transaction=True, handle_local_echo=False, debug=True, serial=Serial<id=0x7f36e3dd90d0, open=True>(port='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False)>
MinimalModbus debug mode. Will write to instrument (expecting 9 bytes back): '\x01\x04\x00\x0c\x00\x02±È' (01 04 00 0C 00 02 B1 C8)
MinimalModbus debug mode. Clearing serial buffers for port /dev/ttyUSB0
MinimalModbus debug mode. No sleep required before write. Time since previous read: 190954.73 ms, minimum silent period: 4.01 ms.
MinimalModbus debug mode. Response from instrument: '\x01\x04\x04\x00\x00\x00\x00û\x84' (01 04 04 00 00 00 00 FB 84) (9 bytes), roundtrip time: 53.3 ms. Timeout for reading: 1000.0 ms.

Active Power in Watts: 0.0
====== SDM120 instrumentB on addres 2 ======
minimalmodbus.Instrument<id=0x7f36e3c55940, address=2, mode=rtu, close_port_after_each_call=False, precalculate_read_size=True, clear_buffers_before_each_transaction=True, handle_local_echo=False, debug=True, serial=Serial<id=0x7f36e3dd90d0, open=True>(port='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False)>
MinimalModbus debug mode. Will write to instrument (expecting 9 bytes back): '\x02\x04\x00\x0c\x00\x02±û' (02 04 00 0C 00 02 B1 FB)
MinimalModbus debug mode. Clearing serial buffers for port /dev/ttyUSB0
MinimalModbus debug mode. Sleeping 2.31 ms before sending. Minimum silent period: 4.01 ms, time since read: 1.70 ms.
MinimalModbus debug mode. Response from instrument: '' () (0 bytes), roundtrip time: 1001.3 ms. Timeout for reading: 1000.0 ms.

Traceback (most recent call last):
  File "./sdm120-daisychain_v3.py", line 25, in <module>
    P = instrumentB.read_float(12, 4, 2)
  File "/home/mattias/.local/lib/python3.8/site-packages/minimalmodbus.py", line 662, in read_float
    return self._generic_command(
  File "/home/mattias/.local/lib/python3.8/site-packages/minimalmodbus.py", line 1170, in _generic_command
    payload_from_slave = self._perform_command(functioncode, payload_to_slave)
  File "/home/mattias/.local/lib/python3.8/site-packages/minimalmodbus.py", line 1240, in _perform_command
    response = self._communicate(request, number_of_bytes_to_read)
  File "/home/mattias/.local/lib/python3.8/site-packages/minimalmodbus.py", line 1406, in _communicate
    raise NoResponseError("No communication with the instrument (no answer)")
minimalmodbus.NoResponseError: No communication with the instrument (no answer)

Output with the time.sleep(0.1):

MinimalModbus debug mode. Create serial port /dev/ttyUSB0
MinimalModbus debug mode. Serial port /dev/ttyUSB0 already exists
====== SDM120 instrumentA on addres 1 ======
minimalmodbus.Instrument<id=0x7f91feddcdf0, address=1, mode=rtu, close_port_after_each_call=False, precalculate_read_size=True, clear_buffers_before_each_transaction=True, handle_local_echo=False, debug=True, serial=Serial<id=0x7f91fedf50d0, open=True>(port='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False)>
MinimalModbus debug mode. Will write to instrument (expecting 9 bytes back): '\x01\x04\x00\x0c\x00\x02±È' (01 04 00 0C 00 02 B1 C8)
MinimalModbus debug mode. Clearing serial buffers for port /dev/ttyUSB0
MinimalModbus debug mode. No sleep required before write. Time since previous read: 176619.62 ms, minimum silent period: 4.01 ms.
MinimalModbus debug mode. Response from instrument: '\x01\x04\x04\x00\x00\x00\x00û\x84' (01 04 04 00 00 00 00 FB 84) (9 bytes), roundtrip time: 53.3 ms. Timeout for reading: 1000.0 ms.

Active Power in Watts: 0.0
====== SDM120 instrumentB on addres 2 ======
minimalmodbus.Instrument<id=0x7f91fec70940, address=2, mode=rtu, close_port_after_each_call=False, precalculate_read_size=True, clear_buffers_before_each_transaction=True, handle_local_echo=False, debug=True, serial=Serial<id=0x7f91fedf50d0, open=True>(port='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1, xonxoff=False, rtscts=False, dsrdtr=False)>
MinimalModbus debug mode. Will write to instrument (expecting 9 bytes back): '\x02\x04\x00\x0c\x00\x02±û' (02 04 00 0C 00 02 B1 FB)
MinimalModbus debug mode. Clearing serial buffers for port /dev/ttyUSB0
MinimalModbus debug mode. No sleep required before write. Time since previous read: 102.09 ms, minimum silent period: 4.01 ms.
MinimalModbus debug mode. Response from instrument: '\x02\x04\x04\x00\x00\x00\x00È\x84' (02 04 04 00 00 00 00 C8 84) (9 bytes), roundtrip time: 52.8 ms. Timeout for reading: 1000.0 ms.

Active Power in Watts: 0.0
m2ts
  • 43
  • 5
  • 1
    [MinimalModbus multiple instruments documentation](https://minimalmodbus.readthedocs.io/en/stable/usage.html?highlight=timeout#using-multiple-instruments) suggests creating a new instrument object for each device, even if they are on the same port/bus. – Bosz Apr 12 '21 at 14:40
  • Thanks @Bosz for the hint, but without a `time.sleep(0.1)` I get the same error. The script I used was: – m2ts Apr 12 '21 at 21:16
  • I've added the additional script I tried to my initial post. – m2ts Apr 12 '21 at 21:22
  • I find this quite surprising. You can read continuously from one instrument but when you try to switch to another one it fails. It looks like a bug but this library is quite simple and I don't see any obvious mistakes. Can you add `debug=True` at the instruments' instantiation, run your code again, and post the full debug log? Note that minimalmodbus already uses `time.sleep()` quite liberally to avoid collisions on the bus (see [here](https://github.com/pyhys/minimalmodbus/blob/master/minimalmodbus.py#L1319)) so if you want to use it in your code you should not feel bad about it... – Marcos G. Apr 13 '21 at 06:36
  • @MarcosG., I have added the `debug=True` and edited my post accordingly (I have removed the old script). I'm ok with the `time.sleep()`, but still I would like to understand why it is needed and why a 0.1 value is ok, but a 0.01 value is not. Does it depend on the library, python, the serial link, the SDM120, will I need another value for a SDM630? What value will that be? So lots of question :-), but already very happy that a time.sleep() at least works :-). – m2ts Apr 13 '21 at 07:58
  • I think I know what the problem is. Can you confirm your SDM120 are model 1000 or 2000? – Marcos G. Apr 13 '21 at 08:46
  • I have the same issue with the need to easily poll multiple slave RS485 devices using Python, with minimal code. As in a test app I have created for Arduino, my code iterates through multiple slave numbers, based on one constant value, 2, 6, 3, etc. So after testing MinimalModbus, I realized the problem with the UART port and slave number being initialized on the same line. Not good. Then I found pyModbus, It uses a read command with (starting register address, Number of inputs, slaveID). – Ashton Apr 18 '23 at 00:45
  • pyModbus cont.: Very hopeful, except the command syntax seems to have changed in the last couple of years, and all of the examples found use the old protocol and give runtime errors. pyModbus' own "examples" are hundreds of code snipits, not including enough details to support use by amateur coders like myself. What is the next option? I'm continuing to work on pyModbus syntax. – Ashton Apr 18 '23 at 00:45

2 Answers2

1

There seems to be nothing wrong with your code or the library you are using (minimalmodbus).

As you probably know, Modbus works in a query-response mode over a half-duplex link. In plain English: you first send a query and the device that query is addressed to answers with the data you asked for.

Both parts of the transaction (queries and responses) travel over the same bus. But the bus is a shared medium and only one device is allowed to take control of the bus (to talk) at any time.

When you have a single master and one or multiple slaves this process works with no issues as long as you guarantee a short silent period after any device writes to the bus. The Modbus specification established this value at 3.5 characters (the time it takes to send 3 and a half characters serially on the bus at the baud rate you are using).

Unfortunately, some manufacturers do not stick to this rule. So some of those devices just take longer than 3.5 characters time to release control of the bus.

This seems to be the case at least with one of your devices. This manual can give you some clues:

manual screenshot

My bet is out of your two devices one of them takes significantly less than the other to release the bus, but that's something you will have to confirm with the debug details. It might even be that the device takes longer to release the bus if you query 20 or 40 registers instead of 4 or 8...

What can you do about it? Well, from the device side, not much, it is what it is. On your software you can do many different things. As I said in the comments above you should not feel bad about using time.sleep() considering that's the way minimalmodbus tries to cope with the bus contention problem.

To make your code more robust you can add try: ... except:. This approach is explained in the documentation. You can keep retrying to read within a loop for a number of attempts or add a small delay to the except chunk. Maybe something like this.

Marcos G.
  • 3,371
  • 2
  • 8
  • 16
1

The answer of Marcos G. (answered Apr 13 at 9:24) includes some background details. In short:

  1. With some trial and error, one can have a value for time.sleep so that minimalmodbus can cope with the bus contention problem.
  2. You can have more robust code with try: ... except:. It might be a good idea to only try a number of times, to avoid a infinite loop.

I include two scripts which use those approaches to my posted problem. Compared to my original question a for loop and an array for the addresses is used.

The first one is with time.sleep

#!/usr/bin/env python3
import minimalmodbus
import time

addr = 1
instrument = minimalmodbus.Instrument('/dev/ttyUSB0', addr)  # port name, slave address (in decimal)

instrument.serial.baudrate = 9600         # Baud
instrument.serial.bytesize = 8
instrument.serial.parity   = minimalmodbus.serial.PARITY_NONE
instrument.serial.stopbits = 1
instrument.serial.timeout  = 1          # seconds
instrument.mode = minimalmodbus.MODE_RTU   # rtu or ascii mode

addresses = [1,2]
registers = [ 0,  6, 12, 18,    24,  30, 70,   72,   74,     76,     78,   84,   86,  88,    90,   92,   94, 258,  264,  342,    344]
names =     ["V","I","P","S",   "Q","PF","f","IAE","EAE",  "IRE",  "ERE","TSP","MSP","ISP","MIP","ESP","MEP","ID","MID","TAE",  "TRE"]
units =     ["V","A","W","VA","var", "","Hz","kWh","kWh","kvarh","kvarh",  "W",  "W",  "W",  "W",  "W",  "W", "A",  "A","kWh","kvarh"]
info = [
"(V for Voltage in volt)",
"(I for Current in ampere)",
"(P for Active Power in watt)",
"(S for Apparent power in volt-ampere)",
"(Q for Reactive power in volt-ampere reactive)",
"(PF for Power Factor)",
"(f for Frequency in hertz)",
"(IAE for Import active energy in kilowatt-hour)",
"(EAE for Export active energy in kilowatt-hour)",
"(IRE for Import reactive energy in kilovolt-ampere reactive hours)",
"(ERE for Export reactive energy in kilovolt-ampere reactive hours)",
"(TSP for Total system power demand in watt)",
"(MSP for Maximum total system power demand in watt)",
"(ISP for Import system power demand in watt)",
"(MIP for Maximum import system power demand in watt)",
"(ESP for Export system power demand in watt)",
"(MEP for MaximumExport system power demand in watt)",
"(ID for current demand in ampere)",
"(MID for Maximum current demand in ampere)",
"(TAE for Total active energy in kilowatt-hour)",
"(TRE for Total reactive energy in kilovolt-ampere reactive hours)",
]
for addr in addresses:
    instrument.address = addr
    print ("=== General info about address", addr, "===")
    print (instrument)
    print ("=== The registers for address", addr, "===")
    for i in range(len(registers)):
        value = instrument.read_float(registers[i], 4, 2)
        print (str(registers[i]).rjust(3), str(value).rjust(20), units[i].ljust(5), info[i])
    time.sleep(0.1) # To avoid minimalmodbus.NoResponseError
    print ("")

The second one with try: ... except:

#!/usr/bin/env python3
import minimalmodbus

# This alternative script `sdm120-daisy-alt.py` will try to reread a device an extra 9 times before giving up and continuing with the other addresses in the array.

addr = 1
instrument = minimalmodbus.Instrument('/dev/ttyUSB0', addr)  # port name, slave address (in decimal)

instrument.serial.baudrate = 9600         # Baud
instrument.serial.bytesize = 8
instrument.serial.parity   = minimalmodbus.serial.PARITY_NONE
instrument.serial.stopbits = 1
instrument.serial.timeout  = 1          # seconds
instrument.mode = minimalmodbus.MODE_RTU   # rtu or ascii mode

addresses = [1,2]
registers = [ 0,  6, 12, 18,    24,  30, 70,   72,   74,     76,     78,   84,   86,  88,    90,   92,   94, 258,  264,  342,    344]
names =     ["V","I","P","S",   "Q","PF","f","IAE","EAE",  "IRE",  "ERE","TSP","MSP","ISP","MIP","ESP","MEP","ID","MID","TAE",  "TRE"]
units =     ["V","A","W","VA","var", "","Hz","kWh","kWh","kvarh","kvarh",  "W",  "W",  "W",  "W",  "W",  "W", "A",  "A","kWh","kvarh"]
info = [
"(V for Voltage in volt)",
"(I for Current in ampere)",
"(P for Active Power in watt)",
"(S for Apparent power in volt-ampere)",
"(Q for Reactive power in volt-ampere reactive)",
"(PF for Power Factor)",
"(f for Frequency in hertz)",
"(IAE for Import active energy in kilowatt-hour)",
"(EAE for Export active energy in kilowatt-hour)",
"(IRE for Import reactive energy in kilovolt-ampere reactive hours)",
"(ERE for Export reactive energy in kilovolt-ampere reactive hours)",
"(TSP for Total system power demand in watt)",
"(MSP for Maximum total system power demand in watt)",
"(ISP for Import system power demand in watt)",
"(MIP for Maximum import system power demand in watt)",
"(ESP for Export system power demand in watt)",
"(MEP for MaximumExport system power demand in watt)",
"(ID for current demand in ampere)",
"(MID for Maximum current demand in ampere)",
"(TAE for Total active energy in kilowatt-hour)",
"(TRE for Total reactive energy in kilovolt-ampere reactive hours)",
]
for addr in addresses:
    instrument.address = addr
    print ("=== General info about address", addr, "===")
    print (instrument)
    print ("=== The registers for address", addr, "===")
    for attempt in range(10):
        try:
            for i in range(len(registers)):
                value = instrument.read_float(registers[i], 4, 2)
                print (str(registers[i]).rjust(3), str(value).rjust(20), units[i].ljust(5), info[i])
        except minimalmodbus.NoResponseError:
            print("NoResponseError: did't work on attempt ", attempt+1, "I will retry")
        else:
            print ("Succeeded on attempt", attempt+1)
            break
    print ("")
m2ts
  • 43
  • 5