Giter VIP home page Giter VIP logo

micropython-modbus's Introduction

MicroPython Modbus library

Downloads Release MicroPython License: MIT CI Test Python package Documentation Status

MicroPython ModBus TCP and RTU library supporting client and host mode


General

Forked from Exo Sense Py, based on PyCom Modbus and extended with other functionalities to become a powerfull MicroPython library

📚 The latest documentation is available at MicroPython Modbus ReadTheDocs 📚

Quickstart

This is a quickstart to install the micropython-modbus library on a MicroPython board.

A more detailed guide of the development environment can be found in SETUP, further details about the usage can be found in USAGE, descriptions for testing can be found in TESTING and several examples in EXAMPLES

python3 -m venv .venv
source .venv/bin/activate

pip install 'rshell>=0.0.30,<1.0.0'
pip install 'mpremote>=0.4.0,<1'

Install package on board with mip or upip

rshell -p /dev/tty.SLAB_USBtoUART --editor nano

Inside the rshell open a REPL and execute these commands inside the REPL

import machine
import network
import time
import mip
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect('SSID', 'PASSWORD')
time.sleep(1)
print('Device connected to network: {}'.format(station.isconnected()))
mip.install('github:brainelectronics/micropython-modbus')
print('Installation completed')
machine.soft_reset()

For MicroPython versions below 1.19.1 use the upip package instead of mip

import machine
import network
import time
import upip
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect('SSID', 'PASSWORD')
time.sleep(1)
print('Device connected to network: {}'.format(station.isconnected()))
upip.install('micropython-modbus')
print('Installation completed')
machine.soft_reset()

Request coil status

After a successful installation of the package and reboot of the system as described in the installation section the following commands can be used to request a coil state of a target/client device. Further usage examples can be found in the examples folder and in the USAGE chapter

TCP

from ummodbus.tcp import ModbusTCPMaster

tcp_device = ModbusTCPMaster(
    slave_ip='172.24.0.2',  # IP address of the target/client/slave device
    slave_port=502,         # TCP port of the target/client/slave device
    # timeout=5.0           # optional, timeout in seconds, default 5.0
)

# address of the target/client/slave device on the bus
slave_addr = 10
coil_address = 123
coil_qty = 1

coil_status = host.read_coils(
    slave_addr=slave_addr,
    starting_addr=coil_address,
    coil_qty=coil_qty)
print('Status of coil {}: {}'.format(coil_status, coil_address))

For further details check the latest MicroPython Modbus TCP documentation example

RTU

from umodbus.serial import Serial as ModbusRTUMaster

host = ModbusRTUMaster(
    pins=(25, 26),      # given as tuple (TX, RX), check MicroPython port specific syntax
    # baudrate=9600,    # optional, default 9600
    # data_bits=8,      # optional, default 8
    # stop_bits=1,      # optional, default 1
    # parity=None,      # optional, default None
    # ctrl_pin=12,      # optional, control DE/RE
    # uart_id=1         # optional, see port specific documentation
)

# address of the target/client/slave device on the bus
slave_addr = 10
coil_address = 123
coil_qty = 1

coil_status = host.read_coils(
    slave_addr=slave_addr,
    starting_addr=coil_address,
    coil_qty=coil_qty)
print('Status of coil {}: {}'.format(coil_address, coil_status))

For further details check the latest MicroPython Modbus RTU documentation example

Install additional MicroPython packages

To use this package with the provided boot.py and main.py file, additional modules are required, which are not part of this repo/package. To install these modules on the device, connect to a network and install them via upip as follows

# with MicroPython version 1.19.1 or newer
import mip
mip.install('github:brainelectronics/micropython-modules')

# before MicroPython version 1.19.1
import upip
upip.install('micropython-brainelectronics-helpers')

Check also the README of the brainelectronics MicroPython modules, the INSTALLATION and the SETUP guides.

Usage

See USAGE and DOCUMENTATION

Supported Modbus functions

Refer to the following table for the list of supported Modbus functions.

ID  Description
1 Read coils
2 Read discrete inputs
3 Read holding registers
4 Read input registers
5 Write single coil
6 Write single register
15 Write multiple coils
16 Write multiple registers

Credits

Big thank you to giampiero7 for the initial implementation of this library.

micropython-modbus's People

Contributors

brainelectronics avatar giampiero7 avatar gimmickng avatar wpyoga avatar wthomson 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

micropython-modbus's Issues

Read/Write register at some location of definition

Found during #33, see #33 (comment) at section "Problem 3" and "Problem 5"

Defining a register on the MicroPython Modbus client device as e.g. (valid for HREGS, COILS, ISTS, IREGS)

{
    "HREGS": {
        "HREG_NAME": {
            "register": 93,
            "len": 3,
            "val": [29, 38, 0]
        }
    }
}

does not allow to read ONLY register 94 which would hold 38.

Reason/Root cause:
Registers are defined as specified by the register element, see

self._set_reg_in_dict(reg_type='COILS',
address=address,
value=value)
for a coil. The provided value is added as value to the dictionary at the key register, here 93. Thereby no element/key 94 exists in the Modbus _register_dict

