Skip to content

Commit

Permalink
Add single char wait time after flush to avoid RTU control pin timing…
Browse files Browse the repository at this point in the history
  • Loading branch information
brainelectronics authored and hmaerki committed Dec 24, 2023
1 parent 8bf8a3f commit 8e8ed49
Show file tree
Hide file tree
Showing 10 changed files with 489 additions and 13 deletions.
7 changes: 6 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- ## [Unreleased] -->

## Released
## [2.3.7] - 2023-07-19
### Fixed
- Add a single character wait time after flush to avoid timing issues with RTU control pin, see #68 and #72

## [2.3.6] - 2023-07-19
### Added
- Add contribution guideline, see #67
Expand Down Expand Up @@ -303,8 +307,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus)

<!-- Links -->
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.6...develop
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.7...develop

[2.3.7]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.7
[2.3.6]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.6
[2.3.5]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.5
[2.3.4]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.4
Expand Down
22 changes: 12 additions & 10 deletions examples/rtu_host_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# import modbus host classes
from umodbus.serial import Serial as ModbusRTUMaster

print("A")

IS_DOCKER_MICROPYTHON = False
try:
import machine
Expand All @@ -32,7 +34,7 @@
IS_DOCKER_MICROPYTHON = True
import sys


print("B")
# ===============================================
# RTU Slave setup
slave_addr = 10 # address on bus of the client/slave
Expand Down Expand Up @@ -159,7 +161,7 @@
starting_addr=coil_address,
coil_qty=coil_qty)
print('Status of COIL {}: {}'.format(coil_address, coil_status))
time.sleep(1)
# time.sleep(1)

# WRITE COILS
new_coil_val = 0
Expand All @@ -168,15 +170,15 @@
output_address=coil_address,
output_value=new_coil_val)
print('Result of setting COIL {} to {}'.format(coil_address, operation_status))
time.sleep(1)
# time.sleep(1)

# READ COILS again
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))
time.sleep(1)
# time.sleep(1)

print()

Expand All @@ -189,7 +191,7 @@
register_qty=register_qty,
signed=False)
print('Status of HREG {}: {}'.format(hreg_address, register_value))
time.sleep(1)
# time.sleep(1)

# WRITE HREGS
new_hreg_val = 44
Expand All @@ -199,7 +201,7 @@
register_value=new_hreg_val,
signed=False)
print('Result of setting HREG {} to {}'.format(hreg_address, operation_status))
time.sleep(1)
# time.sleep(1)

# READ HREGS again
register_value = host.read_holding_registers(
Expand All @@ -208,7 +210,7 @@
register_qty=register_qty,
signed=False)
print('Status of HREG {}: {}'.format(hreg_address, register_value))
time.sleep(1)
# time.sleep(1)

print()

Expand All @@ -220,7 +222,7 @@
starting_addr=ist_address,
input_qty=input_qty)
print('Status of IST {}: {}'.format(ist_address, input_status))
time.sleep(1)
# time.sleep(1)

print()

Expand All @@ -233,7 +235,7 @@
register_qty=register_qty,
signed=False)
print('Status of IREG {}: {}'.format(ireg_address, register_value))
time.sleep(1)
# time.sleep(1)

print()

Expand All @@ -248,7 +250,7 @@
output_address=coil_address,
output_value=new_coil_val)
print('Result of setting COIL {}: {}'.format(coil_address, operation_status))
time.sleep(1)
# time.sleep(1)

print()

Expand Down
13 changes: 13 additions & 0 deletions heizung-Dockerfile.dezentral_rtu
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build image
# $ docker build -t micropython-client-rtu -f Dockerfile.client_rtu .
#
# Run image
# $ docker run -it --rm --name micropython-client-rtu micropython-client-rtu

FROM micropython/unix:v1.18

# use "volumes" in docker-compose file to remove need of rebuilding
# COPY ./ /home
# COPY umodbus /root/.micropython/lib/umodbus

CMD [ "micropython-dev", "-m", "heizung_rtu_dezentral" ]
13 changes: 13 additions & 0 deletions heizung-Dockerfile.zentral_rtu
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build image
# $ docker build -t micropython-host-rtu -f Dockerfile.host_rtu .
#
# Run image
# $ docker run -it --rm --name micropython-host-rtu micropython-host-rtu

FROM micropython/unix:v1.18

