1

I'm a newbie to either docker and Modbus, I am trying to write custom client/server application using Modbus (pymodbus toolkit to be precise), and I run into some issues probably with docker.

I've tried sample scripts client , server

The parts I'm using are:

Server (dockerized)

from pymodbus.server.sync import StartTcpServer
from pymodbus.server.sync import StartTlsServer
from pymodbus.server.sync import StartUdpServer
from pymodbus.server.sync import StartSerialServer

from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSparseDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext

from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer
# --------------------------------------------------------------------------- #
# configure the service logging
# --------------------------------------------------------------------------- #
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s'
          ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)


def run_server():
    store = ModbusSlaveContext(
        di=ModbusSequentialDataBlock(0, [17]*100),
        co=ModbusSequentialDataBlock(0, [17]*100),
        hr=ModbusSequentialDataBlock(0, [17]*100),
        ir=ModbusSequentialDataBlock(0, [17]*100))

    context = ModbusServerContext(slaves=store, single=True)

    identity = ModbusDeviceIdentification()
    identity.VendorName = 'Pymodbus'
    identity.ProductCode = 'PM'
    identity.VendorUrl = 'http://github.com/riptideio/pymodbus/'
    identity.ProductName = 'Pymodbus Server'
    identity.ModelName = 'Pymodbus Server'
    identity.MajorMinorRevision = '2.3.0'


    StartTcpServer(context, identity=identity, address=("0.0.0.0", 5020))

if __name__ == "__main__":
    print("running")
    run_server()

And I've tinker only with IP addresses. I can get it working on localhost, but when I put the server part in Docker container, I'm not able to communicate with it.

I am using docker-compose, with .yml as follows:

version: '3.4'

services:
  server:
    build: ./server
    #network_mode: host
    expose:
      - 5020
    ports:
      - 5020:5020

Client

from pymodbus.client.sync import ModbusTcpClient as ModbusClient
# from pymodbus.client.sync import ModbusUdpClient as ModbusClient
# from pymodbus.client.sync import ModbusSerialClient as ModbusClient

# --------------------------------------------------------------------------- #
# configure the client logging
# --------------------------------------------------------------------------- #
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s '
          '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)

UNIT = 0x1