Cannot run example code (RTU Slave)

Description

Hi,

I installed this library on my RP Pi Pico (H), micropython. I get an syntax error running the example code from the library (Slave RTU). The error exists in the serial.py sub code of the library line 2 of the example code : from umodbus.serial import ModbusRTU(Modbus). When I run serial.py I got an error about the costum packages. Do I miss any extra necessary libraries that are used next to the Micropython-modbus library? I did read other questions about running this library on a RP Pi Pico but it didn't answer my question / I didn't help me fix this problem.

Reproduction steps

This is the error I got when running the example code Slave RTU.

Traceback (most recent call last):
File "", line 2
SyntaxError: invalid syntax

this is the code I'm running:

from machine import Pin
from umodbus.serial import ModbusRTU(Modbus)

rtu_pins = (Pin(0), Pin(1)) # (TX, RX)
uart_id = 0

slave_addr = 1 # address on bus as client

client = ModbusRTU(
addr=slave_addr, # address on bus
pins=rtu_pins, # given as tuple (TX, RX)
baudrate=9600, # optional, default 9600
data_bits=8, # optional, default 8
stop_bits=1, # optional, default 1
parity=None, # optional, default None
ctrl_pin=18, # optional, control DE/RE
uart_id=uart_id # optional, default 1, see port specific documentation
)

register_definitions = {
"COILS": {
"EXAMPLE_COIL": {
"register": 123,
"len": 1,
"val": 1
}
},
"HREGS": {
"EXAMPLE_HREG": {
"register": 93,
"len": 1,
"val": 19
}
},
"ISTS": {
"EXAMPLE_ISTS": {
"register": 67,
"len": 1,
"val": 0
}
},
"IREGS": {
"EXAMPLE_IREG": {
"register": 10,
"len": 1,
"val": 60001

}

}}

MicroPython version

v.1.19.1

MicroPython board

Raspberry Pico

Relevant log output

No response

User code

No response

Additional informations

No response

Bug: error when trying to write multiple coils - `host.write_multiple_coils()`

This bug was extracted from PR #10 report, in this reply:

Error when trying to write multiple coils (host.write_multiple_coils())

I not found examples on the docs about how is the correct way to write multiple coils, so I get a example from here

>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]
>>>
>>> host.write_multiple_coils(slave_addr=10, starting_address=123, output_values=[1,1,1,0,0,1,1,1,0,0,1,1,1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 274, in write_multiple_coils
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
>>> host.write_multiple_coils(slave_addr=10, starting_address=123, output_values=[1,1,1,0,0,1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 274, in write_multiple_coils
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 

Coil quantity specification does not work

As found out during #10 the coil_qty parameter of the read_coils function of TCP and RTU is not working.

host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=1" but showed 8 and not just 1.
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=20" but still showing 8, and not 20.

Question: how to use the write_single_coil() to write on the different positions?

Hello @brainelectronics

I'm using the #10 for tests.

How is possible to use the write_single_coil() to write in position 3, for example? In this example below the ouput_value is setting always to the first position:

>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[False, True, True, False, False, False, False, False]
>>> host.write_single_coil(slave_addr=10, output_address=123, output_value=1)
False
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]
>>> 

Using this register_definitions:
{'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': 0}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 1, 'val': 19}}, 'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [0, 1, 1]}}}

Bug: `write_single_coil()` is settting all other coils to `False`

This bug was extracted from PR #10 report, in this reply:

I suppose that write_single_coil(), without use a list of coils, will write just on the first position, like as I asked on the #15

When I try to use write_single_coil() to write on the first position, all others coils positions are changed to False.

>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[False, True, True, False, False, False, False, False]   <---- See that is (False, True, True..) like as on the "definitions"
>>> host.write_single_coil(slave_addr=10, output_address=123, output_value=1)
False
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False] <---- Here all coils after first position was changed to "False"
>>> 

Using this register_definitions:
{'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': 0}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 1, 'val': 19}}, 'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [0, 1, 1]}}}

Bug: `register_qty` do not works on the `read_holding_registers()`

This bug was extracted from PR #10 report, in this reply:

The register_qty do not works on the read_holding_registers():

>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19,)
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
(19,) <-- I believe that here should be (19, 786) - register address 93 and 94
>>> host.read_holding_registers(slave_addr=10, starting_addr=94, register_qty=2, signed=False)
(786,)
>>> 

