Quickstart

We showed you sample MSM client (master) usage at the main page. However, to run such code properly, you first need a working modbus server (slave) at your localhost. Because of that, we will first create a sample modbus server with some neet memory mapping. Next, we will create a client talking to the server.

The code below will:
  • allocate 16 bytes of memory
  • define some variables on this memory
  • start modbus TCP server worker
  • present those variables in cli

ex_server_mapper.py

from ModbusSharedMemory.client_server import ModbusSlaveTCP
from ModbusSharedMemory.memory import MemoryStore, MemoryVariable
from textwrap import dedent
from random import choice as random_choice
import os, platform
from time import sleep

def clear():
    return os.system('cls' if platform.system() == 'Windows' else 'clear')


# declare memory, 8 words = 16 bytes
mem = MemoryStore(8)

# declare some mappings
# name it as u wish
# e.g. PLC like naming convention

# those variables lays in memory in following places: 
# ( "_" indicates variable place, 
#  "(WW)" represents one word, 
# "bx" represents x-th bit of that word): 

#  _
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.WORK_MODE = MemoryVariable.byte(address=0, byte_number=0) # 8 bit long

#   _
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.PROCESS_STEP = MemoryVariable.byte(address=0, byte_number=1) # 8 bit long

#      __  __
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.CURRENT_VALUE = MemoryVariable.uint32(address=1) # this one takes 32 bits = 4 bytes = 2 words, 
                                                     # declare next variable at add>=3 or they 
                                                     # will overlap

#              __
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.CONTROL_WORD = MemoryVariable.word(address=3) # this takes 16 bits = 2bytes = 1 word

#              b0
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.ERROR_STATE = MemoryVariable.bool(address=3, bit_number=0) # we can overlap by intention, 
                                                               # playing with word bits

# enough, start the server
server = ModbusSlaveTCP(mem) # new server
server.run()                 # run at another thread

if __name__ == "__main__":
    # simple debug screen
    run = True
    while run:
        try:
            clear()
            fmt = dedent("""\
                WORK_MODE: {0},
                PROCESS_STEP: {1},
                CURRENT_VALUE: {2},
                CONTROL_WORD: {3},
                ERROR_STATE: {4}""")
            print(fmt.format(
                mem.WORK_MODE, 
                mem.PROCESS_STEP,
                mem.CURRENT_VALUE,
                mem.CONTROL_WORD,
                mem.ERROR_STATE))
            
            # we can also modify some values, client will see this changes, it is that simple
            mem.ERROR_STATE = random_choice([True, False])
            sleep(0.5)
        except KeyboardInterrupt:
            run = False

    server.kill()
Whole advantage of using MSM as your modbus backend comes from two things:
  • You are not using modbus functions directly, instead you use memory variables like ordinary python variables
  • You dont bother yourself with serving modbus server/client by yourself, instead, you just instantiating it and tells it to run

Server code will pretty print declared memory variables to show its content. Especially it will show changes while you will be altering them as a client, and it will periodically alter ERROR_STATE variable to show you that you can see this changes from the client side. Lets now write some code for the client:

ex_client_mapper.py

from ModbusSharedMemory.client_server import ModbusMasterTCP
from ModbusSharedMemory.memory import MemoryStore, MemoryVariable
from time import sleep
from textwrap import dedent


def print_variables(memory):
    fmt = dedent("""\
                WORK_MODE: {0},
                PROCESS_STEP: {1},
                CURRENT_VALUE: {2},
                CONTROL_WORD: {3},
                ERROR_STATE: {4}\n""")
    print(fmt.format(
        memory.WORK_MODE, 
        memory.PROCESS_STEP,
        memory.CURRENT_VALUE,
        memory.CONTROL_WORD,
        memory.ERROR_STATE))


# declare mappings, preferd is to have same mapping on server and client side
# but it is your choice. Excange is done by addresses.

# declare memory, 8 words = 16 bytes
mem = MemoryStore(8)

# declare some mappings
# name it as u wish
# e.g. PLC like naming convention

# those variables lays in memory in following places: 
# ( "_" indicates variable place, 
#  "(WW)" represents one word, 
#  "bx" represents x-th bit of that word): 

