Git Product home page Git Product logo

umodbus's Introduction

https://coveralls.io/repos/AdvancedClimateSystems/uModbus/badge.svg?service=github

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 argparse import ArgumentParser

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

# Parse command line arguments
parser = ArgumentParser()
parser.add_argument("-b", "--bind", default="localhost:502")

args = parser.parse_args()
if ":" not in args.bind:
    args.bind += ":502"
host, port = args.bind.rsplit(":", 1)
port = int(port)

TCPServer.allow_reuse_address = True
try:
    app = get_server(TCPServer, (host, port), RequestHandler)
except PermissionError:
    print("You don't have permission to bind on {}".format(args.bind))
    print("Hint: try with a different port (ex: --bind localhost:50200)")
    exit(1)

@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()

Doing a Modbus request requires even less code:

#!/usr/bin/env python
# scripts/examples/simple_tcp_client.py
from argparse import ArgumentParser
from socket import create_connection

from umodbus import conf
from umodbus.client import tcp

# Enable values to be signed (default is False).
conf.SIGNED_VALUES = True

# Parse command line arguments
parser = ArgumentParser()
parser.add_argument("-a", "--address", default="localhost:502")

args = parser.parse_args()
if ":" not in args.address:
    args.address += ":502"
host, port = args.address.rsplit(":", 1)
port = int(port)

# 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])

with create_connection((host, port)) as sock:
    # 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)

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.

umodbus's People

Contributors

acolomb avatar appeltabak avatar drummonds avatar greg0pearce avatar jaapz avatar lutostag avatar orangetux avatar rgov avatar tiagocoutinho avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

umodbus's Issues

start client

I can't start the client example.

Python 3.7.0 (default, Sep 15 2018, 19:13:07)
[GCC 8.2.1 20180831] on linux

[dev@dev-vm modbus]$ python modbus_client.py 
Traceback (most recent call last):
  File "modbus_client.py", line 18, in <module>
    response = tcp.send_message(message, sock)
  File "/usr/lib/python3.7/site-packages/umodbus/client/tcp.py", line 262, in send_message
    raise_for_exception_adu(response_error_adu)
  File "/usr/lib/python3.7/site-packages/umodbus/client/tcp.py", line 247, in raise_for_exception_adu
    pdu_to_function_code_or_raise_error(resp_pdu)
  File "/usr/lib/python3.7/site-packages/umodbus/functions.py", line 111, in pdu_to_function_code_or_raise_error
    raise error_code_to_exception_map[error_code]
umodbus.exceptions.IllegalDataAddressError:  The data address received in de request is not an allowable address for
    the server.

How can I fix it?

Modify `umodbus.server.RequestHandler` so this method 'handle' can overriden easily.

Currently the method RequestHandler.handle() reads from the socket, does all request handling logic and writes a response back to the socket. Subclassing this class and in particular the handle() function is hard.

Imagine I want to log all requests and the time a request took to be handled. Currently this is not possible without reimplemting the logic in RequestHandler.handle() because a request can only be read once.

The RequestHandler class should adhire to this interface:

class RequestHandler(BaseRequestHandler):
    def handle(self):
        """ Handle incoming request. It reads data from socket, calls RequestHandler.process() with data
        and passes response value to RequestHandler.respond().
       """
       req = self.request.recv(1024)
       resp = self.process(req)
       self.respond(resp) 

    def process(self, request):
         """" Process request and return response. """"
         # Do process handling logic.
         pass

    def write_response(self, resp):
        """ Send response back to client. """
        self.request.sendall(resp)

Now I can create a custom request handler which logs request message en handling time.

import time
from umodbus import log

class MyCustomHandler(RequestHandler):
    def handle(self):
        start_time = time.now()
        req = self.request.recv(1024)
        resp = self.process(req)
        self.respond(resp) 
        total_time = time.now() - start_time
        log.info('Request {0} took {1} seconds to handle.'.format(req, total_time))

Creating Modbus Server Communication with Client