Using this register_definitions:
{'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [1, 1, 1]}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 4, 'val': 19}, 'EXAMPLE_HREG_2': {'register': 94, 'len': 2, 'val': 786}}, 'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': [1, 0, 0, 1]}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}}

Testing a different register_definitions model:

If I change the register_definitions to this:
{'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [1, 1, 1]}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 1, 'val': [19, 23494]}}, 'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': [1, 0, 0, 1]}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}}

I have this results:
*1. The register_qty still not works, but when I try to read, address 93, 94 came together
*2. When I try to read/write address 94, I have error
*3. When I try to write address 93, the value on address 94 is lost.

>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19, 23494) <-- (1) register_qty is 1, but came 2 address: the 93 and 94
>>> host.read_holding_registers(slave_addr=10, starting_addr=94, register_qty=1, signed=False) <-- (2) 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/tcp.py", line 118, in read_holding_registers
  File "umodbus/tcp.py", line 89, in _send_receive
  File "umodbus/tcp.py", line 74, in _validate_resp_hdr
ValueError: slave returned exception code: 131
>>> 
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19, 23494)
>>> 
>>> host.write_single_register(slave_addr=10, register_address=94, register_value=555, signed=False) <-- (2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/tcp.py", line 157, in write_single_register
  File "umodbus/tcp.py", line 89, in _send_receive
  File "umodbus/tcp.py", line 74, in _validate_resp_hdr
ValueError: slave returned exception code: 134
>>> host.write_single_register(slave_addr=10, register_address=93, register_value=15, signed=False)
True
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
(15,) <-- (3) Check que value 23494 was lost
>>> 

Well, in the second test I think that I broken the register_refinitions logic, but I do not know what is the correct rules for it. What is the correct usage on the register definitions?

Could you please to provide a simple Slave/Master TCP example that works?

Hi @brainelectronics

In the issue #8 I commented that Master (TCP and RTU) is not working (Classes do not exists), but the Slave is not working too. All right, the Slave code class exists and show nothing errors, but I can't get data from my Master TCP simulator.

Here the code of Slave TCP that I'm trying to do, following the docs in the README.md. Could you please tell me what I'm doing wrong?

My goal is to have a Slave (TCP and RTU) on the ESP32 board 1, and Master (TCP and RTU) on the ESP32 board 2, and both communicating over ModBus via WiFi and/or RS485.

# file mb_slave.py

import network, time
from umodbus import modbus

wlan = network.WLAN(network.STA_IF)
wlan.active(1)
wlan.connect('APX', 'test123456')
while True:
    time.sleep(2)
    print('Waiting for Wifi conenction...')
    print(wlan.ifconfig())
    if wlan.isconnected():
        print('Connected to Wifi.')
        break
time.sleep(2)

# TCP Slave setup
# act as client, provide Modbus data via TCP to a host device
local_ip = '192.168.1.186'    # IP address
tcp_port = 502              # port to listen to
slave_tcp = modbus.ModbusTCP()
is_bound = False

# check whether client has been bound to an IP and port
is_bound = slave_tcp.get_bound_status()
if not is_bound:
    slave_tcp.bind(local_ip=local_ip, local_port=tcp_port)

# commond slave register setup, to be used with the Master example above
register_definitions = {
    "COILS": {
        "EXAMPLE_COIL": {
            "register": 123,
            "len": 1,
        }
    },
    "HREGS": {
        "EXAMPLE_HREG": {
            "register": 93,
            "len": 1,
        }
    },
    "ISTS": {
        "EXAMPLE_ISTS": {
            "register": 67,
            "len": 1,
        }
    },
    "IREGS": {
        "EXAMPLE_IREG": {
            "register": 10,
            "len": 2,
        }
    }
}

slave_tcp.setup_registers(registers=register_definitions, use_default_vals=True)

Output:

$ mpremote run mb_slave.py 
Waiting for Wifi conenction...
('0.0.0.0', '0.0.0.0', '0.0.0.0', '192.168.1.5')
Waiting for Wifi conenction...
('192.168.1.186', '255.255.255.0', '192.168.1.31', '192.168.1.5')
Connected to Wifi.

I checked if the port 502 is really opened that IP (192.168.1.186) using nmap, and it is opened:

# nmap -p 502 192.168.1.186
Starting Nmap 7.80 ( https://nmap.org ) at 2022-06-27 17:17 -03
Nmap scan report for 192.168.1.186
Host is up (0.093s latency).

PORT    STATE SERVICE
502/tcp open  mbap
MAC Address: 7C:DF:A1:E0:65:70 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 4.83 seconds

Thank you very much for any help!

Trying to implement TCP client on RPi Pico with ETH HAT

Dear all,
I'm trying to realize a cheap solution for dimming LEDs with the onboard PWM.
This will be controlled by a PLC over ModbusTCP but I'm struggling to get the WIZnet RP2040-HAT-MicroPython code implemented into this example...

I receive a [Errno 22] EINVAL error and not capable to solve it as I have no experience with coding.

This is the code of the WIZnet HTTP server example that works on my Pico:

from usocket import socket
from machine import Pin,SPI
import network
import time

led = Pin(25, Pin.OUT)

#W5x00 chip init
def w5x00_init():
    spi=SPI(0,2_000_000, mosi=Pin(19),miso=Pin(16),sck=Pin(18))
    nic = network.WIZNET5K(spi,Pin(17),Pin(20)) #spi,cs,reset pin
    nic.active(True)
    
    #None DHCP
    nic.ifconfig(('192.168.0.210','255.255.255.0','192.168.0.1','8.8.8.8'))
    
    #DHCP
    #nic.ifconfig('dhcp')
    print('IP address :', nic.ifconfig())
    
    while not nic.isconnected():
        time.sleep(1)
        print(nic.regs())
    
def web_page():
    if led.value()==1:
        led_state="ON"
    else:
        led_state="OFF"
        
    html = """
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Raspberry Pi Pico Web server - WIZnet W5100S</title>
    </head>
    <body>
    <div align="center">
    <H1>Raspberry Pi Pico Web server & WIZnet Ethernet HAT</H1>
    <h2>Control LED</h2>
    PICO LED state: <strong>""" + led_state + """</strong>
    <p><a href="/?led=on"><button class="button">ON</button></a><br>
    </p>
    <p><a href="/?led=off"><button class="button button2">OFF</button></a><br>
    </p>
    </div>
    </body>
    </html>
    """
    return html

def main():
    w5x00_init()
    s = socket()
    s.bind(('192.168.0.210', 502))
    s.listen(5)

    while True:
        conn, addr = s.accept()
        print('Connect from %s' % str(addr))
        request = conn.recv(1024)
        request = str(request)
        #print('Content = %s' % request)
        led_on = request.find('/?led=on')
        led_off = request.find('/?led=off')
        if led_on == 6:
            print("LED ON")
            led.value(1)
        if led_off == 6:
            print("LED OFF")
            led.value(0)
        response = web_page()
        conn.send('HTTP/1.1 200 OK\n')
        conn.send('Connection: close\n')
        conn.send('Content-Type: text/html\n')
        conn.send('Content-Length: %s\n\n' % len(response))
        conn.send(response)
        conn.close()

if __name__ == "__main__":
    main()

This is the code I ended up with combining the 2:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

"""
Main script

Do your stuff here, this file is similar to the loop() function on Arduino

Create a Modbus TCP client (slave) which can be requested for data or set with
specific values by a host device.

The TCP port and IP address can be choosen freely. The register definitions of
the client can be defined by the user.
"""

# system packages
import time

# import modbus client classes
from umodbus.tcp import ModbusTCP
from machine import Pin,SPI

IS_DOCKER_MICROPYTHON = False
try:
    import network
except ImportError:
    IS_DOCKER_MICROPYTHON = True
    import json
    

# ===============================================
if IS_DOCKER_MICROPYTHON is False:
    # connect to a network

    spi = SPI(0,2_000_000, mosi=Pin(19),miso=Pin(16),sck=Pin(18))
    station = network.WIZNET5K(spi,Pin(17),Pin(20)) #spi,cs,reset pin
    if station.active() and station.isconnected():
        station.disconnect()
        time.sleep(1)
    station.active(False)
    time.sleep(1)
    station.active(True)
    
    while True:
        print('Waiting for connection...')
        if station.isconnected():
            print('Connected.')
            print(station.ifconfig())
            break
        time.sleep(2)
#
# ===============================================
# TCP Slave setup
tcp_port = 502              # port to listen to

if IS_DOCKER_MICROPYTHON:
    local_ip = '192.168.0.210'     # static Docker IP address
else:
    # set IP address of the MicroPython device explicitly
    # local_ip = '192.168.4.1'    # IP address
    # or get it from the system after a connection to the network has been made
    local_ip = station.ifconfig()[0]

# ModbusTCP can get TCP requests from a host device to provide/set data
client = ModbusTCP()
is_bound = False

# check whether client has been bound to an IP and port
is_bound = client.get_bound_status()

if not is_bound:
    client.bind(local_ip=local_ip, local_port=tcp_port)


def my_holding_register_set_cb(reg_type, address, val):
    print('Custom callback, called on setting {} at {} to: {}'.
          format(reg_type, address, val))


def my_holding_register_get_cb(reg_type, address, val):
    print('Custom callback, called on getting {} at {}, currently: {}'.
          format(reg_type, address, val))

    print('Custom callback, called on getting {} at {}, currently: {}'.
          format(reg_type, address, val))

    # any operation should be as short as possible to avoid response timeouts
    new_val = val[0] + 1

    # It would be also possible to read the latest ADC value at this time
    # adc = machine.ADC(12)     # check MicroPython port specific syntax
    # new_val = adc.read()

    client.set_ireg(address=address, value=new_val)
    print('Incremented current value by +1 before sending response')




def reset_data_registers_cb(reg_type, address, val):
    # usage of global isn't great, but okay for an example
    global client
    global register_definitions

    print('Resetting register data to default values ...')
    client.setup_registers(registers=register_definitions)
    print('Default values restored')


# commond slave register setup, to be used with the Master example above
register_definitions = {
    "HREGS": {
        "EXAMPLE_HREG": {
            "register": 93,
            "len": 1,
            "val": 19
        }
    }
}

# alternatively the register definitions can also be loaded from a JSON file
# this is always done if Docker is used for testing purpose in order to keep
# the client registers in sync with the test registers
if IS_DOCKER_MICROPYTHON:
    with open('registers/example.json', 'r') as file:
        register_definitions = json.load(file)

# add callbacks for different Modbus functions
# each register can have a different callback
# coils and holding register support callbacks for set and get
register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \
    my_holding_register_set_cb
register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \
    my_holding_register_get_cb

print('Setting up registers ...')
# use the defined values of each register type provided by register_definitions
client.setup_registers(registers=register_definitions)
# alternatively use dummy default values (True for bool regs, 999 otherwise)
# client.setup_registers(registers=register_definitions, use_default_vals=True)
print('Register setup done')

print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port))