#  _
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.WORK_MODE = MemoryVariable.byte(address=0, byte_number=0) # 8 bit long

#   _
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.PROCESS_STEP = MemoryVariable.byte(address=0, byte_number=1) # 8 bit long

#      __  __
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.CURRENT_VALUE = MemoryVariable.uint32(address=1) # this one takes 32 bits = 4 bytes = 2 words, 
                                                     # declare next variable at add>=3 or they 
                                                     # will overlap

#              __
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.CONTROL_WORD = MemoryVariable.word(address=3) # this takes 16 bits = 2bytes = 1 word

#              b0
# (WW)(WW)(WW)(WW)(WW)(WW)(WW)(WW)
mem.ERROR_STATE = MemoryVariable.bool(address=3, bit_number=0) # we can overlap by intention, 
                                                               # playing with word bits

# enough, declare client
client = ModbusMasterTCP(mem)
client.run() # run client exchange service in another thread

# now do what u want with memory
# if u import this file, u can play with variables, remember to kill client at the end! 
# client.kill()
# if u run this file, program will do some actions procedural actions

if __name__ == '__main__':
    mem.WORK_MODE = 4
    print_variables(mem)
    sleep(2)

    mem.PROCESS_STEP = 10
    print_variables(mem)
    sleep(2)

    mem.CURRENT_VALUE = 99999
    print_variables(mem)
    sleep(2)

    mem.CONTROL_WORD = 0b1000000000000000
    print_variables(mem)
    sleep(2)

    client.kill()

Now you can run server code in terminal window. It will run until you terminate it (CTRL+C). Next, in another terminal, you can run a client code. You should see how values are changing on server side, just by writing memory variables on the client side. You can terminate client application by pressing CTRL+C.

Also, you can import client code to play with memory variables by yourself. Open python interactive shell:

>>> from ex_client_mapper import *
>>>
>>> # read ERROR_STATE variable
>>> print(mem.ERROR_STATE)
True
>>> # read it again and you will likely see other value
>>> # because server constanly alters this variable
>>> print(mem.ERROR_STATE)
False
>>> # write a value to CONTROL_WORD and watch server side
>>> mem.CONTROL_WORD = 25
>>>
>>> # kill the worker when you are done
>>> client.kill()
>>> exit()

Now, when you are more familiar with MSM, we can emphasize some notes:

  1. Memory variables are customizable You declare them on MemoryStore instance using special methods of MemoryVariable class, and you can give them any name:

    mem.SOME_REALY_STRANGE_NAME = MemoryVariable.byte(address=0, byte_number=0)
    
  2. Memory variables have addresses It mean that you can declare two diferent variables, that will overlay. Changing the one will impact the other. Doing so sounds strange, but it sometimes has a reason.

    >>> mem.ONE_VARIABLE = MemoryVariable.byte(address=1, byte_number=0)
    >>> mem.OTHER_VARIABLE = MemoryVariable.byte(address=1, byte_number=0)
    >>> mem.ONE_VARIABLE = 10
    >>> print(mem.OTHER_VARIABLE)
    10
    

    Those above share their address, one is always equal to other and vice versa.

  3. It is memory what is exchanged, not variables Variables only reflects state of memory. You can declare two variables on one address, or do not declare variables at all. Memory still will be exchanged. You can even declare variable later on, and it will still reflect memory state it’s pointing on.

  4. Memory space is Word-numbered Modbus protocol defines holding registers to store 16-bit values. Since we want to be consistent with this standard, all addresses are considered to point on 16-bit values (Words).

  5. Client and server runs in separate threads We decided to delegate client/server workers in threads. Those are daemon threds and are started when .run() method is called. They should be properly closed, .kill() method serves that purpose. Care to call it when you are done with modbus communication.

You see all this facts in given examples. Both: server and client declares exactly the same memory mapping, but they could do it in a different way - however this would mess up code logic, but it will work. It is memory what is exchanged, not variables. Also, comments in code presents you that memory space is word-numbered, every register holds 16-bit value. All of this registers have address.