Repository: brainelectronics/micropython-modbus (language: python)

Issue:
Add testing framework for ModbusRTU
Create test framework for ModbusRTU implementation. Try to use similar way as for Modbus TCP with Unix Micropython docker container
Add testing framework for ModbusRTU
Create test framework for ModbusRTU implementation. Try to use similar way as for Modbus TCP with Unix Micropython docker container


Discussion / hints:

Fix patch:
diff --git a/Dockerfile.client_rtu b/Dockerfile.client_rtu
new file mode 100644
index 0000000..0b37d95
--- /dev/null
+++ b/Dockerfile.client_rtu
@@ -0,0 +1,15 @@
+# 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
+
+RUN micropython-dev -m upip install micropython-ulogging
+
+CMD [ "micropython-dev", "-m", "examples/rtu_client_example.py" ]
diff --git a/Dockerfile.client b/Dockerfile.client_tcp
similarity index 68%
rename from Dockerfile.client
rename to Dockerfile.client_tcp
index 46e14d8..e1b5f20 100644
--- a/Dockerfile.client
+++ b/Dockerfile.client_tcp
@@ -1,8 +1,8 @@
 # Build image
-# $ docker build -t micropython-client -f Dockerfile.client .
+# $ docker build -t micropython-client-tcp -f Dockerfile.client_tcp .
 #
 # Run image
-# $ docker run -it --rm --name micropython-client micropython-client
+# $ docker run -it --rm --name micropython-client-tcp micropython-client-tcp
 
 FROM micropython/unix:v1.18
 
diff --git a/Dockerfile.host_rtu b/Dockerfile.host_rtu
new file mode 100644
index 0000000..e1f4dd3
--- /dev/null
+++ b/Dockerfile.host_rtu
@@ -0,0 +1,15 @@
+# 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
+
+RUN micropython-dev -m upip install micropython-ulogging
+
+CMD [ "micropython-dev", "-m", "examples/rtu_host_example.py" ]
diff --git a/Dockerfile.host b/Dockerfile.host_tcp
similarity index 69%
rename from Dockerfile.host
rename to Dockerfile.host_tcp
index 7c61c8d..689a2bc 100644
--- a/Dockerfile.host
+++ b/Dockerfile.host_tcp
@@ -1,8 +1,8 @@
 # Build image
-# $ docker build -t micropython-host -f Dockerfile.host .
+# $ docker build -t micropython-host-tcp -f Dockerfile.host_tcp .
 #
 # Run image
-# $ docker run -it --rm --name micropython-host micropython-host
+# $ docker run -it --rm --name micropython-host-tcp micropython-host-tcp
 
 FROM micropython/unix:v1.18
 
diff --git a/changelog.md b/changelog.md
index 607cc42..fcf8deb 100644
--- a/changelog.md
+++ b/changelog.md
@@ -15,6 +15,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 <!-- ## [Unreleased] -->
 
 ## Released
+## [2.2.0] - 2023-01-03
+### Added
+- [Fake machine module](fakes/machine.py) with UART and Pin class to be used on Unix MicroPython container for RTU tests and examples, see #47
+- [RTU host example script](examples/rtu_host_example.py)
+- [RTU docker compose file](docker-compose-rtu.yaml) and [RTU docker compose file test](docker-compose-rtu-test.yaml) based in MicroPython 1.18 image
+- [RTU client Dockerfile](Dockerfile.client_rtu) and [RTU host Dockerfile](Dockerfile.host_rtu) based on MicroPython 1.18 image
+- Initial [RTU examples unittest](tests/test_rtu_example.py)
+- RTU example section for Client and Host in USAGE
+
+### Changed
+- Outsourced the following common functions of [serial.py](umodbus/serial.py) and [tcp.py](umodbus/tcp.py) into `CommonModbusFunctions` of [common.py](umodbus/common.py):
+    - `read_coils`
+    - `read_discrete_inputs`
+    - `read_holding_registers`
+    - `read_input_registers`
+    - `write_single_coil`
+    - `write_single_register`
+    - `write_multiple_coils`
+    - `write_multiple_registers`
+
+- Inherit from `CommonModbusFunctions` in `Serial` of [serial.py](umodbus/serial.py) and in `TCP` of of [tcp.py](umodbus/tcp.py)
+- Extended RTU client example for Docker usage to load all registers from example JSON file
+- Update internal functions parameter name from `slave_id` to `slave_addr` of TCP's `_create_mbap_hdr` and `_validate_resp_hdr` function to be the same as in Serial
+- Update Modbus function documentation from TCP specific to common module in USAGE file
+- Renamed docker files:
+     - `Dockerfile.client` -> `Dockerfile.client_tcp`
+     - `Dockerfile.host` -> `Dockerfile.host_tcp`
+     - `Dockerfile.test_tcp_example` -> `Dockerfile.test_examples`
+
 ## [2.1.3] - 2022-12-30
 ### Fixed
 - `uart_id` can be specified during init of `ModbusRTU` and `Serial` class and is no longer hardcoded to `1`, but set as `1` by default to ensure backwards compability, see #7 and #43