def run_sync_client():

    client = ModbusClient('127.0.0.1', port=5020)
    client.connect()

    log.debug("Reading Coils")
    rr = client.read_coils(1, 1, unit=UNIT)
    log.debug(rr)

    log.debug("Write to a Coil and read back")
    rq = client.write_coil(0, True, unit=UNIT)
    rr = client.read_coils(0, 1, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    assert(rr.bits[0] == True)          # test the expected value

    log.debug("Write to multiple coils and read back- test 1")
    rq = client.write_coils(1, [True]*8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    rr = client.read_coils(1, 21, unit=UNIT)
    assert(not rr.isError())     # test that we are not an error
    resp = [True]*21

    resp.extend([False]*3)
    assert(rr.bits == resp)         # test the expected value

    log.debug("Write to multiple coils and read back - test 2")
    rq = client.write_coils(1, [False]*8, unit=UNIT)
    rr = client.read_coils(1, 8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    assert(rr.bits == [False]*8)         # test the expected value

    log.debug("Read discrete inputs")
    rr = client.read_discrete_inputs(0, 8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error

    log.debug("Write to a holding register and read back")
    rq = client.write_register(1, 10, unit=UNIT)
    rr = client.read_holding_registers(1, 1, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    assert(rr.registers[0] == 10)       # test the expected value

    log.debug("Write to multiple holding registers and read back")
    rq = client.write_registers(1, [10]*8, unit=UNIT)
    rr = client.read_holding_registers(1, 8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    assert(rr.registers == [10]*8)      # test the expected value

    log.debug("Read input registers")
    rr = client.read_input_registers(1, 8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error

    arguments = {
        'read_address':    1,
        'read_count':      8,
        'write_address':   1,
        'write_registers': [20]*8,
    }
    log.debug("Read write registeres simulataneously")
    rq = client.readwrite_registers(unit=UNIT, **arguments)
    rr = client.read_holding_registers(1, 8, unit=UNIT)
    assert(not rq.isError())     # test that we are not an error
    assert(rq.registers == [20]*8)      # test the expected value
    assert(rr.registers == [20]*8)      # test the expected value

    client.close()


if __name__ == "__main__":
    run_sync_client()

Docker image starts ok, but when I try to run the client script, I get an assertion error, which is different from failed-to-connect error, when no container is running.

2019-11-11 16:57:36,456 MainThread      DEBUG    client         :83       Reading Coils
2019-11-11 16:57:36,456 MainThread      DEBUG    transaction    :115      Current transaction state - IDLE
2019-11-11 16:57:36,456 MainThread      DEBUG    transaction    :120      Running transaction 1
2019-11-11 16:57:36,457 MainThread      DEBUG    transaction    :219      SEND: 0x0 0x1 0x0 0x0 0x0 0x6 0x1 0x1 0x0 0x1 0x0 0x1
2019-11-11 16:57:36,457 MainThread      DEBUG    sync           :75       New Transaction state 'SENDING'
2019-11-11 16:57:36,457 MainThread      DEBUG    transaction    :228      Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2019-11-11 16:57:41,460 MainThread      DEBUG    transaction    :238      Transaction failed. (Modbus Error: [Invalid Message] Incomplete message received, expected at least 8 bytes (0 received)) 
2019-11-11 16:57:41,460 MainThread      DEBUG    socket_framer  :147      Processing: 
2019-11-11 16:57:41,460 MainThread      DEBUG    transaction    :394      Getting transaction 1
2019-11-11 16:57:41,460 MainThread      DEBUG    transaction    :193      Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
2019-11-11 16:57:41,460 MainThread      DEBUG    client         :85       Modbus Error: [Input/Output] Modbus Error: [Invalid Message] Incomplete message received, expected at least 8 bytes (0 received)
2019-11-11 16:57:41,460 MainThread      DEBUG    client         :100      Write to a Coil and read back
2019-11-11 16:57:41,461 MainThread      DEBUG    transaction    :115      Current transaction state - TRANSACTION_COMPLETE
2019-11-11 16:57:41,461 MainThread      DEBUG    transaction    :120      Running transaction 2
2019-11-11 16:57:41,461 MainThread      DEBUG    transaction    :219      SEND: 0x0 0x2 0x0 0x0 0x0 0x6 0x1 0x5 0x0 0x0 0xff 0x0
2019-11-11 16:57:41,461 MainThread      DEBUG    sync           :75       New Transaction state 'SENDING'
2019-11-11 16:57:41,461 MainThread      DEBUG    transaction    :228      Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :304      Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :233      RECV: 
2019-11-11 16:57:45,001 MainThread      DEBUG    socket_framer  :147      Processing: 
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :394      Getting transaction 2
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :193      Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :115      Current transaction state - TRANSACTION_COMPLETE
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :120      Running transaction 3
2019-11-11 16:57:45,001 MainThread      DEBUG    transaction    :219      SEND: 0x0 0x3 0x0 0x0 0x0 0x6 0x1 0x1 0x0 0x0 0x0 0x1
2019-11-11 16:57:45,001 MainThread      DEBUG    sync           :75       New Transaction state 'SENDING'
2019-11-11 16:57:45,002 MainThread      DEBUG    transaction    :238      Transaction failed. ([Errno 32] Broken pipe) 
2019-11-11 16:57:45,002 MainThread      DEBUG    socket_framer  :147      Processing: 
2019-11-11 16:57:45,002 MainThread      DEBUG    transaction    :394      Getting transaction 3
2019-11-11 16:57:45,002 MainThread      DEBUG    transaction    :193      Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Traceback (most recent call last):
  File "client.py", line 166, in <module>
    run_sync_client()
  File "client.py", line 103, in run_sync_client
    assert(not rq.isError())     # test that we are not an error
AssertionError

I also tried to make the container use the host interface (with network-mode: host) and that works just ok. Therefore I believe I've got some mistakes in network setup of my container. From what I can piece together, docker container does listen on the specified port, but my script is not?

I would be glad to know where my mistake is and possibly how to debug these things, i've tried to found something in tcpdump and docker network inspect, but I couldn't find anything relevant.

Matěj Groman
  • 181
  • 1
  • 6

2 Answers2

1

Ok, after some time I realized, that I was redeploying docker container from the same build - so no changes I made to the server script IP was not actually made, because docker always used the first build. Now I get why I should use volume mount instead of copying files.

I've rebuilt docker image with server IP set to 0.0.0.0 (listen on all IPs), and it's working just as expected. Hope at least this will help somebody save some hours.

Matěj Groman
  • 181
  • 1
  • 6
0

At first, you shouldn't be used the localhost (127.0.0.1) IP in the client connection to the server, because when you are using a dockerized server, its IP no longer is localhost. So you have two choices:

  • If you are using the same machine/host for both client and server, you have to use to docker container IP (How to get the docker container IP) or the host IP

  • Else, you have to use the only host IP.


[NOTE]:

  • Your docker-compose file should be as below:

    version: '3.4'
    
    services:
      server:
        build: ./server
        container_name: modbus_server
        network_mode: host
        ports:
          - 5020:5020
    

    Do not need to expose attribute.

  • Also, if both of your server and client be dockerized, and they are relevant to each one in the docker-compose configuration (using links: attribute), you could use the Modbus server container name (I have chosen modbus_server in above) instead of IP in Modbus client side.

Benyamin Jafari
  • 27,880
  • 26
  • 135
  • 150
  • Thank you for your clear answer. This time it was my "thinko" as I was not rebuilding docker container, but the localhost IP could be a problem too. My friend looked at it with me, and we fixed a lot of these mistakes, in fact, now I have a functional Modbus server which can even manage the physical state of my raspberry. – Matěj Groman Dec 16 '19 at 15:32