while True:
    try:
        result = client.process()
    except KeyboardInterrupt:
        print('KeyboardInterrupt, stopping TCP client...')
        break
    except Exception as e:
        print('Exception during execution: {}'.format(e))

print("Finished providing/accepting data as client")

PLC Engineer exploring new fields is thanking you all in advance!

Create test framework

As found out during review of PR #10 and follow up tickets #12 and #15 the implementation does not 100% follow the Modbus requirements

Create a test framework with different requests and check response data for single and multi registers

License

I see parts of the codebase say that it is under GPL v3, because it is from Pycom's Modbus library. How is this licensed under MIT, if it uses GPL code?

Docs: request a simple doc about to create/extend `register definitions` (rules and meanings)

In my opinion, is very important, to prevent errors, to have a simple doc (with examples) to explain two things:

  1. What is rules on the register definitions, I mean, what and how each argument need to be constructed.
  2. What means (what it will affect) each argument. Example: until now is not very clear to me what means the len, like as 'len': 1. The reason that is not very clear yet is that some times I changed the len to other value, and nothing change. Or because my register definitions is not correct - so, is a one more good reason to have a doc explaining in details about register definitions.

Invalid CRC on reading multiple COILS

    > @beyonlo yes, this is expected. As you set the coils of 125 to `[1,1,0]` but then request 16 coils (which are not available, only 3) the CRC is invalid. This again relates to #35 and will be solved after this PR