I want to create a modbus server (with local host : ip address : 152.168.96.11 - same as the system) and the modbus client has (ip address : 152.168.96.32). My client application is running and i am creating the modbus server application with the umodbus server application. Data exchange of 32 bits (either read or write for testing purpose).
Slave Id : 1 and Port : 255

How do i configure python umodbus server with server able to read and write data into the client ip address

Here is the umodbus server application -

#!/usr/bin/env python
# scripts/examples/simple_tcp_server.py
import random
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

# Create Random Values
rndata = []
for i in range(32):
    rndata.append(random.randint(1,32))

# 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, ('152.168.96.11', 255), RequestHandler)


@app.route(slave_ids=[1], function_codes=[3, 4], addresses=list(range(0, 32)))
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, 32)))
def write_data_store(slave_id, function_code, address, value):
    """" Set value for address. """
    data_store[address] = value

# Configuring the Data
write_data_store(1,16,3,784)
write_data_store(1,16,24,678)
rdata1 = read_data_store(1,4,3)
print(rdata1)
rdata2 = read_data_store(1,4,24)
print(rdata2)

if __name__ == '__main__':
    try:
        app.serve_forever()
        print(rdata1)
    finally:
        app.shutdown()
        app.server_close()

unspected crc error

run as rtu serve
when writing to 233 station, 33000
write a data(<0,eg:-100) to addr 33000 (larger than 2^15), will surely cause "salve failure error"
all code as below:


import threading
import serial
from collections import defaultdict
from umodbus.server.serial import get_server
from umodbus.server.serial.rtu import RTUServer
import threadstop

running = threading.Thread()
comName = 'com31'
port = serial.Serial(comName)
port.timeout = 10
port.baudrate = 9600
port.stopbits = 1
port.parity = 'N'

data_rd = defaultdict(int)
data_wr = defaultdict(int)
link = get_server(RTUServer, port)


@link.route(slave_ids=[233], function_codes=[3], addresses=list(range(33000, 33000 + 1000)))
def read_data_store(slave_id, function_code, address):
    """" Return value of address. """
    # return data_store[address]
    return data_rd[address] + address


@link.route(slave_ids=[233], function_codes=[6, 16], addresses=list(range(33000, 33000 + 1000)))
def write_data_store(slave_id, function_code, address, value):
    """" Set value for address. """
    data_rd[address] = value  #OK
    data_wr[address] = value #ERROR

def poll():
    try:
        link.serve_forever()
    finally:
        link.shutdown()

def connect(comx):
    running = threading.Thread(target=poll)
    running.start()


def reconnect(comx):
    try:
        threadstop(running)
        link.shutdown()
    finally:
        comName = comx
        port = serial.Serial(comName)
        link.shutdown()



if __name__ == '__main__':
    try:
        link.serve_forever()
    finally:
        link.shutdown()

Implement Modbus TCP slave.

Currently uModbus only implements a Modbus server (or Modbus slave). A Modbus client (or Modbus master) would be nice.

`RequestHandler.process()` should not catch exceptions.

It's currently hard to catch exceptions raised during processing a Modbus request in a subclass of RequestHandler.process() because RequestHandler.process() catches all exceptions.

It would be better to create a method RequestHandler.process_exceptions which takes the exception and the request ADU and returns the reponse ADU.

RequestHandler(BaseRequestHandler):
  def handle(self):
    try:
      req_adu = socket.recv()
      resp_adu = self.process(req_adu)
    except Exception as e: 
      resp_adu =  self.process_exception(e, req_adu)

    self.respond(resp_adu)

  def process_exception(self, e, req_adu):
    """ Based on error occured return correct response ADU. """
    pass

uModbus should close connection when client closes it.

uModbus closes connection after a request has been processed. uModbus shouldn't close connection after each request, but only when client closes connection. This way clients are able to send multiple message using the same connection.

Unable to set socket options.

After some research it turned out that the OS doesn't always free the socket.

