Search code examples
pythonmodbusplcpymodbus

How to write INPUT REGISTERS using pymodbus for external Modbus client which will read them


I have been tasked with implementing a pymodbus-based Modbus server. The server will run on a Linux machine like a Raspberry Pi or Up2 controller. It is expected to interface with a Modbus client which I have no control over. That external Modbus client is expecting to be able to read INPUT REGISTERS as well as holding registers served by my Modbus server.

I can set the values of the HOLDING registers that will be read by the external client. I have been unable to set the values of the INPUT registers that the external client will read. How does one do that?

I saw this post which asked a similar question but the question doesn't seem to ever have been answered:

How to write to PLC input registers using pymodbus

Thanks in advance for any help!


Solution

  • Thanks to Marker and to all the examples online. I finally got this working as I wanted. Hope this helps someone else.

    There were several gotchas I ran into:

    1. I tried following examples I found online all of which used pymodbus.server.async instead of pymodbus.server.sync. I found that I could not import pymodbus.server.async because "async" is a reserved word in Python3.7! (not in older versions of Python). Either way I wanted to use pymodbus.server.sync because I wanted to avoid importing twisted if at all possible. This server will have 1-3 clients connecting to it at the most.
    2. All the examples showing an updating writer used "LoopingCall" from Twisted. I have no idea what Twisted is and didn't want to use it unless I had to. I was familiar with multiprocessing and threading. I was already launching the ModbusTcpServer in a process and was trying to create managed object(s) around the store / context so I could have a different Process doing the updating. But that didn't work: I'm guessing StartTcpServer doesn't like receiving managed objects(?) and I didn't want to delve into that function.
    3. One of the examples commented that a Python thread could be used, and that solved it. I still have the ModbusTcpServer launched in a Process but right before I call "StartTcpServer" I kickoff a THREAD rather than a PROCESS with the updating writer. Then I didn't need to put the store / context in managed object(s) since the Thread can see the same dataspace as the Process that kicked it off. I just needed ANOTHER managed object to send messages into this Thread the way I was already used to doing with a Process.

    Sooo...

    First I had to do this: from threading import Thread

    Then I kicked the following off in a Process as I'd done before, but RIGHT BEFORE calling StartTcpServer I kicked off the updating_writer Thread (all start_addr, init_val and num_addrs variables are set earlier).

    discrete_inputs_obj = ModbusSequentialDataBlock(di_start_addr, [di_init_val]*di_num_addrs)
    coils_obj = ModbusSequentialDataBlock(co_start_addr, [co_init_val]*co_num_addrs)
    holding_regs_obj = ModbusSequentialDataBlock(hr_start_addr, [hr_init_val]*hr_num_addrs)
    input_regs_obj = ModbusSequentialDataBlock(ir_start_addr, [ir_init_val]*ir_num_addrs)
    mb_store = ModbusSlaveContext(di=discrete_inputs_obj, co=coils_obj, hr=holding_regs_obj, ir=input_regs_obj, zero_mode=True)
    mb_context = ModbusServerContext(slaves=mb_store, single=True)
    
    mb_store = ModbusSlaveContext(
        di=ModbusSequentialDataBlock(di_start_addr, [di_init_val]*di_num_addrs),
        co=ModbusSequentialDataBlock(co_start_addr, [co_init_val]*co_num_addrs),
        hr=ModbusSequentialDataBlock(hr_start_addr, [hr_init_val]*hr_num_addrs),
        ir=ModbusSequentialDataBlock(ir_start_addr, [ir_init_val]*ir_num_addrs))
    mb_context = ModbusServerContext(slaves=mb_store, single=True)
    
    updating_writer_cfg = {}
    updating_writer_cfg["mb_context"] = mb_context
    updating_writer_cfg["managed_obj"] = managed_obj    #For being able to send messages to this Thread
    
    updating_writer_thread = Thread(target = updating_writer, args = [updating_writer_cfg])    # We need this to be a thread in this process so that they can share the same datastore
    updating_writer_thread.start()
    StartTcpServer(mb_context, address=("", port))
    

    In the While loop of updating_writer I have code that polls the managed_obj to receive messages. In adding the key bits of code in that loop are:

    mb_context[0].setValues(4, addr_to_write, regs_to_write)
    

    ...where 4 is the write function, addr_to_write is the register address at which to start writing and regs_to_write is a list of register values...AND...

    regs_to_read = mb_context[0].getValues(3, addr_to_read, num_regs_to_read)
    

    ...where 3 is the read function, addr_to_read is the register address at which to start reading. regs_to_read will be a list of length num_regs_to_read.