@brainelectronics unfortunately that error happen with coil_qty=3 as well. Details below:

Slave:

$ mpremote run rtu_client_example_with_callback.py 
Running ModBus version: 2.3.0-rc22.dev51
Custom callback, called on getting COILS at 125, currently: [1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1]
Custom callback, called on setting COILS at 125 to: [False, True, True]
Custom callback, called on getting COILS at 125, currently: [False, True, True]
Custom callback, called on getting COILS at 125, currently: [False, True]
Custom callback, called on getting COILS at 125, currently: [False]
Custom callback, called on getting COILS at 125, currently: [False, True, True]
Custom callback, called on getting COILS at 125, currently: [False, True, True]

Master:

>>> from umodbus.serial import Serial as ModbusRTUMaster
>>> rtu_pins = (17, 18)
>>> host = ModbusRTUMaster(baudrate=115200, data_bits=8, stop_bits=1, parity=None, pins=rtu_pins, ctrl_pin=15)
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=16)
[True, True, True, True, True, False, False, False, False, True, True, True, False, False, False, True]
>>> host.write_multiple_coils(slave_addr=10, starting_address=125, output_values=[1,1,0])
True
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 136, in read_coils
  File "/lib/umodbus/serial.py", line 289, in _send_receive
  File "/lib/umodbus/serial.py", line 322, in _validate_resp_hdr
OSError: invalid response CRC
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 136, in read_coils
  File "/lib/umodbus/serial.py", line 289, in _send_receive
  File "/lib/umodbus/serial.py", line 322, in _validate_resp_hdr
OSError: invalid response CRC
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 136, in read_coils
  File "/lib/umodbus/serial.py", line 289, in _send_receive
  File "/lib/umodbus/serial.py", line 322, in _validate_resp_hdr
OSError: invalid response CRC
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 136, in read_coils
  File "/lib/umodbus/serial.py", line 289, in _send_receive
  File "/lib/umodbus/serial.py", line 322, in _validate_resp_hdr
OSError: invalid response CRC
>>> 
>>> host.read_coils(slave_addr=10, starting_addr=125, coil_qty=4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 136, in read_coils
  File "/lib/umodbus/serial.py", line 289, in _send_receive
  File "/lib/umodbus/serial.py", line 322, in _validate_resp_hdr
OSError: invalid response CRC
>>>

Originally posted by @beyonlo in #51 (comment)

Bug: error when trying to write multiple registers - `host.write_multiple_registers()`

This bug was extracted from PR #10 report, in this reply:

Error when trying to write multiple registers (host.write_multiple_registers())

>>> host.write_single_register(slave_addr=10, register_address=93, register_value=44, signed=False)
True
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(44,)
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, -4, 6, -256, 1024], signed=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, 1024], signed=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 

Raspberry Pico Modbus TCP IO Device

Description

Iam New with micropython and the Raspberry Pico W and i would like to create a Modbus TCP IO Divice for a PLC on an Raspberry Pico W
I would read some GPIO to send they over Modbus TCP to the Modbus Master

Can someone show me an code example to fix my problem.

Thank you so much

Reproduction steps

1.install Micropython an Modbus on Raspberry was okay
2.I use the Thonny IDE and I can run the TCP Client Example on the Raspberry Pico
3. but I down know how can I send GPIO Data to the Modbus Master
4. I need a example for send A Bool GPIO value (coil)
5. I need a example for send A ADC value (holding register)

MicroPython version

v1.19.1

MicroPython board

Raspberry Pico

MicroPython Modbus version

v2.3.3

Relevant log output

No response

User code

No response

Additional informations

No response

Value limits accepted by Slave on the Holding Register (HREGS)

Description