@@ -195,8 +224,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.1.3...develop
+[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.2.0...develop
 
+[2.2.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.2.0
 [2.1.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.3
 [2.1.2]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.2
 [2.1.1]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.1
diff --git a/docker-compose-rtu.yaml b/docker-compose-rtu.yaml
new file mode 100644
index 0000000..b71bb25
--- /dev/null
+++ b/docker-compose-rtu.yaml
@@ -0,0 +1,57 @@
+#
+# 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-client:
+    build:
+      context: .
+      dockerfile: Dockerfile.client_rtu
+    container_name: micropython-client
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+      - ./fakes:/usr/lib/micropython
+    expose:
+      - "65433"
+    ports:
+      - "65433:65433" # reach "micropython-client" at 172.25.0.2:65433, see networks
+    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-host:
+    build:
+      context: .
+      dockerfile: Dockerfile.host_rtu
+    container_name: micropython-host
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+      - ./fakes:/usr/lib/micropython
+    depends_on:
+      - micropython-client
+    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
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 549527f..6a224d4 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -11,7 +11,7 @@ services:
   micropython-client:
     build:
       context: .
-      dockerfile: Dockerfile.client
+      dockerfile: Dockerfile.client_tcp
     container_name: micropython-client
     volumes:
       - ./:/home
@@ -29,7 +29,7 @@ services:
   micropython-host:
     build:
       context: .
-      dockerfile: Dockerfile.host
+      dockerfile: Dockerfile.host_tcp
     container_name: micropython-host
     volumes:
       - ./:/home
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 3e6c3c4..bbec012 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -224,18 +224,18 @@ be `percent`.
 
 ### Register usage
 
-This section describes the usage of the following available functions
-
- - [0x01 `read_coils`](umodbus.tcp.TCP.read_coils)
- - [0x02 `read_discrete_inputs`](umodbus.tcp.TCP.read_discrete_inputs)
- - [0x03 `read_holding_registers`](umodbus.tcp.TCP.read_holding_registers)
- - [0x04 `read_input_registers`](umodbus.tcp.TCP.read_input_registers)
- - [0x05 `write_single_coil`](umodbus.tcp.TCP.write_single_coil)
- - [0x06 `write_single_register`](umodbus.tcp.TCP.write_single_register)
- - [0x0F `write_multiple_coils`](umodbus.tcp.TCP.write_multiple_coils)
- - [0x10 `write_multiple_registers`](umodbus.tcp.TCP.write_multiple_registers)
-
-based on TCP togehter with the latest provided
+This section describes the usage of the following implemented functions
+
+ - [0x01 `read_coils`](umodbus.common.CommonModbusFunctions.read_coils)
+ - [0x02 `read_discrete_inputs`](umodbus.common.CommonModbusFunctions.read_discrete_inputs)
+ - [0x03 `read_holding_registers`](umodbus.common.CommonModbusFunctions.read_holding_registers)
+ - [0x04 `read_input_registers`](umodbus.common.CommonModbusFunctions.read_input_registers)
+ - [0x05 `write_single_coil`](umodbus.common.CommonModbusFunctions.write_single_coil)
+ - [0x06 `write_single_register`](umodbus.common.CommonModbusFunctions.write_single_register)
+ - [0x0F `write_multiple_coils`](umodbus.common.CommonModbusFunctions.write_multiple_coils)
+ - [0x10 `write_multiple_registers`](umodbus.common.CommonModbusFunctions.write_multiple_registers)
+
+which are available on Modbus RTU and Modbus TCP as shown in the
 [examples](https://github.com/brainelectronics/micropython-modbus/tree/develop/examples)
 
 All described functions require a successful setup of a Host communicating
@@ -270,7 +270,6 @@ slave_addr = 10                 # bus address of client
 # the following example is for an ESP32
 rtu_pins = (25, 26)         # (TX, RX)
 host = ModbusRTUMaster(
-    addr=1,                 # bus address of this Host/Master, usually '1'
     baudrate=9600,          # optional, default 9600
     pins=rtu_pins,          # given as tuple (TX, RX)
     # data_bits=8,          # optional, default 8
@@ -293,8 +292,9 @@ The function code `0x01` is used to read from 1 to 2000 contiguous status of
 coils in a remote device.
 ```
 
-With the function [`read_coils`](umodbus.tcp.TCP.read_coils) a single coil
-status can be read.
+With the function
+[`read_coils`](umodbus.common.CommonModbusFunctions.read_coils)
+a single coil status can be read.
 
 ```python
 coil_address = 125
@@ -331,7 +331,8 @@ The function code `0x05` is used to write a single output to either `ON` or
 `OFF` in a remote device.
 ```
 
-With the function [`write_single_coil`](umodbus.tcp.TCP.write_single_coil)
+With the function
+[`write_single_coil`](umodbus.common.CommonModbusFunctions.write_single_coil)
 a single coil status can be set.
 
 ```python
@@ -363,7 +364,8 @@ The function code `0x0F` is used to force each coil in a sequence of coils to
 either `ON` or `OFF` in a remote device.
 ```
 
-With the function [`write_multiple_coils`](umodbus.tcp.TCP.write_multiple_coils)
+With the function
+[`write_multiple_coils`](umodbus.common.CommonModbusFunctions.write_multiple_coils)
 multiple coil states can be set at once.
 
 ```python
@@ -401,7 +403,8 @@ The function code `0x02` is used to read from 1 to 2000 contiguous status of
 discrete inputs in a remote device.
 ```
 
-With the function [`read_discrete_inputs`](umodbus.tcp.TCP.read_discrete_inputs)
+With the function
+[`read_discrete_inputs`](umodbus.common.CommonModbusFunctions.read_discrete_inputs)
 discrete inputs can be read.
 
 ```python
@@ -429,8 +432,8 @@ of holding registers in a remote device.
 ```
 
 With the function
-[`read_holding_registers`](umodbus.tcp.TCP.read_holding_registers) a single
-holding register can be read.
+[`read_holding_registers`](umodbus.common.CommonModbusFunctions.read_holding_registers)
+a single holding register can be read.
 
 ```python
 hreg_address = 94
@@ -470,8 +473,8 @@ remote device.
 ```
 
 With the function
-[`write_single_register`](umodbus.tcp.TCP.write_single_register) a single
-holding register can be set.
+[`write_single_register`](umodbus.common.CommonModbusFunctions.write_single_register)
+a single holding register can be set.
 
 ```python
 hreg_address = 93
@@ -493,7 +496,7 @@ The function code `0x10` is used to write a block of contiguous registers
 ```
 
 With the function
-[`write_multiple_registers`](umodbus.tcp.TCP.write_multiple_registers)
+[`write_multiple_registers`](umodbus.common.CommonModbusFunctions.write_multiple_registers)
 holding register can be set at once.
 
 ```python
@@ -536,7 +539,8 @@ The function code `0x04` is used to read from 1 to 125 contiguous input
 registers in a remote device.
 ```
 
-With the function [`read_input_registers`](umodbus.tcp.TCP.read_input_registers)
+With the function
+[`read_input_registers`](umodbus.common.CommonModbusFunctions.read_input_registers)
 input registers can be read.
 
 ```python
@@ -609,15 +613,15 @@ Connected to WiFi.
 ('192.168.178.42', '255.255.255.0', '192.168.178.1', '192.168.178.1')
 Requesting and updating data on TCP client at 192.168.178.69:502
 
-Status of COIL 123: [True, False, False, False, False, False, False, False]
+Status of COIL 123: [True]
 Result of setting COIL 123: True
-Status of COIL 123: [False, False, False, False, False, False, False, False]
+Status of COIL 123: [False]
 
 Status of HREG 93: (44,)
 Result of setting HREG 93: True
 Status of HREG 93: (44,)
 
-Status of IST 67: [False, False, False, False, False, False, False, False]
+Status of IST 67: [False]
 Status of IREG 10: (60001,)
 
 Finished requesting/setting data on client
@@ -626,12 +630,74 @@ Type "help()" for more information.
 >>>
 ```
 
-<!--
 ### RTU
 
 Get two UART/RS485 capable boards up and running, collecting and setting data
 on each other.
--->
+
+Adjust the UART pins according to the MicroPython port specific
+[documentation][ref-uart-documentation]. RP2 boards e.g. require the UART pins
+as tuple of `Pin`, like `rtu_pins = (Pin(4), Pin(5))` and the specific
+`uart_id=1` for those, whereas ESP32 boards can use almost alls pins for UART
+communication and shall be given as `rtu_pins = (25, 26)`.
+
+#### Client
+
+The client, former known as slave, provides some dummy registers which can be
+read and updated by another device.
+
+```bash
+cp examples/rtu_client_example.py /pyboard/main.py
+cp examples/boot.py /pyboard/boot.py
+repl
+```
+
+Inside the REPL press CTRL+D to perform a soft reboot. The device will serve
+several registers now. The log output might look similar to this
+
+```
+MPY: soft reboot
+System booted successfully!
+Setting up registers ...
+Register setup done
+```
+
+#### Host
+
+The host, former known as master, requests and updates some dummy registers of
+another device.
+
+```bash
+cp examples/rtu_host_example.py /pyboard/main.py
+cp examples/boot.py /pyboard/boot.py
+repl
+```
+
+Inside the REPL press CTRL+D to perform a soft reboot. The device will request
+and update registers of the Client after a few seconds. The log output might
+look similar to this
+
+```
+MPY: soft reboot
+System booted successfully!
+Requesting and updating data on RTU client at 10 with 9600 baud.
+
+Status of COIL 123: [True]
+Result of setting COIL 123: True
+Status of COIL 123: [False]
+
+Status of HREG 93: (44,)
+Result of setting HREG 93: True
+Status of HREG 93: (44,)
+
+Status of IST 67: [False]
+Status of IREG 10: (60001,)
+
+Finished requesting/setting data on client
+MicroPython v1.18 on 2022-01-17; ESP32 module (spiram) with ESP32
+Type "help()" for more information.
+>>>
+```
 
 ### TCP-RTU bridge
 
@@ -777,14 +843,21 @@ failed.
 
 #### Docker compose
 
-The following command uses the setup defined in the `docker-compose.yaml` file
-to act as two MicroPython devices communicating via TCP. The container
-`micropython-host` defined by `Dockerfile.host` acts as host and sets/gets
-data at/from the client as defined by `tcp_host_example.py`. On the other hand
-the container `micropython-client` defined by `Dockerfile.client` acts as
-client and provides data for the host as defined by `tcp_client_example.py`.
+The following command uses the setup defined in the individual
+`docker-compose-*-test.yaml` file to act as two MicroPython devices
+communicating via TCP or RTU. The container `micropython-host-*` defined by
+`Dockerfile.host_*` acts as host and sets/gets data at/from the client as
+defined by `*_host_example.py`. On the other hand the container
+`micropython-client-*` defined by `Dockerfile.client_*` acts as client and
+provides data for the host as defined by `*_client_example.py`.
+
 The port defined in `tcp_host_example.py` and `tcp_client_example.py` has to
-be open and optionally exposed in the `docker-compose.yaml` file.
+be open and optionally exposed in the `docker-compose-tcp-example.yaml` file.
+
+As the [MicroPython containers](https://hub.docker.com/r/micropython/unix/tags)
+does not have a UART interface with is additionally not connectable via two
+containers a UART fake has been implemented. It is using a socket connection
+to exchange all the data.
 
 ```bash
 docker compose up --build --exit-code-from micropython-host
@@ -796,7 +869,13 @@ the containers. All "dynamic" data is shared via `volumes`
 ##### Test for TCP example
 
 ```bash
-docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host
+docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host --remove-orphans
+```
+
+##### Test for RTU example
+
+```bash
+docker compose -f docker-compose-rtu-test.yaml up --build --exit-code-from micropython-host-rtu --remove-orphans
 ```
 
 ## Documentation
@@ -835,6 +914,7 @@ The created documentation can be found at [`docs/build/html`](docs/build/html).
 [ref-myevse-be]: https://brainelectronics.de/
 [ref-myevse-tindie]: https://www.tindie.com/stores/brainelectronics/
 [ref-package-main-file]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/main.py
+[ref-uart-documentation]: https://docs.micropython.org/en/latest/library/machine.UART.html
 [ref-github-be-modbus-wrapper]: https://github.com/brainelectronics/be-modbus-wrapper
 [ref-modules-folder]: https://github.com/brainelectronics/python-modules/tree/43bad716b7db27db07c94c2d279cee57d0c8c753
 [ref-rtd-micropython-modbus]: https://micropython-modbus.readthedocs.io/en/latest/
diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py
index acc93b9..d30ced4 100644
--- a/examples/rtu_client_example.py
+++ b/examples/rtu_client_example.py
@@ -18,6 +18,18 @@
 # 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
@@ -41,6 +53,10 @@
     # uart_id=1             # optional, see port specific documentation
 )
 
+if IS_DOCKER_MICROPYTHON:
+    # works only with fake machine UART
+    assert client._itf._uart._is_server is True
+
 # common slave register setup, to be used with the Master example above
 register_definitions = {
     "COILS": {
@@ -78,20 +94,35 @@
     }
 }
 
-"""
 # alternatively the register definitions can also be loaded from a JSON file
-import json
-
-with open('registers/example.json', 'r') as file:
-    register_definitions = json.load(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)
 
+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')
+
+reset_data_register = \
+    register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']
 
 while True:
-    result = client.process()
+    try:
+        result = client.process()
+        if reset_data_register in client.coils:
+            if client.get_coil(address=reset_data_register):
+                print('Resetting register data to default values ...')
+                client.setup_registers(registers=register_definitions)
+                print('Default values restored')
+    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")
diff --git a/examples/rtu_host_example.py b/examples/rtu_host_example.py
new file mode 100644
index 0000000..e8d3106
--- /dev/null
+++ b/examples/rtu_host_example.py
@@ -0,0 +1,215 @@
+#!/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 host (master) which requests or sets data on a client
+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.
+"""
+
+# system packages
+import time
+
+# import modbus host classes
+from umodbus.serial import Serial as ModbusRTUMaster
+
+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 sys
+
+
+# ===============================================
+# RTU Slave setup
+slave_addr = 10             # address on bus of the client/slave
+
+# RTU Master setup
+# act as host, collect Modbus data via RTU from a client device
+# ModbusRTU can perform serial requests to a client device to get/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
+rtu_pins = (25, 26)         # (TX, RX)
+baudrate = 9600
+host = ModbusRTUMaster(
+    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=12,          # optional, control DE/RE
+    # uart_id=1             # optional, see port specific documentation
+)
+
+if IS_DOCKER_MICROPYTHON:
+    # works only with fake machine UART
+    assert host._uart._is_server is False
+
+# commond 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": 2,
+            "val": 60001
+        }
+    }
+}
+
+"""
+# alternatively the register definitions can also be loaded from a JSON file
+import json
+
+with open('registers/example.json', 'r') as file:
+    register_definitions = json.load(file)
+"""
+
+print('Requesting and updating data on RTU client at {} with {} baud'.
+      format(slave_addr, baudrate))
+print()
+
+# READ COILS
+coil_address = register_definitions['COILS']['EXAMPLE_COIL']['register']
+coil_qty = register_definitions['COILS']['EXAMPLE_COIL']['len']
+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))
+time.sleep(1)
+
+# WRITE COILS
+new_coil_val = 0
+operation_status = host.write_single_coil(
+    slave_addr=slave_addr,
+    output_address=coil_address,
+    output_value=new_coil_val)
+print('Result of setting coil {} to {}'.format(coil_address, operation_status))
+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_status, coil_address))
+time.sleep(1)
+
+print()
+
+# READ HREGS
+hreg_address = register_definitions['HREGS']['EXAMPLE_HREG']['register']
+register_qty = register_definitions['HREGS']['EXAMPLE_HREG']['len']
+register_value = host.read_holding_registers(
+    slave_addr=slave_addr,
+    starting_addr=hreg_address,
+    register_qty=register_qty,
+    signed=False)
+print('Status of hreg {}: {}'.format(hreg_address, register_value))
+time.sleep(1)
+
+# WRITE HREGS
+new_hreg_val = 44
+operation_status = host.write_single_register(
+    slave_addr=slave_addr,
+    register_address=hreg_address,
+    register_value=new_hreg_val,
+    signed=False)
+print('Result of setting hreg {} to {}'.format(hreg_address, operation_status))
+time.sleep(1)
+
+# READ HREGS again
+register_value = host.read_holding_registers(
+    slave_addr=slave_addr,
+    starting_addr=hreg_address,
+    register_qty=register_qty,
+    signed=False)
+print('Status of hreg {}: {}'.format(hreg_address, register_value))
+time.sleep(1)
+
+print()
+
+# READ ISTS
+ist_address = register_definitions['ISTS']['EXAMPLE_ISTS']['register']
+input_qty = register_definitions['ISTS']['EXAMPLE_ISTS']['len']
+input_status = host.read_discrete_inputs(
+    slave_addr=slave_addr,
+    starting_addr=ist_address,
+    input_qty=input_qty)
+print('Status of ist {}: {}'.format(ist_address, input_status))
+time.sleep(1)
+
+# READ IREGS
+ireg_address = register_definitions['IREGS']['EXAMPLE_IREG']['register']
+register_qty = register_definitions['IREGS']['EXAMPLE_IREG']['len']
+register_value = host.read_input_registers(
+    slave_addr=slave_addr,
+    starting_addr=ireg_address,
+    register_qty=register_qty,
+    signed=False)
+print('Status of ireg {}: {}'.format(ireg_address, register_value))
+time.sleep(1)
+
+print()
+
+# reset all registers back to their default values on the client
+# WRITE COILS
+print('Resetting register data to default values...')
+coil_address = \
+    register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']
+new_coil_val = True
+operation_status = host.write_single_coil(
+    slave_addr=slave_addr,
+    output_address=coil_address,
+    output_value=new_coil_val)
+print('Result of setting COIL {}: {}'.format(coil_address, operation_status))
+time.sleep(1)
+
+print()
+
+print("Finished requesting/setting data on client")
+
+if IS_DOCKER_MICROPYTHON:
+    sys.exit(0)
diff --git a/fakes/machine.py b/fakes/machine.py
new file mode 100755
index 0000000..1053f66
--- /dev/null
+++ b/fakes/machine.py
@@ -0,0 +1,487 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+
+"""Fakes of several MicroPython classes which are not available in UNIX"""
+
+import _thread
+try:
+    import ulogging as logging
+    from umodbus.typing import Optional, Union
+except ImportError:
+    import logging
+    from typing import Optional, Union
+from queue import Queue
+import socket
+import sys
+import time
+
+
+class MachineError(Exception):
+    """Base class for exceptions in this module."""
+    pass
+
+
+class UART(object):
+    """
+    Fake Micropython UART class
+
+    See https://docs.micropython.org/en/latest/library/machine.UART.html
+    """
+    def __init__(self,
+                 uart_id: int,
+                 baudrate: int = 9600,
+                 bits: int = 8,
+                 parity: int = None,
+                 stop: int = 1,
+                 tx: int = 1,
+                 rx: int = 2,
+                 timeout: int = 0) -> None:
+        self._uart_id = uart_id
+        if timeout == 0:
+            timeout = 5.0
+        self._timeout = timeout
+
+        self.logger = self._configure_logger()
+
+        # Standard loopback interface address (localhost)
+        # Port to listen on (non-privileged ports are > 1023)
+        if sys.implementation.name.lower() == 'micropython':
+            # on MicroPython connect to the (potentially) running TCP server
+            # see docker-compose-rtu-test.yaml at micropython-client-rtu
+            self._host = '172.25.0.2'
+        else:
+            # on non MicroPython system connect to localhost
+            self._host = '127.0.0.1'
+        self._port = 65433
+        self._sock = None
+
+        self._server_lock = _thread.allocate_lock()
+        self._send_queue = Queue(maxsize=10)
+        self._receive_queue = Queue(maxsize=10)
+
+        self._is_server = self.no_socket_server_exists(
+            host=self._host,
+            port=self._port)
+
+        self.logger.debug('Timeout is: {}'.format(self._timeout))
+
+        if self._is_server:
+            self._sock = socket.socket()
+
+            if sys.implementation.name.lower() == 'micropython':
+                # MicroPython requires a bytearray
+                # https://docs.micropython.org/en/v1.18/library/socket.html#socket.getaddrinfo
+                addr_info = socket.getaddrinfo(self._host, self._port)[0][-1]
+            else:
+                addr_info = ((self._host, self._port))
+
+            self._sock.bind(addr_info)
+            self._sock.listen(10)
+            # self._sock.settimeout(self._timeout)
+            self._sock.setblocking(False)
+
+            self.logger.debug('Bound to {} on {}'.
+                              format(self._host, self._port))
+
+            # start the socket server thread as soon as init is done
+            self.serving = True
+        else:
+            self._sock = socket.socket()
+            self._sock.settimeout(self._timeout)
+            # self._sock.setblocking(False)
+
+            if sys.implementation.name.lower() == 'micropython':
+                # MicroPython requires a bytearray
+                # https://docs.micropython.org/en/v1.18/library/socket.html#socket.socket.connect
+                addr_info = socket.getaddrinfo(self._host, self._port)[0][-1]
+            else:
+                addr_info = ((self._host, self._port))
+
+            self._sock.connect(addr_info)
+            self.logger.debug('Connecting to {} on {}'.
+                              format(self._host, self._port))
+
+        self.logger.debug('Will act as {}'.
+                          format('server' if self._is_server else 'client'))
+
+    def _configure_logger(self) -> logging.Logger:
+        """
+        Create and configure a logger
+
+        :returns:   Configured logger
+        :rtype:     logging.Logger
+        """
+        if sys.implementation.name.lower() == 'micropython':
+            logging.basicConfig(level=logging.INFO)
+        else:
+            custom_format = '[%(asctime)s] [%(levelname)-8s] [%(filename)-15s'\
+                            ' @ %(funcName)-15s:%(lineno)4s] %(message)s'
+            logging.basicConfig(level=logging.INFO,
+                                format=custom_format,
+                                stream=sys.stdout)
+
+        logger = logging.getLogger(__name__)
+        logger.setLevel(logging.DEBUG)
+
+        return logger
+
+    def no_socket_server_exists(self,
+                                host: str,
+                                port: int,
+                                timeout: Optional[float] = 1.0) -> bool:
+        """
+        Determines if no socket server exists.
+
+        :param      host:     The host IP
+        :type       host:     str
+        :param      port:     The port
+        :type       port:     int
+        :param      timeout:  The timeout
+        :type       timeout:  float
+
+        :returns:   True if no socket server exists, False otherwise.
+        :rtype:     bool
+        """
+        become_server = True
+
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(timeout)
+
+        try:
+            if sys.implementation.name.lower() == 'micropython':
+                # MicroPython requires a bytearray
+                # https://docs.micropython.org/en/v1.18/library/socket.html#socket.socket.connect
+                addr_info = socket.getaddrinfo(host, port)[0][-1]
+            else:
+                addr_info = ((host, port))
+            sock.connect(addr_info)
+        except Exception:
+            self.logger.debug('Exception during potential server connect')
+            self.logger.debug('Failed to connect to {} on {}'.
+                              format(host, port))
+            sock.close()
+            # assume no server is available
+            # this device shall thereby become a server itself
+            return become_server
+
+        self.logger.debug('Connection to server successful')
+
+        # send test message to server
+        sock.send(b'Ping. Hello UART server?')  # not the best message
+        self.logger.debug('Sent ping to potential server')
+
+        # wait for specified timeout for a response from the server
+        try:
+            received_data = sock.recv(127)
+            if received_data:
+                self.logger.info('Received as response to ping: {}'.
+                                 format(received_data))
+                if received_data == b'pong':
+                    # a potential server shall respond with b'pong'.
+                    # The return value shall thereby by "False", as the
+                    # check for no socket server existance failed
+                    become_server = False
+        except OSError as e:
+            # 11 = timeout expired (MicroPython)
+            # 35 = Resource temporarily unavailable (Python OS X)
+            # "timed out" (Python Windows)
+            if e.args[0] in [11, 35, 'timed out']:
+                self.logger.debug('Timeout waiting for server response')
+            else:
+                raise e
+
+        sock.close()
+
+        return become_server
+
+    @property
+    def is_server(self) -> bool:
+        """
+        Determines if this object acts as server aka UART host.
+
+        :returns:   True if server, False otherwise.
+        :rtype:     bool
+        """
+        return self._is_server
+
+    @property
+    def serving(self) -> bool:
+        """
+        Get the server status.
+
+        :returns:   Flag whether socket server is running or not.
+        :rtype:     bool
+        """
+        return self._server_lock.locked()
+
+    @serving.setter
+    def serving(self, value: bool) -> None:
+        """
+        Start or stop running the socket server.
+
+        :param      value:  The value
+        :type       value:  bool
+        """
+        if value and (not self._server_lock.locked()):
+            # start socket server if not already running
+            self._server_lock.acquire()
+
+            # parameters of the _serve function
+            params = (
+                self._sock,
+                self._send_queue,
+                self._receive_queue,
+                self._server_lock,
+                self.logger
+            )
+            _thread.start_new_thread(self._serve, params)
+            self.logger.info('Socket {} started'.
+                             format('server' if self._is_server else 'client'))
+        elif (value is False) and self._server_lock.locked():
+            # stop server if not already stopped
+            self._server_lock.release()
+            self.logger.info('Socket {} lock released'.
+                             format('server' if self._is_server else 'client'))
+
+    def _serve(self,
+               sock: socket,
+               send_queue: Queue,
+               receive_queue: Queue,
+               lock: int,
+               logger: logging.Logger) -> None:
+        """
+        Internal socket server function
+
+        :param      sock:           The socket
+        :type       sock:           socket
+        :param      send_queue:     The send queue
+        :type       send_queue:     Queue
+        :param      receive_queue:  The receive queue
+        :type       receive_queue:  Queue
+        :param      lock:           The lock flag
+        :type       lock:           int
+        :param      logger:         The logger
+        :type       logger:         logging.Logger
+        """
+        if sock is None:
+            raise Exception('Server not bound')
+        else:
+            logger.info('Acting as server')
+
+        _client_sock = None
+
+        while lock.locked():
+            new_client_sock = None
+
+            try:
+                # either a connection is made from a client or
+                # an OSError is thrown after the configured timeout
+                new_client_sock, addr = sock.accept()
+            except OSError as e:
+                # 11 = timeout expired (MicroPython)
+                # 35 = Resource temporarily unavailable (Python OS X)
+                # "timed out" (Python Windows)
+                if e.args[0] not in [11, 35, 'timed out']:
+                    logger.warning('OSError {}: {}'.
+                                   format(e, e.args[0]))
+
+            if new_client_sock is not None:
+                logger.debug('Connection from {}'.format(addr))
+                if _client_sock is not None:
+                    logger.debug('Closed connection to last client')
+                    _client_sock.close()
+
+                _client_sock = new_client_sock
+                _client_sock.settimeout(0.5)
+
+            if _client_sock is not None:
+                # get client message
+                try:
+                    received_data = _client_sock.recv(127)
+
+                    # log data and send response
+                    if received_data:
+                        logger.info('Received from client: {}'.
+                                    format(received_data))
+
+                        if received_data == b'Ping. Hello UART server?':
+                            _client_sock.send(b'pong')
+                            logger.info('Responded with "pong"')
+                        else:
+                            receive_queue.put_nowait(received_data)
+
+                            # grant host enough time to process received
+                            # data, prepare a response for the client and put
+                            # it into the send_queue
+                            # Sleep time shall be less than client timeout
+                            # specified during UART init
+                            # Approx. process time is below 100ms
+                            time.sleep(0.1)
+                except Exception as e:
+                    logger.debug('Error during connection: {}'.format(e))
+                    _client_sock.close()
+                    _client_sock = None
+
+                if send_queue.qsize():
+                    send_data = send_queue.get_nowait()
+
+                    logger.info('Responding to client: {}'.
+                                format(send_data))
+                    _client_sock.send(send_data)
+
+    def deinit(self) -> None:
+        """Turn off the UART bus"""
+        raise MachineError('Not yet implemented')
+
+    def any(self) -> int:
+        """
+        Return number of characters available.
+
+        Mock does not return the actual number of characters but the elements
+        in the receive queue. Which is usually "0" or "1"
+
+        :returns:   Number of characters available
+        :rtype:     int
+        """
+        available_data_amount = 0
+
+        if self._is_server:
+            available_data_amount = self._receive_queue.qsize()
+        else:
+            # patch available data, as reading the socket buffer would drain it
+            available_data_amount = 1
+
+        return available_data_amount
+
+    def read(self, nbytes: Optional[int] = None) -> Union[None, bytes]:
+        """
+        Read characters
+
+        :param      nbytes:  The amount of bytes to read
+        :type       nbytes:  int
+
+        :returns:   Bytes read, None on timeout
+        :rtype:     Union[None, bytes]
+        """
+        data = None
+
+        if self._is_server:
+            if self._receive_queue.qsize():
+                data = self._receive_queue.get_nowait()
+        else:
+            try:
+                data = self._sock.recv(127)
+            except OSError as e:
+                self.logger.warning('OSError {}: {}'.format(e, e.args[0]))
+
+        return data
+
+    def readinto(self,
+                 buf: bytes,
+                 nbytes: Optional[int] = None) -> Union[None, bytes]:
+        """
+        Read bytes into the buffer.
+
+        If nbytes is specified then read at most that many bytes. Otherwise,
+        read at most len(buf) bytes
+
+        :param      buf:     The buffer
+        :type       buf:     bytes
+        :param      nbytes:  The nbytes
+        :type       nbytes:  Optional[int]
+
+        :returns:   Number of bytes read and stored in buffer, None on timeout
+        :rtype:     Union[None, bytes]
+        """
+        raise MachineError('Not yet implemented')
+
+    def readline(self) -> Union[None, str]:
+        """
+        Read a line, ending in a newline character
+
+        :returns:   The line read, None on timeout.
+        :rtype:     Union[None, str]
+        """
+        raise MachineError('Not yet implemented')
+
+    def write(self, buf: bytes) -> Union[None, int]:
+        """
+        Write the buffer of bytes to the bus
+
+        :param      buf:  The buffer
+        :type       buf:  bytes
+
+        :returns:   Number of bytes written, None on timeout
+        :rtype:     Union[None, int]
+        """
+        if self._is_server:
+            self._send_queue.put_nowait(buf)
+        else:
+            self._sock.send(buf)
+
+        return len(buf)
+
+    def sendbreak(self) -> None:
+        """Send a break condition on the bus"""
+        raise MachineError('Not yet implemented')
+
+    def flush(self) -> None:
+        """
+        Waits until all data has been sent
+
+        In case of a timeout, an exception is raised
+
+        Only available with newer versions than 1.19
+        """
+        raise MachineError('Not yet implemented')
+
+    def txdone(self) -> bool:
+        """
+        Tells whether all data has been sent or no data transfer is happening
+
+        :returns:   If data transmission is ngoing return False, True otherwise
+        :rtype:     bool
+        """
+        raise MachineError('Not yet implemented')
+
+
+class Pin(object):
+    """
+    Fake Micropython Pin class
+
+    See https://docs.micropython.org/en/latest/library/machine.Pin.html
+    """
+    IN = 1
+    OUT = 2
+
+    def __init__(self, pin: int, mode: int):
+        self._pin = pin
+        self._mode = mode
+        self._value = False
+
+    def value(self, val: Optional[Union[int, bool]] = None) -> Optional[bool]:
+        """
+        Set or get the value of the pin
+
+        :param      val:  The value
+        :type       val:  Optional[Union[int, bool]]
+
+        :returns:   State of the pin if no value specifed, None otherwise
+        :rtype:     Optional[bool]
+        """
+        if val is not None and self._mode == Pin.OUT:
+            # set pin state
+            self._value = bool(val)
+        else:
+            # get pin state
+            return self._value
+
+    def on(self) -> None:
+        """Set pin to "1" output level"""
+        if self._mode == Pin.OUT:
+            self.value(val=True)
+
+    def off(self) -> None:
+        """Set pin to "0" output level"""
+        if self._mode == Pin.OUT:
+            self.value(val=False)
diff --git a/fakes/queue.py b/fakes/queue.py
new file mode 100755
index 0000000..d0b2156
--- /dev/null
+++ b/fakes/queue.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+
+"""
+queue.py: adapted from uasyncio V2
+
+Copyright (c) 2018-2020 Peter Hinch
+Released under the MIT License (MIT) - see LICENSE file
+
+Code is based on Paul Sokolovsky's work.
+This is a temporary solution until uasyncio V3 gets an efficient official
+version
+"""
+
+try:
+    import uasyncio as asyncio
+except ImportError:
+    import asyncio
+
+
+class QueueEmpty(Exception):
+    """Exception raised by get_nowait()"""
+    pass
+
+
+class QueueFull(Exception):
+    """Exception raised by put_nowait()"""
+    pass
+
+
+class Queue:
+    """AsyncIO based Queue"""
+    def __init__(self, maxsize: int = 0):
+        self.maxsize = maxsize
+        self._queue = []
+        self._evput = asyncio.Event()  # Triggered by put, tested by get
+        self._evget = asyncio.Event()  # Triggered by get, tested by put
+
+    def _get(self):
+        """
+        Remove and return an item from the queue without blocking
+
+        :returns:   Return an item if one is immediately available
+        :rtype:     Any
+        """
+        # Schedule all tasks waiting on get
+        self._evget.set()
+        self._evget.clear()
+        return self._queue.pop(0)
+
+    async def get(self):
+        """
+        Remove and return an item from the queue in async blocking mode
+
+        Usage: item = await queue.get()
+
+        :returns:   Return an item if one is immediately available
+        :rtype:     Any
+        """
+        # May be multiple tasks waiting on get()
+        while self.empty():
+            # Queue is empty, suspend task until a put occurs
+            # 1st of N tasks gets, the rest loop again
+            await self._evput.wait()
+        return self._get()
+
+    def get_nowait(self):
+        """
+        Remove and return an item from the queue without blocking
+
+        :returns:   Return an item if one is immediately available
+        :rtype:     Any
+
+        :raises     QueueEmpty:  Queue is empty
+        :raises     QueueFull:   Queue is full
+        """
+        if self.empty():
+            raise QueueEmpty()
+        return self._get()
+
+    def _put(self, val) -> None:
+        """
+        Put an item into the queue without blocking
+
+        :param      val:        The value
+        :type       val:        Any
+        """
+        # Schedule tasks waiting on put
+        self._evput.set()
+        self._evput.clear()
+        self._queue.append(val)
+
+    async def put(self, val) -> None:
+        """
+        Put an item into the queue in async blocking mode
+
+        Usage: await queue.put(item)
+
+        :param      val:        The value
+        :type       val:        Any
+
+        :raises     QueueFull:  Queue is full
+        """
+        while self.full():
+            # Queue full
+            await self._evget.wait()
+            # Task(s) waiting to get from queue, schedule first Task
+        self._put(val)
+
+    def put_nowait(self, val) -> None:
+        """
+        Put an item into the queue without blocking
+
+        :param      val:        The value
+        :type       val:        Any
+
+        :raises     QueueFull:  Queue is full
+        """
+        if self.full():
+            raise QueueFull()
+        self._put(val)
+
+    def qsize(self) -> int:
+        """
+        Get number of items in the queue
+
+        :returns:   Number of items in the queue
+        :rtype:     int
+        """
+        return len(self._queue)
+
+    def empty(self) -> bool:
+        """
+        Check is queue is empty
+
+        :returns:   Return True if the queue is empty, False otherwise
+        :rtype:     bool
+        """
+        return len(self._queue) == 0
+
+    def full(self) -> bool:
+        """
+        Check if queue is full
+
+        Note: if the Queue was initialized with maxsize=0 (the default) or
+        any negative number, then full() is never True.
+
+        :returns:   Return True if there are maxsize items in the queue.
+        :rtype:     bool
+        """
+        return self.maxsize > 0 and self.qsize() >= self.maxsize
diff --git a/umodbus/common.py b/umodbus/common.py
index 59a7b04..a5ebe9f 100644
--- a/umodbus/common.py
+++ b/umodbus/common.py
@@ -13,9 +13,10 @@
 
 # custom packages
 from . import const as Const
