uModbus¶
uModbus or (μModbus) is a pure Python implementation of the Modbus protocol as described in the MODBUS Application Protocol Specification V1.1b3. uModbus implements both a Modbus client (both TCP and RTU) and a Modbus server (both TCP and RTU). The “u” or “μ” in the name comes from the the SI prefix “micro-“. uModbus is very small and lightweight. The source can be found on GitHub. Documentation is available at Read the Docs.
Quickstart¶
Creating a Modbus TCP server is easy:
#!/usr/bin/env python
# scripts/examples/simple_tcp_server.py
import logging
from socketserver import TCPServer
from collections import defaultdict
from umodbus import conf
from umodbus.server.tcp import RequestHandler, get_server
from umodbus.utils import log_to_stream
# Add stream handler to logger 'uModbus'.
log_to_stream(level=logging.DEBUG)
# A very simple data store which maps addresss against their values.
data_store = defaultdict(int)
# Enable values to be signed (default is False).
conf.SIGNED_VALUES = True
TCPServer.allow_reuse_address = True
app = get_server(TCPServer, ('localhost', 502), RequestHandler)
@app.route(slave_ids=[1], function_codes=[3, 4], addresses=list(range(0, 10)))
def read_data_store(slave_id, function_code, address):
"""" Return value of address. """
return data_store[address]
@app.route(slave_ids=[1], function_codes=[6, 16], addresses=list(range(0, 10)))
def write_data_store(slave_id, function_code, address, value):
"""" Set value for address. """
data_store[address] = value
if __name__ == '__main__':
try:
app.serve_forever()
finally:
app.shutdown()
app.server_close()
Doing a Modbus request requires even less code:
#!/usr/bin/env python
# scripts/examples/simple_tcp_client.py
import socket
from umodbus import conf
from umodbus.client import tcp
# Enable values to be signed (default is False).
conf.SIGNED_VALUES = True
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 502))
# Returns a message or Application Data Unit (ADU) specific for doing
# Modbus TCP/IP.
message = tcp.write_multiple_coils(slave_id=1, starting_address=1, values=[1, 0, 1, 1])
# Response depends on Modbus function code. This particular returns the
# amount of coils written, in this case it is.
response = tcp.send_message(message, sock)
sock.close()
Features¶
The following functions have been implemented for Modbus TCP and Modbus RTU:
- 01: Read Coils
- 02: Read Discrete Inputs
- 03: Read Holding Registers
- 04: Read Input Registers
- 05: Write Single Coil
- 06: Write Single Register
- 15: Write Multiple Coils
- 16: Write Multiple Registers
Other featues:
- Support for signed and unsigned register values.
License¶
uModbus software is licensed under Mozilla Public License. © 2018 Advanced Climate Systems.
How uModus works¶
Installation¶
uModbus has been tested on Python 2.7 and Python 3.3+.
As package¶
uModbus is available on Pypi and can be installed through Pip:
$ pip install umodbus
Or you can install from source using setup.py:
$ python setup.py install
For development, debugging and testing¶
uModbus has no runtime dependencies. However to run the the tests or build the documentation some dependencies are required. They are listed in dev_requirements.txt and can be installed through Pip:
$ pip install -r dev_requirements.txt
Now you can build the docs:
$ sphinx-build -b html docs/source docs/build
Or run the tests:
$ py.test tests
Modbus Server¶
Viewpoint¶
The uModbus server code is built with routing in mind. Routing (groups of) requests to a certain callback is easy. This is in contrast with with other Modbus implementation which often focus on reading and writing from a data store.
Because of this difference in viewpoint uModbus doesn’t know the concept of Modbus’ data models like discrete inputs, coils, input registers, holding registers and their read/write properties.
Routing¶
The routing system was inspired by Flask. Like Flask, uModbus requires a global app or server. This server contains a route map. Routes can be added to the route map.
The following code example demonstrates how to implement a very simple data store for 10 addresses.
Modbus TCP example¶
#!/usr/bin/env python
# scripts/examples/simple_data_store.py
import logging
from socketserver import TCPServer
from collections import defaultdict
from umodbus import conf
from umodbus.server.tcp import RequestHandler, get_server
from umodbus.utils import log_to_stream
# Add stream handler to logger 'uModbus'.
log_to_stream(level=logging.DEBUG)
# A very simple data store which maps addresses against their values.
data_store = defaultdict(int)
# Enable values to be signed (default is False).
conf.SIGNED_VALUES = True
TCPServer.allow_reuse_address = True
app = get_server(TCPServer, ('localhost', 502), RequestHandler)
@app.route(slave_ids=[1], function_codes=[1, 2], addresses=list(range(0, 10)))
def read_data_store(slave_id, function_code, address):
"""" Return value of address. """
return data_store[address]
@app.route(slave_ids=[1], function_codes=[5, 15], addresses=list(range(0, 10)))
def write_data_store(slave_id, function_code, address, value):
"""" Set value for address. """
data_store[address] = value
if __name__ == '__main__':
try:
app.serve_forever()
finally:
app.shutdown()
app.server_close()
Modbus RTU example¶
#!/usr/bin/env python
from serial import Serial
from collections import defaultdict
from umodbus.server.serial import get_server
from umodbus.server.serial.rtu import RTUServer
s = Serial('/dev/ttyS1')
s.timeout = 10
data_store = defaultdict(int)
app = get_server(RTUServer, s)
@app.route(slave_ids=[1], function_codes=[1, 2], addresses=list(range(0, 10)))
def read_data_store(slave_id, function_code, address):
"""" Return value of address. """
return data_store[address]
@app.route(slave_ids=[1], function_codes=[5, 15], addresses=list(range(0, 10)))
def write_data_store(slave_id, function_code, address, value):
"""" Set value for address. """
data_store[address] = value
if __name__ == '__main__':
try:
app.serve_forever()
finally:
app.shutdown()
Modbus Client¶
uModbus contains a client for Modbus TCP and Modbus RTU.
Modbus TCP¶
Example¶
All function codes for Modbus TCP/IP are supported. You can use the client like this:
#!/usr/bin/env python
# scripts/examples/simple_tcp_client.py
import socket
from umodbus import conf
from umodbus.client import tcp
# Enable values to be signed (default is False).
conf.SIGNED_VALUES = True
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 502))
# Returns a message or Application Data Unit (ADU) specific for doing
# Modbus TCP/IP.
message = tcp.write_multiple_coils(slave_id=1, starting_address=1, values=[1, 0, 1, 1])
# Response depends on Modbus function code. This particular returns the
# amount of coils written, in this case it is.
response = tcp.send_message(message, sock)
sock.close()
API¶
-
umodbus.client.tcp.
send_message
(adu, sock)[source]¶ Send ADU over socket to to server and return parsed response.
Parameters: - adu – Request ADU.
- sock – Socket instance.
Returns: Parsed response from server.
-
umodbus.client.tcp.
parse_response_adu
(resp_adu, req_adu=None)[source]¶ Parse response ADU and return response data. Some functions require request ADU to fully understand request ADU.
Parameters: - resp_adu – Resonse ADU.
- req_adu – Request ADU, default None.
Returns: Response data.
-
umodbus.client.tcp.
read_coils
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 01: Read Coils.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.tcp.
read_discrete_inputs
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 02: Read Discrete Inputs.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.tcp.
read_holding_registers
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 03: Read Holding Registers.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.tcp.
read_input_registers
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 04: Read Input Registers.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.tcp.
write_single_coil
(slave_id, address, value)[source]¶ Return ADU for Modbus function code 05: Write Single Coil.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.tcp.
write_single_register
(slave_id, address, value)[source]¶ Return ADU for Modbus function code 06: Write Single Register.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
Modbus RTU¶
Example¶
Note
uModbus doesn’t support all the functions defined for Modbus RTU. It currently supports the following functions:
- 01: Read Coils
- 02: Read Discrete Inputs
- 03: Read Holding Registers
- 04: Read Input Registers
- 05: Write Single Coil
- 06: Write Single Register
- 15: Write Multiple Coils
- 16: Write Multiple Registers
#!/usr/bin/env python
# scripts/example/simple_rtu_client.py
import fcntl
import struct
from serial import Serial, PARITY_NONE
from umodbus.client.serial import rtu
def get_serial_port():
""" Return serial.Serial instance, ready to use for RS485."""
port = Serial(port='/dev/ttyS1', baudrate=9600, parity=PARITY_NONE,
stopbits=1, bytesize=8, timeout=1)
fh = port.fileno()
# A struct with configuration for serial port.
serial_rs485 = struct.pack('hhhhhhhh', 1, 0, 0, 0, 0, 0, 0, 0)
fcntl.ioctl(fh, 0x542F, serial_rs485)
return port
serial_port = get_serial_port()
# Returns a message or Application Data Unit (ADU) specific for doing
# Modbus RTU.
message = rtu.write_multiple_coils(slave_id=1, address=1, values=[1, 0, 1, 1])
# Response depends on Modbus function code. This particular returns the
# amount of coils written, in this case it is.
response = rtu.send_message(message, serial_port)
serial_port.close()
API¶
-
umodbus.client.serial.rtu.
send_message
(adu, serial_port)[source]¶ Send ADU over serial to to server and return parsed response.
Parameters: - adu – Request ADU.
- sock – Serial port instance.
Returns: Parsed response from server.
-
umodbus.client.serial.rtu.
parse_response_adu
(resp_adu, req_adu=None)[source]¶ Parse response ADU and return response data. Some functions require request ADU to fully understand request ADU.
Parameters: - resp_adu – Resonse ADU.
- req_adu – Request ADU, default None.
Returns: Response data.
-
umodbus.client.serial.rtu.
read_coils
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 01: Read Coils.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.serial.rtu.
read_discrete_inputs
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 02: Read Discrete Inputs.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.serial.rtu.
read_holding_registers
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 03: Read Holding Registers.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.serial.rtu.
read_input_registers
(slave_id, starting_address, quantity)[source]¶ Return ADU for Modbus function code 04: Read Input Registers.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.serial.rtu.
write_single_coil
(slave_id, address, value)[source]¶ Return ADU for Modbus function code 05: Write Single Coil.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
-
umodbus.client.serial.rtu.
write_single_register
(slave_id, address, value)[source]¶ Return ADU for Modbus function code 06: Write Single Register.
Parameters: slave_id – Number of slave. Returns: Byte array with ADU.
Configuration¶
umodbus.conf
is a global configuration object and is an instance of
umodbus.config.Config. It can be used like this:
from umodbus import conf
conf.SIGNED_VALUES = True
Changelog¶
1.0.2 (2018-05-22)¶
I released uModbus 1.0.1 without updating the version number in setup.py. This releases fixes this.
0.8.1 (2016-11-02)¶
Bugs
0.7.2 (2016-09-27)¶
Bugs
- #44 Remove print statement.
- #46 Transaction ID overflow. Thanks `@greg0pearce`_
0.4.0 (2016-01-22)¶
Features
0.2.0 (2015-11-19)¶
Features
- #10 Support for signed values.
Bugs
- #13 Fix shutdown of server in simple_data_store.py
0.1.2 (2015-11-16)¶
Bugs
- #8 WriteMultipleCoils.create_from_request_pdu sometimes doesn’t unpack PDU correct.
0.1.0 (2015-11-10)¶
- First release.
The Modbus protocol explained¶
Decompose requests¶
Modbus requests and responses contain an Application Data Unit (ADU) which contains a Protocol Data Unit (PDU). The ADU is an envelope containing a message, the PDU is the message itself. Modbus requests can be sent via two communication layers, RTU or TCP/IP. The ADU for these layers differs. But the PDU, the message, always has the same strcuture, regardless of the way it’s transported.
PDU¶
Note
This section is based on MODBUS Application Protocol Specification V1.1b3
The Protocol Data Unit (PDU) is the request or response message and is indepedent of the underlying communication layer. This module only implements requests PDU’s.
A request PDU contains two parts: a function code and request data. A response PDU contains the function code from the request and response data. The general structure is listed in table below:
Field | Size (bytes) |
Function code | 1 |
data | N |
Below you see the request PDU with function code 1, requesting status of 3 coils, starting from coil 100.
>>> req_pdu = b'\x01