Is there a way to set what are the values allowed and/or min and max values allowed on the Slave?

"HREGS": {
    "EXAMPLE_HREG": {
        "register": 93,
        "len": 9,
        "val": [1, 38, 0, 1600, 2150, 5067, 2564, 8450, 3456]
    }

Example 1:
The address 93 is number 29. I would like that address accept just min value 1 and max value 8

Example 2:
The address 96 is number 1600. I would like that address accept just one of this list [1600, 2100, 2300, 5000, 132, 8003]

So, if ModBus Master try to set a different value than that allowed, will be not accepted by the Slave, and an error will be returned to the Master.

Is possible to do that or something similar?

Thank you!

Reproduction steps

...

MicroPython version

1.19.1

MicroPython board

ESP32

MicroPython Modbus version

2.4.0rc62.dev56

Relevant log output

No response

User code

No response

Additional informations

No response

Serial problem on RPi Pico W

Issue:

When creating a modbus master on RPi Pico W, you get the following error:

>>> m = ModbusRTU(addr=1, pins=(0,1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 58, in __init__
  File "umodbus/serial.py", line 104, in __init__
ValueError: expecting a Pin```

If you create Pin objects and use these instead, you get the following error:

>>> tx_pin = Pin(0)
>>> rx_pin = Pin(1)
>>> m = ModbusRTU(addr=1, pins=(tx_pin, rx_pin))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 58, in __init__
  File "umodbus/serial.py", line 104, in __init__
ValueError: bad TX pin

Is Circuitpython supported?

It seems this package only works with micropython, is there any possibility to add support for circuitpython?

Reading coils is limited to 8 bits

Related to #12, found during #33 (comment) section "Problem 1" and "Problem 2"

Only up to 8 coils can be read at once. Reading 9 or more return only valid results for the first 8 bit.

Reason/root cause:
Only the first integer value of the byte list is taken into account, see

bool_list = [bool(int(x)) for x in fmt.format(list(byte_list)[0])]
although the format is correctly set to contain the specified amount of bits

Problem with RS485 direction control

Description

I am using a ST3485 RS485 transceiver with the DE/RE pins shortened and connected to a GPIO pin.

Running as Modbus master, the request is received correctly by the slave but the response is not received and umodbus throws an error (see below).

I suspect some timing issue with the transceiver switching to read mode but can't quite find the root cause of this.

Any ideas?

Reproduction steps

Initialization:

mbm0 = ModbusRTUMaster(pins=(Pin(GP_UART0_TX), Pin(GP_UART0_RX)),ctrl_pin=GP_UART0_DC, uart_id=0)

Send request to slave

mbm0.read_holding_registers(42,1,1)

MicroPython version

MicroPython v1.19.1-994-ga4672149b

MicroPython board

Raspberry Pico

MicroPython Modbus version

v2.3.4

Relevant log output

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/common.py", line 199, in read_holding_registers
  File "/lib/umodbus/serial.py", line 287, in _send_receive
  File "/lib/umodbus/serial.py", line 177, in _uart_read
  File "/lib/umodbus/serial.py", line 149, in _exit_read
IndexError: bytearray index out of range

User code

No response

Additional informations

No response

Example do not works: ModbusRTUMaster and ModbusTCPMaster class do not exists.

Hello!

I following the example in the README.md of Slave and Master (TCP and RTU), but there is no class of ModbusRTUMaster and ModbusTCPMaster inside file modbus.py. I tried to find in all files inside umodbus dir, but nothing too.

Example from README.md:

# TCP Master setup
# act as host, get Modbus data via TCP from a client device
host = ModbusTCPMaster(
    slave_ip=192.168.178.34,
    slave_port=180,
    timeout=5)              # optional, default 5

Thank you in advance!

Add contribution guideline

Mention steps to be execucted locally before pushing changes in a PR. Maybe add also precommit hooks at least for flake8

          @beyonlo I've published https://test.pypi.org/project/micropython-modbus/2.4.0rc62.dev56/ for testing. 

@GimmickNG please check flake8 before pushing changes, builds are not even reaching the tests anymore

pip install -r requirements-test.txt
flake8 .
yamllint .

# tests can be locally checked by
docker build --tag micropython-test --file Dockerfile.tests .
docker compose up --build --exit-code-from micropython-host
docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host
docker compose -f docker-compose-rtu.yaml up --build --exit-code-from micropython-host
docker compose -f docker-compose-rtu-test.yaml up --build --exit-code-from micropython-host

Originally posted by @brainelectronics in #56 (comment)

Cite this repository

Description

I would like to cite this repository. Github provides a means of doing so via the CITATIONS file (IIRC). Would this be supported, or should I just cite the start page?

Reproduction steps

N/A

MicroPython version

N/A

MicroPython board

other

Relevant log output

No response

User code

No response

Additional informations

No response

Bug: incorrect return when write a COIL with `1` or `True`

This bug was extracted from PR #10 report, in this reply:

On the host.write_single_coil() has incorrect return when write 1/True.

I believe that return (True or False) on write methods is to check if has success or not writing data, is this interpretation correct?
When I write COIL value as 1 or True, it works, I mean, that value is writing with success, but the return is False (not success), differently when I write 0 or False, where the return is True (success).

>>> host.write_single_coil(slave_addr=10, output_address=123, output_value=0)
True
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[False, False, False, False, False, False, False, False]
>>> host.write_single_coil(slave_addr=10, output_address=123, output_value=1)
False
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]
>>> 

Reading holding registers does always return all registers

Related to #25, similar to #36, contributes to #35, found during #33 (comment) section "Problem 4"

Defining a holding register with more than one element, but requesting only 1 element, returns all elements of the register.

register_definitions = {
    "HREGS": {
        "ANOTHER_EXAMPLE_HREG": {
            "register": 94,
            "len": 9,
            "val": [29, 38, 0, 1600, 2150, 5067, 2564, 8450, 3456],
            "description": "Example HREGS registers with length of 9, Hregs (setter+getter) [0, 65535]",
            "range": "",
            "unit": ""
        }
    }
}
>>> host.read_holding_registers(slave_addr=10, starting_addr=94, register_qty=1, signed=False)
(29, 38, 0, 1600, 2150, 5067, 2564, 8450, 3456)
# expectation would be (29,)

Reason/root cause:
The response quantity is defined as the length of the returned byte list, see

response_quantity = int(len(byte_array) / 2)
fmt = '>' + (('h' if signed else 'H') * response_quantity)
not taking into account the actual amount of requested data. Thereby the format to unpack the data is >hhhhhhhhh instead of >h as the response byte array lenght is 9, but the register quantity of interest is 1

Implementing GPIO pins in TCP client mode on Pico

Description

Finally I got the communication running but I'm really stuck with how to transfer the ModbusTCP received values (register) to the GPIO pins...
After days of searching the web I find no examples at all regarding what I'm trying to do here.
I know this is not an issue with the library but maybe useful for others in the future as documentation.

So my apologies in advance for such a rookie question.

Reproduction steps

I will receive multiple 0-100% values (HREG) in the registry from my s7-1200 PLC and want to control onboard PWMs accordingly as a cheap way to dim LED lighting in my house.

Per example, how do I get the value of this register entry:

"EXAMPLE_HREG": {
            "register": 1,
            "len": 1,
            "val": 19,
            "description": "Example HREGS register, Hregs (setter+getter) [0, 65535]",
            "range": "",
            "unit": ""
        }

as a usable value inside the rest of the code?
Let's say in here, where the 32000 is:

def PWM_init():
    LED_BUILTIN = 25 # For Raspberry Pi Pico

    pwm_led = PWM(Pin(LED_BUILTIN, mode=Pin.OUT)) # Attach PWM object on the LED pin
    

    # Settings
    pwm_led.freq(1000)

    while True:
        pwm_led.duty_u16(32000)

MicroPython version

v1.19.1

MicroPython board

Raspberry Pico

Relevant log output

No response

User code

No response

Additional informations

No response

Single Coil Write and Holding Register Write Initial Response Error

Using an RS-485 Transceiver with automatic flow control (MAX22028) and also tried with THVD-2410 (flow control pin).
THVD-2410 works with the flow control code, but when I try to use a transceiver without the flow control pin, the single coil write and single register write responses are duplicated 3 times. Timeout shows to be 8020 microseconds. The issue is masked with the THVD-2410 and likely 95% of people attempting this because the transceiver keeps the responses from reaching the 485 bus. Below is my test code to recreate the issue.

from umodbus.serial import ModbusRTU
from machine import Pin
tx = machine.Pin(0)
rx = machine.Pin(1)

rtu_pins = (tx, rx) # (TX, RX)
slave_addr = 3 # address on bus as client
baudrate = 9600
client = ModbusRTU(
addr=slave_addr, # address on bus
baudrate=baudrate, # optional, default 9600
pins=rtu_pins, # given as tuple (TX, RX)
# data_bits=8, # optional, default 8
# stop_bits=1, # optional, default 1
# parity=None, # optional, default None
ctrl_pin=None, # optional, control DE/RE
uart_id=0 # optional, see port specific documentation
)

When I do not use flow control I get 3 consecutive response messages on the Rx line with function code 6:
image

Using function code 16 works though:
image

THVD-2410 (w/flow control) setup - this works flawlessly - absolutely no errors
image

MAX22028 (automatic flow control) setup - only fails on the initial response
image
After settings a holding register to say 0, the initial response coming out of the device is wrong, but it does somehow take that value, so when the register is read with FC3, it indeed has the correct value in it.

Wondering if this is happening because the RX buffer is not actually getting cleared somehow, but I can't find any discrepancies in the umodbus code.

Originally posted by @j-broome in #7 (comment)

Reading coil data bits is reversed

Found during resolving #36

Setting up a coil with the following data and reading the coil states returns 1100 1101 instead of expected 1011 0011 as setup. This issue was covered by the limit of only reading up to 8 coils in a row. And due to the bad unittest, not covering this case.

{
    "COILS": {
        "MANY_COILS": {
            "register": 150,
            "len": 19,
            "val": [
                1, 0, 1, 1, 0, 0, 1, 1,
                1, 1, 0, 1, 0, 1, 1, 0,
                1, 0, 1
            ],
            "description": "Modbus_Application_Protocol_V1_1b3 Read Coils example"
        }
    }
}
b'\xCD\x6B\x05',
[
            # 20, 21,    22,   23,   24,    25,    26,   27
            True, False, True, True, False, False, True, True,
            # 28, 29,   30,    31,   32,    33,   34,   35
            True, True, False, True, False, True, True, False,
            # 36, 37,    38
            True, False, True
]

Reason/root cause
Not sure yet, but it might be due to

if function_code in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]:
sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)] # noqa: E501
output_value = []
for index, byte in enumerate(sectioned_list):
# see https://github.com/brainelectronics/micropython-modbus/issues/22
# output = sum(v << i for i, v in enumerate(byte))
output = 0
for bit in byte:
output = (output << 1) | bit
output_value.append(output)
fmt = 'B' * len(output_value)
return struct.pack('>BB' + fmt,
function_code,
((len(value_list) - 1) // 8) + 1,
*output_value)
which is of course the only untested function of functions.py. In particular the constructed output_value is mixing up LSB and MSB.

The status of outputs 27–20 is shown as the byte value CD hex, or binary 1100 1101. Output 27 is the MSB of this byte, and output 20 is the LSB.

Taken from Modbus Application Protocol, chapter 6.1 01 (0x01) Read Coils

Add issue template file

Fill existing contribution markdown file in the docs folder with content.

Also add an issues and PR template file

'UART' object has no attribute 'wait_tx_done'

I'm trying to use this library on Pi Pico W to communicate with ModbusRTU device. I'm using MAX485 for modbus bridge (required ctrl_pin).

When you try to read holding registers it fails with the following error:

  File "<stdin>", line 18, in <module>
  File "/lib/umodbus/serial.py", line 199, in read_coils
  File "/lib/umodbus/serial.py", line 171, in _send_receive
  File "/lib/umodbus/serial.py", line 163, in _send
AttributeError: 'UART' object has no attribute 'wait_tx_done'

ModbusRTU is trying to call "wait_tx_done" from machine UART lib, and that function is not included in machine library.

while not self._uart.wait_tx_done(2):

How to read registers?

First of all, it is the first time that I work with modbus. So sorry if the question is stupid.
How can I read or write a register of a frequency converter?

W600 and ESP8266 MemoryError

Description

So I am using Thonny and installed this library and ran the slave code with Wemos d1 mini esp8266, Wemos W600-pico and the Raspberry Pi PICO. The code ran fine with the Raspberry Pi Pico. However, both the Wemos W600-PICO and ESP8266 ran into trouble with memory allocation.

Reproduction steps

1.Install the umodbus library on Micropython via the manage packages in Thonny
2. Copy/ paste the RTU slave code in it.
3.Run the code
...

MicroPython version

v1.19.1-870-gb9300ac5b-dirty

MicroPython board

other

Relevant log output

For W600:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/flash/lib/umodbus/serial.py", line 23, in <module>
MemoryError: memory allocation failed, allocating 704 bytes

User code

No response

Additional informations

No response

Initial Response on Write Single Reg/Coil

After modifying serial.py lines 58 and 71 (image 1), I am able to setup the Pico as an RTU slave (image 2). When I set a holding register, it looks like the initial response looks funny and that response isn't liked by the master (throws a frame error, but it seems fine after that. Is there something in the code that would cause that? (see image 3)
image 1:
image
image 2:
image
image 3:
image

Originally posted by @j-broome in #7 (comment)

Can't install using upip - ESP8266

Description

I'm following the docs in a new esp8266 chip. I'm getting error.

Reproduction steps

>>> upip.install('micropython-modbus')
Installing to: /lib/
Error installing 'micropython-modbus': , packages may be partially installed

MicroPython version

v1.19.1

MicroPython board

ESP8266

MicroPython Modbus version

>>> import os
>>> from umodbus import version
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'umodbus'
>>> print('MicroPython infos:', os.uname())
MicroPython infos: (sysname='esp8266', nodename='esp8266', release='2.2.0-dev(9422289)', version='v1.19.1 on 2022-06-18', machine='ESP module with ESP8266')
>>> print('Used micropthon-modbus version:', version.__version__))
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax

Compatibility with W5500 or ENC28J60 - SPI to Ethernet Modules

Description

I am a beginner with both MicroPython & Ethernet communication. Can you guide me with using this library with an ESP32 or STM32 connected via SPI to W5500 or ENC28J60 module - Link (Note that there might be other devices connected to that SPI bus with their own CS pins).

I would really appreciate your help and guidance.

ModbusRTU Client pin usage on STM32

      # RTU Slave setup
         # act as client, provide Modbus data via RTU to a host device
          rtu_pins = ('P3', 'P4')         # (TX, RX)
    
            slave_addr = 10             # address on bus as client
              baudrate = 9600

line 679 client = ModbusRTU(addr=slave_addr , baudrate=9600,pins= rtu_pins)

File "/flash/lib/umodbus/modbus.py", line 679, in init
File "/flash/lib/umodbus/serial.py", line 42, in init
TypeError: extra keyword arguments given

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.