Running an example several times with too small delay between executions, could lead to this error:

OSError: [Errno 98] Address already in use

This is because the previous execution has left the socket in a TIME_WAIT state, and can’t be immediately reused.

There is a socket flag to set, in order to prevent this, socket.SO_REUSEADDR:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))

the SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire.

Currenty uModbus doesn't allow easily to set socket options for the server. This should be changed.

rtu.send_message should not read more bytes than needed.

Currrently rtu.send_message() is to greedy when reading response from the serial port. uModbus will fail when a server appends a response with for example a few nul bytes. The function should not read more bytes than needed.

Often the length of the response can be calculated. For response on function codes 1, 2, 3, 4,15 and 16 the 2nd byte of the PDU contains the byte count. That is how many bytes are left in the PDU.

The response of function code 5 en 6 is of the same length as the request PDU.

In case of an error the response is only 2 bytes.

Modbus RTU over TCP

Hi,
Is there a way to send RTU messages through TCP?
I need this functionality because we usually convert all the serial devices with rs485/ethernet converter.

function_code is missing in route signature.

Image a route like this:

@app.route(slave_ids=[1], function_codes=[1, 2], addresses=[1, 2, 3, 4])
def read_coil(slave_id, address):
    """ Return value of digital input.  """
    return address % 2

The route cannot know the Modbus function code of the request. Routes should be called with function_code so the signature for read functions becomes this:

def read_route(slave_id, address, function_code)

And for write functions:

def write_rout(slave_id, address, value, function_code)

Route is called with wrong value when one write single coil with value 1.

Assume you've a server with this route:

@app.route(slave_ids=[1], function_codes=[5], addresses=list(range(0, 10)))
def write_data_store(slave_id, address, value):
    """" Set value of address. """
    print(value)
    data[address] = value

When one use write_single_coil(slave_id=1, address=1, value=1) the server calls with write_data_store(1, 1, 65280). This is wrong. It should call write_data_store(1, 1, 1).

Add easy to use client wrapper for doing Modbus RTU and Modbus TCP/IP requests.

It requires quite a lot of code to do a Modbus request, see below. This could be wrapped easily in a simple wrapper.

#!/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()

A solution should look something like this:

with tcp.Client('localhost', 502) as c:
    resp = c.read_holding_registers(slave_id=1,starting_address=1, quantity=3)

pip3 install

I get this error when trying to install on my odroid-xu4 (Ubuntu 18.04 LTS (GNU/Linux 4.14.37-135 armv7l, Python 3.6.5 (default, Apr 1 2018, 05:46:30) [GCC 7.3.0] on linux)

How can I fix it?

odroid@odroid:~/centopeia_firm$ pip3 install umodbus
Collecting umodbus
  Using cached https://files.pythonhosted.org/packages/9d/ef/a575ba0840854b937ab7e5c6c4b40c4491089e42114a9b4413e87a84b2a8/uModbus-1.0.2.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-ty56b9mk/umodbus/setup.py", line 12, in <module>
        long_description = open(os.path.join(cwd, 'README.rst'), 'r').read()
      File "/usr/lib/python3.6/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 547: ordinal not in range(128)

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-ty56b9mk/umodbus/

Clean up Config class

The Config class is a bit verbose. It should only have the public attributes: TYPE_CHAR, SIGNED_VALUES and BIT_SIZE.

Transaction ID overflow

Firstly, fantastic job Auke.

When I run some dummy code

#!/usr/bin/env python
# scripts/examples/simple_tcp_client.py
import socket
import sys
from umodbus import conf
from umodbus.client import tcp

conf.SIGNED_VALUES = True
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('10.10.1.8', 502))

i=0
while True:
    try:
        i+=1
        message = tcp.read_discrete_inputs(slave_id=1, starting_address=0,quantity=1)
        response = tcp.send_message(message, sock)
    except:
        print("Unexpected error:", sys.exc_info())
        print(i)

I receive error