+from . import functions
 
 # typing not natively supported on MicroPython
-from .typing import Optional
+from .typing import List, Optional, Tuple, Union
 
 
 class Request(object):
@@ -105,3 +106,287 @@ class ModbusException(Exception):
     def __init__(self, function_code: int, exception_code: int) -> None:
         self.function_code = function_code
         self.exception_code = exception_code
+
+
+class CommonModbusFunctions(object):
+    """Common Modbus functions"""
+    def __init__(self):
+        pass
+
+    def read_coils(self,
+                   slave_addr: int,
+                   starting_addr: int,
+                   coil_qty: int) -> List[bool]:
+        """
+        Read coils (COILS).
+
+        :param      slave_addr:     The slave address
+        :type       slave_addr:     int
+        :param      starting_addr:  The coil starting address
+        :type       starting_addr:  int
+        :param      coil_qty:       The amount of coils to read
+        :type       coil_qty:       int
+
+        :returns:   State of read coils as list
+        :rtype:     List[bool]
+        """
+        modbus_pdu = functions.read_coils(starting_address=starting_addr,
+                                          quantity=coil_qty)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=True)
+
+        status_pdu = functions.bytes_to_bool(byte_list=response,
+                                             bit_qty=coil_qty)
+
+        return status_pdu
+
+    def read_discrete_inputs(self,
+                             slave_addr: int,
+                             starting_addr: int,
+                             input_qty: int) -> List[bool]:
+        """
+        Read discrete inputs (ISTS).
+
+        :param      slave_addr:     The slave address
+        :type       slave_addr:     int
+        :param      starting_addr:  The discrete input starting address
+        :type       starting_addr:  int
+        :param      input_qty:      The amount of discrete inputs to read
+        :type       input_qty:      int
+
+        :returns:   State of read discrete inputs as list
+        :rtype:     List[bool]
+        """
+        modbus_pdu = functions.read_discrete_inputs(
+            starting_address=starting_addr,
+            quantity=input_qty)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=True)
+
+        status_pdu = functions.bytes_to_bool(byte_list=response,
+                                             bit_qty=input_qty)
+
+        return status_pdu
+
+    def read_holding_registers(self,
+                               slave_addr: int,
+                               starting_addr: int,
+                               register_qty: int,
+                               signed: bool = True) -> Tuple[int, ...]:
+        """
+        Read holding registers (HREGS).
+
+        :param      slave_addr:     The slave address
+        :type       slave_addr:     int
+        :param      starting_addr:  The holding register starting address
+        :type       starting_addr:  int
+        :param      register_qty:   The amount of holding registers to read
+        :type       register_qty:   int
+        :param      signed:         Indicates if signed
+        :type       signed:         bool
+
+        :returns:   State of read holding register as tuple
+        :rtype:     Tuple[int, ...]
+        """
+        modbus_pdu = functions.read_holding_registers(
+            starting_address=starting_addr,
+            quantity=register_qty)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=True)
+
+        register_value = functions.to_short(byte_array=response, signed=signed)
+
+        return register_value
+
+    def read_input_registers(self,
+                             slave_addr: int,
+                             starting_addr: int,
+                             register_qty: int,
+                             signed: bool = True) -> Tuple[int, ...]:
+        """
+        Read input registers (IREGS).
+
+        :param      slave_addr:     The slave address
+        :type       slave_addr:     int
+        :param      starting_addr:  The input register starting address
+        :type       starting_addr:  int
+        :param      register_qty:   The amount of input registers to read
+        :type       register_qty:   int
+        :param      signed:         Indicates if signed
+        :type       signed:         bool
+
+        :returns:   State of read input register as tuple
+        :rtype:     Tuple[int, ...]
+        """
+        modbus_pdu = functions.read_input_registers(
+            starting_address=starting_addr,
+            quantity=register_qty)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=True)
+
+        register_value = functions.to_short(byte_array=response, signed=signed)
+
+        return register_value
+
+    def write_single_coil(self,
+                          slave_addr: int,
+                          output_address: int,
+                          output_value: Union[int, bool]) -> bool:
+        """
+        Update a single coil.
+
+        :param      slave_addr:      The slave address
+        :type       slave_addr:      int
+        :param      output_address:  The output address
+        :type       output_address:  int
+        :param      output_value:    The output value
+        :type       output_value:    Union[int, bool]
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
+        modbus_pdu = functions.write_single_coil(output_address=output_address,
+                                                 output_value=output_value)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=False)
+
+        if response is None:
+            return False
+
+        operation_status = functions.validate_resp_data(
+            data=response,
+            function_code=Const.WRITE_SINGLE_COIL,
+            address=output_address,
+            value=output_value,
+            signed=False)
+
+        return operation_status
+
+    def write_single_register(self,
+                              slave_addr: int,
+                              register_address: int,
+                              register_value: int,
+                              signed: bool = True) -> bool:
+        """
+        Update a single register.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      register_address:  The register address
+        :type       register_address:  int
+        :param      register_value:    The register value
+        :type       register_value:    int
+        :param      signed:            Indicates if signed
+        :type       signed:            bool
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
+        modbus_pdu = functions.write_single_register(
+            register_address=register_address,
+            register_value=register_value,
+            signed=signed)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=False)
+
+        if response is None:
+            return False
+
+        operation_status = functions.validate_resp_data(
+            data=response,
+            function_code=Const.WRITE_SINGLE_REGISTER,
+            address=register_address,
+            value=register_value,
+            signed=signed)
+
+        return operation_status
+
+    def write_multiple_coils(self,
+                             slave_addr: int,
+                             starting_address: int,
+                             output_values: List[Union[int, bool]]) -> bool:
+        """
+        Update multiple coils.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The address of the first coil
+        :type       starting_address:  int
+        :param      output_values:     The output values
+        :type       output_values:     List[Union[int, bool]]
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
+        modbus_pdu = functions.write_multiple_coils(
+            starting_address=starting_address,
+            value_list=output_values)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=False)
+
+        if response is None:
+            return False
+
+        operation_status = functions.validate_resp_data(
+            data=response,
+            function_code=Const.WRITE_MULTIPLE_COILS,
+            address=starting_address,
+            quantity=len(output_values))
+
+        return operation_status
+
+    def write_multiple_registers(self,
+                                 slave_addr: int,
+                                 starting_address: int,
+                                 register_values: List[int],
+                                 signed: bool = True) -> bool:
+        """
+        Update multiple registers.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The starting address
+        :type       starting_address:  int
+        :param      register_values:   The register values
+        :type       register_values:   List[int]
+        :param      signed:            Indicates if signed
+        :type       signed:            bool
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
+        modbus_pdu = functions.write_multiple_registers(
+            starting_address=starting_address,
+            register_values=register_values,
+            signed=signed)
+
+        response = self._send_receive(slave_addr=slave_addr,
+                                      modbus_pdu=modbus_pdu,
+                                      count=False)
+
+        if response is None:
+            return False
+
+        operation_status = functions.validate_resp_data(
+            data=response,
+            function_code=Const.WRITE_MULTIPLE_REGISTERS,
+            address=starting_address,
+            quantity=len(register_values),
+            signed=signed
+        )
+
+        return operation_status
diff --git a/umodbus/serial.py b/umodbus/serial.py
index 7d21e3b..d24b981 100644
--- a/umodbus/serial.py
+++ b/umodbus/serial.py
@@ -18,12 +18,12 @@
 # custom packages
 from . import const as Const
 from . import functions