# use "volumes" in docker-compose file to remove need of rebuilding
# COPY ./ /home
# COPY umodbus /root/.micropython/lib/umodbus

CMD [ "micropython-dev", "-m", "heizung_rtu_zentral" ]
62 changes: 62 additions & 0 deletions heizung-docker-compose-rtu.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---

#
# build all non-image containers
# $ docker-compose build
# can be combined into one command to also start it afterwards
# $ docker-compose up --build
#

version: "3.8"

services:
micropython-dezentral:
build:
context: .
dockerfile: heizung-Dockerfile.dezentral_rtu
container_name: micropython-dezentral
volumes:
- ./:/home
- ./umodbus:/root/.micropython/lib/umodbus
- ./fakes:/usr/lib/micropython
- ./tests/ulogging.py:/root/.micropython/lib/ulogging.py
expose:
- "65433"
ports:
# reach "micropython-dezentral" at 172.25.0.2:65433, see networks
- "65433:65433"
networks:
serial_bridge:
# fix IPv4 address to be known and in the MicroPython scripts
# https://docs.docker.com/compose/compose-file/#ipv4_address
ipv4_address: 172.25.0.2

micropython-zentral:
build:
context: .
dockerfile: heizung-Dockerfile.zentral_rtu
container_name: micropython-zentral
volumes:
- ./:/home
- ./umodbus:/root/.micropython/lib/umodbus
- ./fakes:/usr/lib/micropython
- ./tests/ulogging.py:/root/.micropython/lib/ulogging.py
depends_on:
- micropython-dezentral
networks:
serial_bridge:
# fix IPv4 address to be known and in the MicroPython scripts
# https://docs.docker.com/compose/compose-file/#ipv4_address
ipv4_address: 172.25.0.3

networks:
serial_bridge:
# use "external: true" if the network already exists
# check available networks with "docker network ls"
# external: true
driver: bridge
# https://docs.docker.com/compose/compose-file/#ipam
ipam:
config:
- subnet: 172.25.0.0/16
gateway: 172.25.0.1
149 changes: 149 additions & 0 deletions heizung_rtu_dezentral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/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 RTU client (slave) which can be requested for data or set with
specific values by a host device.
The RTU communication pins can be choosen freely (check MicroPython device/
port specific limitations).
The register definitions of the client as well as its connection settings like
bus address and UART communication speed can be defined by the user.
"""

# import modbus client classes
from umodbus.serial import ModbusRTU

IS_DOCKER_MICROPYTHON = False
try:
import machine
machine.reset_cause()
except ImportError:
raise Exception('Unable to import machine, are all fakes available?')
except AttributeError:
# machine fake class has no "reset_cause" function
IS_DOCKER_MICROPYTHON = True
import json


# ===============================================
# RTU Slave setup
# act as client, provide Modbus data via RTU to a host device
# ModbusRTU can get serial requests from a host device to provide/set data
# check MicroPython UART documentation
# https://docs.micropython.org/en/latest/library/machine.UART.html
# for Device/Port specific setup
#
# RP2 needs "rtu_pins = (Pin(4), Pin(5))" whereas ESP32 can use any pin
# the following example is for an ESP32.
# For further details check the latest MicroPython Modbus RTU documentation
# example https://micropython-modbus.readthedocs.io/en/latest/EXAMPLES.html#rtu
rtu_pins = (25, 26) # (TX, RX)
slave_addr = 10 # address on bus as client
baudrate = 9600
uart_id = 1

from machine import Pin
import os
from umodbus import version

print('Using pins {} with UART ID {}'.format(rtu_pins, uart_id))

client = ModbusRTU(
addr=slave_addr, # address on bus
pins=rtu_pins, # given as tuple (TX, RX)
baudrate=baudrate, # 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=uart_id # optional, default 1, see port specific docs
)

if IS_DOCKER_MICROPYTHON:
# works only with fake machine UART
assert client._itf._uart._is_server is True


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')


# common slave register setup, to be used with the Master example above
register_definitions = {
"COILS": {
"RESET_REGISTER_DATA_COIL": {
"register": 42,
"len": 1,
"val": 0
},
"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
}
}
}

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

# reset all registers back to their default value with a callback
register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \
reset_data_registers_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 RTU client on address {} at {} baud'.
format(slave_addr, baudrate))

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

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

0 comments on commit 8e8ed49

Please sign in to comment.