File "C:\Python34\lib\site-packages\umodbus\client\tcp.py", line 106, in _create_mbap_header
return struct.pack('>HHHB', transaction_id, 0, length, slave_id)
struct.error: 'H' format requires 0 <= number <= 65535

I think uModbus/umodbus/client/tcp.py Line 103 be (2^16)-1=65535 to fit in 2 bytes

#65536 = 2**16 aka maximum number that fits in 2 bytes.
transaction_id = randint(0, **65536**)
length = len(pdu) + 1

Best Regards,
Greg Pearce

If RTU response is empty, don't validate CRC but fail earlier.

Hi -- I fired this up really quick to see if I could use it in a home-rolled program, and correct me if I'm wrong, but there doesn't seem to be any error checking (specifically around validate_crc()) on the responses, right?

I'm firing a write_multiple_registers command with no RS485 devices on the serial bus. Output is fine, I guess, but my simple driver program just craps out because of an exception in validate_crc().

Can you confirm? Also, good luck on further developing this. I like the approach!

Allow clients to select format character themselves based on signess of value.

Currently you've to configure whether values are signed or unsigned.
The following code results in an IllegalValueError:

conf.SIGNED_VALUES = False

with tcp.Client() as c:
    # Raises IllegalValueError
    c.write_single_register(slave_id=1, address=1, value=-5)

I new option should be introducted: conf.DYNAMIC_SIGNESS. This value, False by default, allows a a client to determine the type character based on the signess of the value that is being written.

conf.DYNAMIC_TYPE_CHAR = True

with tcp.Client() as c:
    # In modbus.functions this call is done: struct.pack('>h', -5)
    c.write_single_register(slave_id=1, address=1, value=-5)

    # In umodbus.functions this call is done: struct.pack('>H', 5)
     c.write_single_register(slave_id=1, address=1, value=-5)

Merge logic in uModbus.functions and uModbus._functions.

uModbus.functions implements logic for a Modbus TCP server. This is not extendable and implementing same logic for a Modbus RTU or Modbus ASCII server is difficult.

uModbus._functions is not tied to Modbus TCP, RTU or ASCII and can be used to implement logic for all 3 types of Modbus. Currently this module can only by used by Modbus clients. Both modules should be merged to create 1 general module which is base for all 3 Modbus implementations.

Possibility to use GPIO driving DE/RE Pin

Hi There,
On some RS485 hardware, sometimes, the DE/RE pins are tied together to a GPIO pin. In this case we need to drive the GPIO pin in the driver when sending/receiving.

Plenty of example here:
https://github.com/madleech/Auto485
https://github.com/swarkn/MAX485
https://github.com/andresarmento/modbus-arduino

Sample Code of libraries are really simple, they check if we defined pin for RE/DE, and if so, setup the pin to High before each write on serial. After write, we flush serial, then wait protocol delay (t35 or t15 depending on speed) and then set back pin to Low.

Do you think it's do able simply in your library ?

Simple driver example works like that

bool ModbusSerial::config(HardwareSerial* port, long baud,  SerialConfig format, int txPin) 
{
    this->_port = port;
    this->_txPin = txPin;

 
    if (txPin >= 0) {
        pinMode(txPin, OUTPUT);
        digitalWrite(txPin, LOW);
    }

    if (baud > 19200) {
        _t15 = 750;
        _t35 = 1750;
    } else {
        _t15 = 15000000/baud; // 1T * 1.5 = T1.5
        _t35 = 35000000/baud; // 1T * 3.5 = T3.5
    }
    return true;
}

bool ModbusSerial::send(byte* frame) {
    byte i;

    if (this->_txPin >= 0) {
        digitalWrite(this->_txPin, HIGH);
        delayMicroseconds(1000);
    }

    for (i = 0 ; i < _len ; i++) {
        (*_port).write(frame[i]);
    }

    (*_port).flush();
    delayMicroseconds(_t35);

    if (this->_txPin >= 0) {
        digitalWrite(this->_txPin, LOW);
    }
}

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.