-from .common import Request
+from .common import Request, CommonModbusFunctions
 from .common import ModbusException
 from .modbus import Modbus
 
 # typing not natively supported on MicroPython
-from .typing import List, Optional, Tuple, Union
+from .typing import List, Optional, Union
 
 
 class ModbusRTU(Modbus):
@@ -69,7 +69,7 @@ def __init__(self,
         )
 
 
-class Serial(object):
+class Serial(CommonModbusFunctions):
     def __init__(self,
                  uart_id: int = 1,
                  baudrate: int = 9600,
@@ -333,265 +333,6 @@ def _validate_resp_hdr(self,
 
         return response[hdr_length:len(response) - Const.CRC_LENGTH]
 
-    def read_coils(self,
-                   slave_addr: int,
-                   starting_addr: int,
-                   coil_qty: int) -> List[bool]:
-        """
-        Read coils (COILS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The coil starting address
-        :type       starting_addr:  int
-        :param      coil_qty:       The amount of coils to read
-        :type       coil_qty:       int
-
-        :returns:   State of read coils as list
-        :rtype:     List[bool]
-        """
-        modbus_pdu = functions.read_coils(starting_address=starting_addr,
-                                          quantity=coil_qty)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=True)
-        status_pdu = functions.bytes_to_bool(byte_list=resp_data,
-                                             bit_qty=coil_qty)
-
-        return status_pdu
-
-    def read_discrete_inputs(self,
-                             slave_addr: int,
-                             starting_addr: int,
-                             input_qty: int) -> List[bool]:
-        """
-        Read discrete inputs (ISTS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The discrete input starting address
-        :type       starting_addr:  int
-        :param      input_qty:      The amount of discrete inputs to read
-        :type       input_qty:      int
-
-        :returns:   State of read discrete inputs as list
-        :rtype:     List[bool]
-        """
-        modbus_pdu = functions.read_discrete_inputs(
-            starting_address=starting_addr,
-            quantity=input_qty)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=True)
-        status_pdu = functions.bytes_to_bool(byte_list=resp_data,
-                                             bit_qty=input_qty)
-
-        return status_pdu
-
-    def read_holding_registers(self,
-                               slave_addr: int,
-                               starting_addr: int,
-                               register_qty: int,
-                               signed: bool = True) -> Tuple[int, ...]:
-        """
-        Read holding registers (HREGS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The holding register starting address
-        :type       starting_addr:  int
-        :param      register_qty:   The amount of holding registers to read
-        :type       register_qty:   int
-        :param      signed:         Indicates if signed
-        :type       signed:         bool
-
-        :returns:   State of read holding register as tuple
-        :rtype:     Tuple[int, ...]
-        """
-        modbus_pdu = functions.read_holding_registers(
-            starting_address=starting_addr,
-            quantity=register_qty)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=True)
-        register_value = functions.to_short(byte_array=resp_data,
-                                            signed=signed)
-
-        return register_value
-
-    def read_input_registers(self,
-                             slave_addr: int,
-                             starting_addr: int,
-                             register_qty: int,
-                             signed: bool = True) -> Tuple[int, ...]:
-        """
-        Read input registers (IREGS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The input register starting address
-        :type       starting_addr:  int
-        :param      register_qty:   The amount of input registers to read
-        :type       register_qty:   int
-        :param      signed:         Indicates if signed
-        :type       signed:         bool
-
-        :returns:   State of read input register as tuple
-        :rtype:     Tuple[int, ...]
-        """
-        modbus_pdu = functions.read_input_registers(
-            starting_address=starting_addr,
-            quantity=register_qty)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=True)
-        register_value = functions.to_short(byte_array=resp_data,
-                                            signed=signed)
-
-        return register_value
-
-    def write_single_coil(self,
-                          slave_addr: int,
-                          output_address: int,
-                          output_value: Union[int, bool]) -> bool:
-        """
-        Update a single coil.
-
-        :param      slave_addr:      The slave address
-        :type       slave_addr:      int
-        :param      output_address:  The output address
-        :type       output_address:  int
-        :param      output_value:    The output value
-        :type       output_value:    Union[int, bool]
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_single_coil(output_address=output_address,
-                                                 output_value=output_value)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=False)
-        operation_status = functions.validate_resp_data(
-            data=resp_data,
-            function_code=Const.WRITE_SINGLE_COIL,
-            address=output_address,
-            value=output_value,
-            signed=False)
-
-        return operation_status
-
-    def write_single_register(self,
-                              slave_addr: int,
-                              register_address: int,
-                              register_value: int,
-                              signed=True) -> bool:
-        """
-        Update a single register.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      register_address:  The register address
-        :type       register_address:  int
-        :param      register_value:    The register value
-        :type       register_value:    int
-        :param      signed:            Indicates if signed
-        :type       signed:            bool
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_single_register(register_address,
-                                                     register_value,
-                                                     signed)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=False)
-        operation_status = functions.validate_resp_data(
-            data=resp_data,
-            function_code=Const.WRITE_SINGLE_REGISTER,
-            address=register_address,
-            value=register_value,
-            signed=signed)
-
-        return operation_status
-
-    def write_multiple_coils(self,
-                             slave_addr: int,
-                             starting_address: int,
-                             output_values: List[Union[int, bool]]) -> bool:
-        """
-        Update multiple coils.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      starting_address:  The address of the first coil
-        :type       starting_address:  int
-        :param      output_values:     The output values
-        :type       output_values:     List[Union[int, bool]]
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_multiple_coils(
-            starting_address=starting_address,
-            value_list=output_values)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=False)
-        operation_status = functions.validate_resp_data(
-            data=resp_data,
-            function_code=Const.WRITE_MULTIPLE_COILS,
-            address=starting_address,
-            quantity=len(output_values))
-
-        return operation_status
-
-    def write_multiple_registers(self,
-                                 slave_addr: int,
-                                 starting_address: int,
-                                 register_values: List[int],
-                                 signed=True) -> bool:
-        """
-        Update multiple registers.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      starting_address:  The starting address
-        :type       starting_address:  int
-        :param      register_values:   The register values
-        :type       register_values:   List[int]
-        :param      signed:            Indicates if signed
-        :type       signed:            bool
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_multiple_registers(
-            starting_address=starting_address,
-            register_values=register_values,
-            signed=signed)
-
-        resp_data = self._send_receive(modbus_pdu=modbus_pdu,
-                                       slave_addr=slave_addr,
-                                       count=False)
-        operation_status = functions.validate_resp_data(
-            data=resp_data,
-            function_code=Const.WRITE_MULTIPLE_REGISTERS,
-            address=starting_address,
-            quantity=len(register_values),
-            signed=signed
-        )
-
-        return operation_status
-
     def send_response(self,
                       slave_addr: int,
                       function_code: int,
diff --git a/umodbus/tcp.py b/umodbus/tcp.py
index 9c1e9e4..00239b3 100644
--- a/umodbus/tcp.py
+++ b/umodbus/tcp.py
@@ -17,12 +17,12 @@
 # custom packages
 from . import functions
 from . import const as Const
-from .common import Request
+from .common import Request, CommonModbusFunctions
 from .common import ModbusException
 from .modbus import Modbus
 
 # typing not natively supported on MicroPython
-from .typing import List, Optional, Tuple, Union
+from .typing import Optional, Tuple, Union
 
 
 class ModbusTCP(Modbus):
@@ -63,7 +63,7 @@ def get_bound_status(self) -> bool:
             return False
 
 
-class TCP(object):
+class TCP(CommonModbusFunctions):
     """
     TCP class handling socket connections and parsing the Modbus data
 
@@ -88,13 +88,13 @@ def __init__(self,
         self._sock.settimeout(timeout)
 
     def _create_mbap_hdr(self,
-                         slave_id: int,
+                         slave_addr: int,
                          modbus_pdu: bytes) -> Tuple[bytes, int]:
         """
         Create a Modbus header.
 
-        :param      slave_id:    The slave identifier
-        :type       slave_id:    int
+        :param      slave_addr:  The slave identifier
+        :type       slave_addr:  int
         :param      modbus_pdu:  The modbus Protocol Data Unit
         :type       modbus_pdu:  bytes
 
@@ -110,14 +110,14 @@ def _create_mbap_hdr(self,
         self.trans_id_ctr += 1
 
         mbap_hdr = struct.pack(
-            '>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_id)
+            '>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_addr)
 
         return mbap_hdr, trans_id
 
     def _validate_resp_hdr(self,
                            response: bytearray,
                            trans_id: int,
-                           slave_id: int,
+                           slave_addr: int,
                            function_code: int,
                            count: bool = False) -> bytes:
         """
@@ -127,8 +127,8 @@ def _validate_resp_hdr(self,
         :type       response:       bytearray
         :param      trans_id:       The transaction identifier
         :type       trans_id:       int
-        :param      slave_id:       The slave identifier
-        :type       slave_id:       int
+        :param      slave_addr:     The slave identifier
+        :type       slave_addr:     int
         :param      function_code:  The function code
         :type       function_code:  int
         :param      count:          The count
@@ -146,7 +146,7 @@ def _validate_resp_hdr(self,
         if (rec_pid != 0):
             raise ValueError('invalid protocol ID')
 
-        if (slave_id != rec_uid):
+        if (slave_addr != rec_uid):
             raise ValueError('wrong slave ID')
 
         if (rec_fc == (function_code + Const.ERROR_BIAS)):
@@ -159,14 +159,14 @@ def _validate_resp_hdr(self,
         return response[hdr_length:]
 
     def _send_receive(self,
-                      slave_id: int,
+                      slave_addr: int,
                       modbus_pdu: bytes,
                       count: bool) -> bytes:
         """
         Send a modbus message and receive the reponse.
 
-        :param      slave_id:    The slave identifier
-        :type       slave_id:    int
+        :param      slave_addr:    The slave identifier
+        :type       slave_addr:    int
         :param      modbus_pdu:  The modbus PDU
         :type       modbus_pdu:  bytes
         :param      count:       The count
@@ -175,281 +175,19 @@ def _send_receive(self,
         :returns:   Modbus data
         :rtype:     bytes
         """
-        mbap_hdr, trans_id = self._create_mbap_hdr(slave_id=slave_id,
+        mbap_hdr, trans_id = self._create_mbap_hdr(slave_addr=slave_addr,
                                                    modbus_pdu=modbus_pdu)
         self._sock.send(mbap_hdr + modbus_pdu)
 
         response = self._sock.recv(256)
         modbus_data = self._validate_resp_hdr(response=response,
                                               trans_id=trans_id,
-                                              slave_id=slave_id,
+                                              slave_addr=slave_addr,
                                               function_code=modbus_pdu[0],
                                               count=count)
 
         return modbus_data
 
-    def read_coils(self,
-                   slave_addr: int,
-                   starting_addr: int,
-                   coil_qty: int) -> List[bool]:
-        """
-        Read coils (COILS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The coil starting address
-        :type       starting_addr:  int
-        :param      coil_qty:       The amount of coils to read
-        :type       coil_qty:       int
-
-        :returns:   State of read coils as list
-        :rtype:     List[bool]
-        """
-        modbus_pdu = functions.read_coils(
-            starting_address=starting_addr,
-            quantity=coil_qty)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=True)
-        status_pdu = functions.bytes_to_bool(byte_list=response,
-                                             bit_qty=coil_qty)
-
-        return status_pdu
-
-    def read_discrete_inputs(self,
-                             slave_addr: int,
-                             starting_addr: int,
-                             input_qty: int) -> List[bool]:
-        """
-        Read discrete inputs (ISTS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The discrete input starting address
-        :type       starting_addr:  int
-        :param      input_qty:      The amount of discrete inputs to read
-        :type       input_qty:      int
-
-        :returns:   State of read discrete inputs as list
-        :rtype:     List[bool]
-        """
-        modbus_pdu = functions.read_discrete_inputs(
-            starting_address=starting_addr,
-            quantity=input_qty)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=True)
-        status_pdu = functions.bytes_to_bool(byte_list=response,
-                                             bit_qty=input_qty)
-
-        return status_pdu
-
-    def read_holding_registers(self,
-                               slave_addr: int,
-                               starting_addr: int,
-                               register_qty: int,
-                               signed: bool = True) -> Tuple[int, ...]:
-        """
-        Read holding registers (HREGS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The holding register starting address
-        :type       starting_addr:  int
-        :param      register_qty:   The amount of holding registers to read
-        :type       register_qty:   int
-        :param      signed:         Indicates if signed
-        :type       signed:         bool
-
-        :returns:   State of read holding register as tuple
-        :rtype:     Tuple[int, ...]
-        """
-        modbus_pdu = functions.read_holding_registers(
-            starting_address=starting_addr,
-            quantity=register_qty)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=True)
-        register_value = functions.to_short(byte_array=response, signed=signed)
-
-        return register_value
-
-    def read_input_registers(self,
-                             slave_addr: int,
-                             starting_addr: int,
-                             register_qty: int,
-                             signed: bool = True) -> Tuple[int, ...]:
-        """
-        Read input registers (IREGS).
-
-        :param      slave_addr:     The slave address
-        :type       slave_addr:     int
-        :param      starting_addr:  The input register starting address
-        :type       starting_addr:  int
-        :param      register_qty:   The amount of input registers to read
-        :type       register_qty:   int
-        :param      signed:         Indicates if signed
-        :type       signed:         bool
-
-        :returns:   State of read input register as tuple
-        :rtype:     Tuple[int, ...]
-        """
-        modbus_pdu = functions.read_input_registers(
-            starting_address=starting_addr,
-            quantity=register_qty)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=True)
-        register_value = functions.to_short(byte_array=response, signed=signed)
-
-        return register_value
-
-    def write_single_coil(self,
-                          slave_addr: int,
-                          output_address: int,
-                          output_value: Union[int, bool]) -> bool:
-        """
-        Update a single coil.
-
-        :param      slave_addr:      The slave address
-        :type       slave_addr:      int
-        :param      output_address:  The output address
-        :type       output_address:  int
-        :param      output_value:    The output value
-        :type       output_value:    Union[int, bool]
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_single_coil(output_address=output_address,
-                                                 output_value=output_value)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=False)
-        if response is None:
-            return False
-
-        operation_status = functions.validate_resp_data(
-            data=response,
-            function_code=Const.WRITE_SINGLE_COIL,
-            address=output_address,
-            value=output_value,
-            signed=False)
-
-        return operation_status
-
-    def write_single_register(self,
-                              slave_addr: int,
-                              register_address: int,
-                              register_value: int,
-                              signed: bool = True) -> bool:
-        """
-        Update a single register.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      register_address:  The register address
-        :type       register_address:  int
-        :param      register_value:    The register value
-        :type       register_value:    int
-        :param      signed:            Indicates if signed
-        :type       signed:            bool
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_single_register(
-            register_address=register_address,
-            register_value=register_value,
-            signed=signed)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=False)
-        operation_status = functions.validate_resp_data(
-            data=response,
-            function_code=Const.WRITE_SINGLE_REGISTER,
-            address=register_address,
-            value=register_value,
-            signed=signed)
-
-        return operation_status
-
-    def write_multiple_coils(self,
-                             slave_addr: int,
-                             starting_address: int,
-                             output_values: List[Union[int, bool]]) -> bool:
-        """
-        Update multiple coils.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      starting_address:  The address of the first coil
-        :type       starting_address:  int
-        :param      output_values:     The output values
-        :type       output_values:     List[Union[int, bool]]
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_multiple_coils(
-            starting_address=starting_address,
-            value_list=output_values)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=False)
-        operation_status = functions.validate_resp_data(
-            data=response,
-            function_code=Const.WRITE_MULTIPLE_COILS,
-            address=starting_address,
-            quantity=len(output_values))
-
-        return operation_status
-
-    def write_multiple_registers(self,
-                                 slave_addr: int,
-                                 starting_address: int,
-                                 register_values: List[int],
-                                 signed=True) -> bool:
-        """
-        Update multiple registers.
-
-        :param      slave_addr:        The slave address
-        :type       slave_addr:        int
-        :param      starting_address:  The starting address
-        :type       starting_address:  int
-        :param      register_values:   The register values
-        :type       register_values:   List[int]
-        :param      signed:            Indicates if signed
-        :type       signed:            bool
-
-        :returns:   Result of operation
-        :rtype:     bool
-        """
-        modbus_pdu = functions.write_multiple_registers(
-            starting_address=starting_address,
-            register_values=register_values,
-            signed=signed)
-
-        response = self._send_receive(slave_id=slave_addr,
-                                      modbus_pdu=modbus_pdu,
-                                      count=False)
-        operation_status = functions.validate_resp_data(
-            data=response,
-            function_code=Const.WRITE_MULTIPLE_REGISTERS,
-            address=starting_address,
-            quantity=len(register_values),
-            signed=signed
-        )
-
-        return operation_status
-
 
 class TCPServer(object):
     """Modbus TCP host class"""


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e07ac30..aa7f769 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -42,6 +42,12 @@ jobs:
     - name: Run Client/Host TCP test
       run: |
         docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host
+    - name: Run Client/Host RTU example
+      run: |
+        docker compose -f docker-compose-rtu.yaml up --build --exit-code-from micropython-host
+    - name: Run Client/Host RTU test
+      run: |
+        docker compose -f docker-compose-rtu-test.yaml up --build --exit-code-from micropython-host
     - name: Build package
       run: |
         changelog2version \
diff --git a/Dockerfile.test_tcp_example b/Dockerfile.test_examples
similarity index 100%
rename from Dockerfile.test_tcp_example
rename to Dockerfile.test_examples
diff --git a/docker-compose-rtu-test.yaml b/docker-compose-rtu-test.yaml
new file mode 100644
index 0000000..3ae87e7
--- /dev/null
+++ b/docker-compose-rtu-test.yaml
@@ -0,0 +1,64 @@
+#
+# build all non-image containers
+# $ docker-compose -f docker-compose-rtu-test.yaml build
+# can be combined into one command to also start it afterwards
+# $ docker-compose -f docker-compose-rtu-test.yaml up --build
+#
+
+version: "3.8"
+
+services:
+  micropython-client:
+    build:
+      context: .
+      dockerfile: Dockerfile.client_rtu
+    container_name: micropython-client
+    volumes:
+      - ./:/home
+      - ./registers:/home/registers
+      - ./umodbus:/root/.micropython/lib/umodbus
+      - ./fakes:/usr/lib/micropython
+    expose:
+      - "65433"
+    ports:
+      - "65433:65433" # reach "micropython-client" at 172.25.0.2:65433, see networks
+    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-host:
+    build:
+      context: .
+      dockerfile: Dockerfile.test_examples
+    container_name: micropython-host
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+      - ./fakes:/usr/lib/micropython
+      - ./mpy_unittest.py:/root/.micropython/lib/mpy_unittest.py
+    depends_on:
+      - micropython-client
+    command:
+      - /bin/bash
+      - -c
+      - |
+        micropython-dev -c "import mpy_unittest as unittest; unittest.main(name='tests.test_rtu_example', fromlist=['TestRtuExample'])"
+    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
diff --git a/docker-compose-tcp-test.yaml b/docker-compose-tcp-test.yaml
index 25589dd..1e5ace3 100644
--- a/docker-compose-tcp-test.yaml
+++ b/docker-compose-tcp-test.yaml
@@ -11,7 +11,7 @@ services:
   micropython-client:
     build:
       context: .
-      dockerfile: Dockerfile.client
+      dockerfile: Dockerfile.client_tcp
     container_name: micropython-client
     volumes:
       - ./:/home
@@ -30,7 +30,7 @@ services:
   micropython-host:
     build:
       context: .
-      dockerfile: Dockerfile.test_tcp_example
+      dockerfile: Dockerfile.test_examples
     container_name: micropython-host
     volumes:
       - ./:/home
diff --git a/tests/__init__.py b/tests/__init__.py
index efb9b9b..f29af82 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -7,3 +7,6 @@
 
 # TestTcpExample is a non static test and requires a running TCP client
 # from .test_tcp_example import *
+
+# TestRtuExample is a non static test and requires a running RTU client
+# from .test_rtu_example import *
diff --git a/tests/test_rtu_example.py b/tests/test_rtu_example.py
new file mode 100644
index 0000000..213f438
--- /dev/null
+++ b/tests/test_rtu_example.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""Unittest for testing RTU functions of umodbus"""
+
+import json
+import ulogging as logging
+import mpy_unittest as unittest
+from umodbus.serial import Serial as ModbusRTUMaster
+
+
+class TestRtuExample(unittest.TestCase):
+    def setUp(self) -> None:
+        """Run before every test method"""
+        # set basic config and level for the logger
+        logging.basicConfig(level=logging.INFO)
+
+        # create a logger for this TestSuite
+        self.test_logger = logging.getLogger(__name__)
+
+        # set the test logger level
+        self.test_logger.setLevel(logging.DEBUG)
+
+        # enable/disable the log output of the device logger for the tests
+        # if enabled log data inside this test will be printed
+        self.test_logger.disabled = False
+
+        self._client_addr = 10          # bus address of client
+
+        self._host = ModbusRTUMaster(baudrate=9600, pins=(25, 26))  # (TX, RX)
+
+        test_register_file = 'registers/example.json'
+        try:
+            with open(test_register_file, 'r') as file:
+                self._register_definitions = json.load(file)
+        except Exception as e:
+            self.test_logger.error(
+                'Is the test register file available at {}?'.format(
+                    test_register_file))
+            raise e
+
+    def test_setup(self) -> None:
+        """Test successful setup of ModbusRTUMaster and the defined register"""
+        # although it is called "Master" the host is here a client connecting
+        # to one or more clients/slaves/devices which are providing data
+        # The reason for calling it "ModbusRTUMaster" is the status of having
+        # the functions to request or get data from other client/slave/devices
+        self.assertFalse(self._host._uart._is_server)
+        self.assertIsInstance(self._register_definitions, dict)
+
+        for reg_type in ['COILS', 'HREGS', 'ISTS', 'IREGS']:
+            with self.subTest(reg_type=reg_type):
+                self.assertIn(reg_type, self._register_definitions.keys())
+                self.assertIsInstance(self._register_definitions[reg_type],
+                                      dict)
+                self.assertGreaterEqual(
+                    len(self._register_definitions[reg_type]), 1)
+
+        self._read_coils_single()
+
+    def _read_coils_single(self) -> None:
+        """Test reading sinlge coil of client"""
+        # read coil with state ON/True
+        coil_address = \
+            self._register_definitions['COILS']['EXAMPLE_COIL']['register']
+        coil_qty = self._register_definitions['COILS']['EXAMPLE_COIL']['len']
+        expectation_list = [
+            bool(self._register_definitions['COILS']['EXAMPLE_COIL']['val'])
+        ]
+
+        coil_status = self._host.read_coils(
+            slave_addr=self._client_addr,
+            starting_addr=coil_address,
+            coil_qty=coil_qty)
+
+        self.test_logger.debug('Status of COIL {}: {}, expectation: {}'.
+                               format(coil_address,
+                                      coil_status,
+                                      expectation_list))
+        self.assertIsInstance(coil_status, list)
+        self.assertEqual(len(coil_status), coil_qty)
+        self.assertTrue(all(isinstance(x, bool) for x in coil_status))
+        self.assertEqual(coil_status, expectation_list)
+
+        # read coil with state OFF/False
+        coil_address = \
+            self._register_definitions['COILS']['EXAMPLE_COIL_OFF']['register']
+        coil_qty = \
+            self._register_definitions['COILS']['EXAMPLE_COIL_OFF']['len']
+        expectation_list = [bool(
+            self._register_definitions['COILS']['EXAMPLE_COIL_OFF']['val']
+        )]
+
+        coil_status = self._host.read_coils(
+            slave_addr=self._client_addr,
+            starting_addr=coil_address,
+            coil_qty=coil_qty)
+
+        self.test_logger.debug('Status of COIL {}: {}, expectation: {}'.
+                               format(coil_address,
+                                      coil_status,
+                                      expectation_list))
+        self.assertIsInstance(coil_status, list)
+        self.assertEqual(len(coil_status), coil_qty)
+        self.assertTrue(all(isinstance(x, bool) for x in coil_status))
+        self.assertEqual(coil_status, expectation_list)
+
+    @unittest.skip('Test not yet implemented')
+    def test__calculate_crc16(self) -> None:
+        """Test calculating Modbus CRC16"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__exit_read(self) -> None:
+        """Test validating received response"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__uart_read(self) -> None:
+        """Test reading data from UART"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__uart_read_frame(self) -> None:
+        """Test reading a Modbus frame"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__send(self) -> None:
+        """Test sending a Modbus frame"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__send_receive(self) -> None:
+        """Test sending a Modbus frame"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test__validate_resp_hdr(self) -> None:
+        """Test response header validation"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test_send_response(self) -> None:
+        """Test sending a response to a client"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test_send_exception_response(self) -> None:
+        """Test sending a exception response to a client"""
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test_get_request(self) -> None:
+        """Test checking for a request"""
+        pass
+
+    def tearDown(self) -> None:
+        """Run after every test method"""
+        self._host._uart._sock.close()
+        self.test_logger.debug('Closed ModbusRTUMaster socket at tearDown')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_tcp_example.py b/tests/test_tcp_example.py
index d5a343b..73ccefc 100644
--- a/tests/test_tcp_example.py
+++ b/tests/test_tcp_example.py
@@ -68,7 +68,7 @@ def test__create_mbap_hdr(self) -> None:
         expectation = (struct.pack('>H', trans_id) + b'\x00\x00\x00\x06\x0A',
                        trans_id)
 
-        result = self._host._create_mbap_hdr(slave_id=self._client_addr,
+        result = self._host._create_mbap_hdr(slave_addr=self._client_addr,
                                              modbus_pdu=modbus_pdu)
 
         self.assertIsInstance(result, tuple)
@@ -127,7 +127,7 @@ def test__validate_resp_hdr(self) -> None:
                 result = self._host._validate_resp_hdr(
                     response=response,
                     trans_id=trans_id,
-                    slave_id=self._client_addr,
+                    slave_addr=self._client_addr,
                     function_code=function_code)
                 self.test_logger.debug('result: {}, expectation: {}'.format(
                     result, expectation))
@@ -149,7 +149,7 @@ def test__validate_resp_hdr(self) -> None:
             self._host._validate_resp_hdr(
                 response=response,
                 trans_id=data['tid'] + 1,
-                slave_id=data['sid'],
+                slave_addr=data['sid'],
                 function_code=data['fid'])
 
         # trigger wrong function ID/throw Modbus exception code assert
@@ -157,7 +157,7 @@ def test__validate_resp_hdr(self) -> None:
             self._host._validate_resp_hdr(
                 response=response,
                 trans_id=data['tid'],
-                slave_id=data['sid'],
+                slave_addr=data['sid'],
                 function_code=data['fid'] + 1)
 
         # trigger wrong slave ID assert
@@ -165,7 +165,7 @@ def test__validate_resp_hdr(self) -> None:
             self._host._validate_resp_hdr(
                 response=response,
                 trans_id=data['tid'],
-                slave_id=data['sid'] + 1,
+                slave_addr=data['sid'] + 1,
                 function_code=data['fid'])
 
     @unittest.skip('Test not yet implemented')