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

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

Create a test framework with different requests and check response data for single and multi registers
How to read registers?
First of all, it is the first time that I work with modbus. So sorry if the question is stupid.
How can I read or write a register of a frequency converter?
Bug: `register_qty` do not works on the `read_holding_registers()`
This bug was extracted from PR #10 report, in this [reply](https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1234736964):


The `register_qty` do not works on the `read_holding_registers()`:

```Bash
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19,)
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
(19,) <-- I believe that here should be (19, 786) - register address 93 and 94
>>> host.read_holding_registers(slave_addr=10, starting_addr=94, register_qty=2, signed=False)
(786,)
>>> 

```

Using this register_definitions: 
`{'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [1, 1, 1]}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 4, 'val': 19}, 'EXAMPLE_HREG_2': {'register': 94, 'len': 2, 'val': 786}}, 'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': [1, 0, 0, 1]}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}}
`


**Testing a different `register_definitions`  model:**

If I change the register_definitions to this:
`{'COILS': {'EXAMPLE_COIL': {'register': 123, 'len': 3, 'val': [1, 1, 1]}}, 'HREGS': {'EXAMPLE_HREG': {'register': 93, 'len': 1, 'val': [19, 23494]}}, 'ISTS': {'EXAMPLE_ISTS': {'register': 67, 'len': 1, 'val': [1, 0, 0, 1]}}, 'IREGS': {'EXAMPLE_IREG': {'register': 10, 'len': 2, 'val': 60001}}}
`

I have this results:
**1.* The `register_qty` still not works, but when I  try to read, address 93, 94 came together
**2.* When I try to read/write address 94, I have error
**3.* When I try to write address 93, the value on address 94 is lost.

```Bash
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19, 23494) <-- (1) register_qty is 1, but came 2 address: the 93 and 94
>>> host.read_holding_registers(slave_addr=10, starting_addr=94, register_qty=1, signed=False) <-- (2) 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/tcp.py", line 118, in read_holding_registers
  File "umodbus/tcp.py", line 89, in _send_receive
  File "umodbus/tcp.py", line 74, in _validate_resp_hdr
ValueError: slave returned exception code: 131
>>> 
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(19, 23494)
>>> 
>>> host.write_single_register(slave_addr=10, register_address=94, register_value=555, signed=False) <-- (2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/tcp.py", line 157, in write_single_register
  File "umodbus/tcp.py", line 89, in _send_receive
  File "umodbus/tcp.py", line 74, in _validate_resp_hdr
ValueError: slave returned exception code: 134
>>> host.write_single_register(slave_addr=10, register_address=93, register_value=15, signed=False)
True
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
(15,) <-- (3) Check que value 23494 was lost
>>> 
```

Well, in the second test I think that I broken the `register_refinitions` logic, but I do not know what is the correct rules for it. What is the correct usage on the `register definitions`?
Broken url on the tag 1.2.0
Hi @brainelectronics 

Just two points:

1. Correct if I'm wrong, please. I understand that this new release is not for me to test, just for your tests, because I do not see any new  bug fixed on the [1.2.0 changelog](https://github.com/brainelectronics/micropython-modbus/blob/1.2.0/changelog.md) 

2. Anyway, as curious, I decided to click in that links and checked that **"Update [modules submodule](https://github.com/brainelectronics/micropython-modbus/blob/1.2.0/modules) to 1.3.0"** link/url inside the [1.2.0 changelog](https://github.com/brainelectronics/micropython-modbus/blob/1.2.0/changelog.md)  and [1.2.0 tag](https://github.com/brainelectronics/micropython-modbus/releases/tag/1.2.0)  is broken.

Thank you very much!
Coil quantity specification does not work
As found out during #10 the `coil_qty` parameter of the `read_coils` function of [TCP](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/tcp.py#L94) and [RTU](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/serial.py#L196) is not working. 

```python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=1" but showed 8 and not just 1.
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=20" but still showing 8, and not 20.
```

Coil quantity specification does not work
As found out during #10 the `coil_qty` parameter of the `read_coils` function of [TCP](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/tcp.py#L94) and [RTU](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/serial.py#L196) is not working. 

```python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=1" but showed 8 and not just 1.
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=20" but still showing 8, and not 20.
```

Coil quantity specification does not work
As found out during #10 the `coil_qty` parameter of the `read_coils` function of [TCP](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/tcp.py#L94) and [RTU](https://github.com/brainelectronics/micropython-modbus/blob/dd5b8e593697d098b29abf2615d57216c7346f4a/umodbus/serial.py#L196) is not working. 

```python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=1" but showed 8 and not just 1.
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
# [True, False, False, False, False, False, False, False]   # <--- "coil_qty=20" but still showing 8, and not 20.
```

Docs: request a simple doc about to create/extend `register definitions` (rules and meanings)
In my opinion, is very important, to prevent errors, to have a simple doc (with examples) to explain two things:

1. What is rules on the `register definitions`, I mean, what and how each argument need to be constructed.
2.  What means (what it will affect) each argument. Example: until now is not very clear to me what means the `len`, like as `'len': 1`. The reason that is not `very` clear yet is that some times I changed the `len` to other value, and nothing change. Or because my `register definitions` is not correct - so, is a one more good reason to have a doc explaining in details about `register definitions`.

Read/Write register at some location of definition
Found during #33, see https://github.com/brainelectronics/micropython-modbus/pull/33#issuecomment-1336274272 at section "Problem 3" and "Problem 5"

Defining a register on the MicroPython Modbus client device as e.g. (valid for HREGS, COILS, ISTS, IREGS)

```
{
    "HREGS": {
        "HREG_NAME": {
            "register": 93,
            "len": 3,
            "val": [29, 38, 0]
        }
    }
}
```

does not allow to read **ONLY** register `94` which would hold `38`.

Reason/Root cause:
Registers are defined as specified by the `register` element, see https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/umodbus/modbus.py#L53-L55 for a coil. The provided `value` is added as value to the dictionary at the key `register`, here `93`. Thereby no element/key `94` exists in the Modbus `_register_dict`
Bug: error when trying to write multiple coils - `host.write_multiple_coils()`
This bug was extracted from PR #10 report, in this [reply](https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1189078241):


Error when trying to write multiple coils (`host.write_multiple_coils()`)

I not found examples on the docs about how is the correct way to write multiple coils, so I get a example from [here](https://docs.pycom.io/tutorials/hardware/modbus/)

```
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]
>>>
>>> host.write_multiple_coils(slave_addr=10, starting_address=123, output_values=[1,1,1,0,0,1,1,1,0,0,1,1,1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 274, in write_multiple_coils
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
>>> host.write_multiple_coils(slave_addr=10, starting_address=123, output_values=[1,1,1,0,0,1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 274, in write_multiple_coils
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
```



Discussion / hints:


See [Modbus Application Protocol](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf) section 6.3

Relates to #18 

And only the first coil value is correct, the others  always appears as false.
> And only the first coil value is correct, the others always appears as false.

That's correct @javiercp64 for [default definitions example](https://github.com/brainelectronics/micropython-modbus/blob/bugfix/improve-docs-and-add-examples/examples/rtu_client_example.py#L36) because the len is `1` (`"len": 1`) for the value `1` (`"val": 1`), so others bits that are not set, are `0` (`False`) by default.

If you change the definitions to set others bit values you will receive `True` if you set as `1`. See example below with new definitions - look at `"val"`:

`register_definitions = {    "COILS": {        "EXAMPLE_COIL": {            "register": 123,            "len": 3,            "val": [1, 0, 1]     }   },
`

**Output:**
```
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=3)
[True, False, True, False, False, False, False, False]
>>>
```

However, the write multiple COILS has a bug, as reported on the PR #10, in [this section](https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1189078241), `item 2`.
As detected by @brainelectronics on the thread https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1203521346 is correct to return always multiple of 8 bits. So is correct when `coil_qty=1` returning the 8 bits/elements. But the bug still exists, once that `coil_qty=20` is returning just first 8 bits.

```Python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]   # CORRECT: "coil_qty=1"  and showed 8 bits (multiple of 8).
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
[True, False, False, False, False, False, False, False]   # NOT CORRECT: "coil_qty=20" showing only first 8 bits.
```
@brainelectronics 
Check Implementation to follow requirement of returning multiple coils as requested, see http://www.simplymodbus.ca/FC01.htm
https://github.com/brainelectronics/micropython-modbus/compare/develop...ShaunL36:micropython-modbus:develop
related to #25 
And only the first coil value is correct, the others  always appears as false.
> And only the first coil value is correct, the others always appears as false.

That's correct @javiercp64 for [default definitions example](https://github.com/brainelectronics/micropython-modbus/blob/bugfix/improve-docs-and-add-examples/examples/rtu_client_example.py#L36) because the len is `1` (`"len": 1`) for the value `1` (`"val": 1`), so others bits that are not set, are `0` (`False`) by default.

If you change the definitions to set others bit values you will receive `True` if you set as `1`. See example below with new definitions - look at `"val"`:

`register_definitions = {    "COILS": {        "EXAMPLE_COIL": {            "register": 123,            "len": 3,            "val": [1, 0, 1]     }   },
`

**Output:**
```
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=3)
[True, False, True, False, False, False, False, False]
>>>
```

However, the write multiple COILS has a bug, as reported on the PR #10, in [this section](https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1189078241), `item 2`.
As detected by @brainelectronics on the thread https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1203521346 is correct to return always multiple of 8 bits. So is correct when `coil_qty=1` returning the 8 bits/elements. But the bug still exists, once that `coil_qty=20` is returning just first 8 bits.

```Python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]   # CORRECT: "coil_qty=1"  and showed 8 bits (multiple of 8).
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
[True, False, False, False, False, False, False, False]   # NOT CORRECT: "coil_qty=20" showing only first 8 bits.
```
@brainelectronics 
Check Implementation to follow requirement of returning multiple coils as requested, see http://www.simplymodbus.ca/FC01.htm
https://github.com/brainelectronics/micropython-modbus/compare/develop...ShaunL36:micropython-modbus:develop
related to #25 
And only the first coil value is correct, the others  always appears as false.
> And only the first coil value is correct, the others always appears as false.

That's correct @javiercp64 for [default definitions example](https://github.com/brainelectronics/micropython-modbus/blob/bugfix/improve-docs-and-add-examples/examples/rtu_client_example.py#L36) because the len is `1` (`"len": 1`) for the value `1` (`"val": 1`), so others bits that are not set, are `0` (`False`) by default.

If you change the definitions to set others bit values you will receive `True` if you set as `1`. See example below with new definitions - look at `"val"`:

`register_definitions = {    "COILS": {        "EXAMPLE_COIL": {            "register": 123,            "len": 3,            "val": [1, 0, 1]     }   },
`

**Output:**
```
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=3)
[True, False, True, False, False, False, False, False]
>>>
```

However, the write multiple COILS has a bug, as reported on the PR #10, in [this section](https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1189078241), `item 2`.
As detected by @brainelectronics on the thread https://github.com/brainelectronics/micropython-modbus/pull/10#issuecomment-1203521346 is correct to return always multiple of 8 bits. So is correct when `coil_qty=1` returning the 8 bits/elements. But the bug still exists, once that `coil_qty=20` is returning just first 8 bits.

```Python
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[True, False, False, False, False, False, False, False]   # CORRECT: "coil_qty=1"  and showed 8 bits (multiple of 8).
host.read_coils(slave_addr=10, starting_addr=123, coil_qty=20)
[True, False, False, False, False, False, False, False]   # NOT CORRECT: "coil_qty=20" showing only first 8 bits.
```
@brainelectronics 
Check Implementation to follow requirement of returning multiple coils as requested, see http://www.simplymodbus.ca/FC01.htm
https://github.com/brainelectronics/micropython-modbus/compare/develop...ShaunL36:micropython-modbus:develop
related to #25 
found by #10 

See [Modbus Application Protocol](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf) section 6.11

> The request  data  contents  are  two  bytes:  CD  01  hex  (1100  1101  0000  0001  binary).  The binary bits correspond to the outputs in the following way:
```
Bit:     1 1 0 0 1 1 0 1 0 0 0 0 0 0 0 1
Output: 27 26 25 24 23 22 21 20 – – – – – – 29 28
```

Fix patch:
diff --git a/.flake8 b/.flake8
index 57a3f58..33c99f3 100644
--- a/.flake8
+++ b/.flake8
@@ -20,6 +20,7 @@ ignore = D107, D400, E501, W504, D204
 # Specify a list of mappings of files and the codes that should be ignored for the entirety of the file.
 per-file-ignores =
     tests/*:D101,D102,D104
+    umodbus/const.py:F821
 
 # Provide a comma-separated list of glob patterns to exclude from checks.
 exclude =
@@ -32,7 +33,7 @@ exclude =
     # There's no value in checking cache directories
     __pycache__,
     # The conf file is mostly autogenerated, ignore it
-    docs/source/conf.py,
+    docs/conf.py,
     # This contains our built documentation
     build,
     # This contains builds that we don't want to check
@@ -42,6 +43,7 @@ exclude =
     # example testing folder before going live
     thinking
     .idea
+    .bak
     # custom scripts, not being part of the distribution
     libs_external
     modules
diff --git a/.gitignore b/.gitignore
index 513d659..47dd3b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,6 @@
 .DS_Store
 .DS_Store?
 pymakr.conf
-tests/
 config/config*.py
 thinking/
 *.bin
diff --git a/.gitmodules b/.gitmodules
index c918708..839d04e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
 [submodule "modules"]
 	path = modules
-	url = git@github.com:brainelectronics/python-modules.git
+	# changed to https due to RTD build issue
+	# see https://github.com/readthedocs/readthedocs.org/issues/4043
+	# url = git@github.com:brainelectronics/python-modules.git
+	url = https://github.com/brainelectronics/python-modules.git
\ No newline at end of file
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..aeea64b
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,20 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.9"
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+  configuration: docs/conf.py
+
+# Optionally declare the Python requirements required to build your docs
+python:
+  install:
+  - requirements: docs/requirements.txt
diff --git a/Dockerfile.client b/Dockerfile.client
new file mode 100644
index 0000000..faa03b3
--- /dev/null
+++ b/Dockerfile.client
@@ -0,0 +1,16 @@
+# Build image
+# $ docker build -t micropython-client -f Dockerfile.client .
+#
+# Run image
+# $ docker run -it --rm --name micropython-client micropython-client
+
+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
+RUN micropython-dev -m upip install micropython-urequests
+
+CMD [ "micropython-dev", "-m", "examples/tcp_client_example.py" ]
diff --git a/Dockerfile.host b/Dockerfile.host
new file mode 100644
index 0000000..145739e
--- /dev/null
+++ b/Dockerfile.host
@@ -0,0 +1,16 @@
+# Build image
+# $ docker build -t micropython-host -f Dockerfile.host .
+#
+# Run image
+# $ docker run -it --rm --name micropython-host micropython-host
+
+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
+RUN micropython-dev -m upip install micropython-urequests
+
+CMD [ "micropython-dev", "-m", "examples/tcp_host_example.py" ]
diff --git a/README.md b/README.md
index 08fa6a9..aaf335e 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,8 @@
 ![MicroPython](https://img.shields.io/badge/micropython-Ok-green.svg)
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 [![CI](https://github.com/brainelectronics/micropython-modbus/actions/workflows/release.yml/badge.svg)](https://github.com/brainelectronics/micropython-modbus/actions/workflows/release.yml)
+[![Test Python package](https://github.com/brainelectronics/micropython-modbus/actions/workflows/test.yml/badge.svg)](https://github.com/brainelectronics/micropython-modbus/actions/workflows/test.yml)
+[![Documentation Status](https://readthedocs.org/projects/micropython-modbus/badge/?version=latest)](https://micropython-modbus.readthedocs.io/en/latest/?badge=latest)
 
 MicroPython ModBus TCP and RTU library supporting client and host mode
 
@@ -16,15 +18,18 @@ Forked from [Exo Sense Py][ref-sferalabs-exo-sense], based on
 [PyCom Modbus][ref-pycom-modbus] and extended with other functionalities to
 become a powerfull MicroPython library
 
+The latest documentation is available at
+[MicroPython Modbus ReadTheDocs][ref-rtd-micropython-modbus]
+
 <!-- MarkdownTOC -->
 
 - [Quickstart](#quickstart)
     - [Install package on board with pip](#install-package-on-board-with-pip)
+    - [Request coil status](#request-coil-status)
+        - [TCP](#tcp)
+        - [RTU](#rtu)
     - [Install additional MicroPython packages](#install-additional-micropython-packages)
 - [Usage](#usage)
-    - [Master implementation](#master-implementation)
-    - [Slave implementation](#slave-implementation)
-    - [Register configuration](#register-configuration)
 - [Supported Modbus functions](#supported-modbus-functions)
 - [Credits](#credits)
 
@@ -36,7 +41,8 @@ This is a quickstart to install the `micropython-modbus` library on a
 MicroPython board.
 
 A more detailed guide of the development environment can be found in
-[SETUP](SETUP.md)
+[SETUP](SETUP.md). Further details about the usage can be found in
+[USAGE](USAGE.md)
 
 ```bash
 python3 -m venv .venv
@@ -51,15 +57,8 @@ pip install 'rshell>=0.0.30,<1.0.0'
 rshell -p /dev/tty.SLAB_USBtoUART --editor nano
 ```
 
-Inside the rshell
-
-```bash
-cp main.py /pyboard
-cp boot.py /pyboard
-repl
-```
-
-Inside the REPL
+Inside the [rshell][ref-remote-upy-shell] open a REPL and execute these
+commands inside the REPL
 
 ```python
 import machine
@@ -76,135 +75,83 @@ print('Installation completed')
 machine.soft_reset()
 ```
 
-### Install additional MicroPython packages
-
-To use this package with the provided [`boot.py`](boot.py) and
-[`main.py`](main.py) file, additional modules are required, which are not part
-of this repo/package. To install these modules on the device, connect to a
-network and install them via `upip` as follows
-
-```python
-import upip
-upip.install('micropython-brainelectronics-helpers')
-```
+### Request coil status
 
-or check the README of the
-[brainelectronics MicroPython modules][ref-github-be-mircopython-modules]
+After a successful installation of the package and reboot of the system as
+described in the [installation section](#install-package-on-board-with-pip)
+the following commands can be used to request a coil state of a target/client
+device. Further usage examples can be found in the
+[examples folder][ref-examples-folder] and in the
+[Micropython section of USAGE](USAGE.md#micropython)
 
-## Usage
+#### TCP
 
-See also [USAGE](USAGE.md)
+```python
+from ummodbus.tcp import ModbusTCPMaster
 
-Start a REPL (may perform a soft reboot), wait for network connection and
-start performing Modbus requests to the device.
+tcp_device = ModbusTCPMaster(
+    slave_ip='172.24.0.2',  # IP address of the target/client/slave device
+    slave_port=502,         # TCP port of the target/client/slave device
+    # timeout=5.0           # optional, timeout in seconds, default 5.0
+)
 
-For further details about a TCP-RTU bridge implementation check the header
-comment of [`main.py`](main.py).
+# address of the target/client/slave device on the bus
+slave_addr = 10
+coil_address = 123
+coil_qty = 1
 
-### Master implementation
+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))
+```
 
-Act as host, get Modbus data via RTU or TCP from a client device
+#### RTU
 
 ```python
-# import modbus host classes
-from umodbus.tcp import TCP as ModbusTCPMaster
-from umodbus.serial import Serial as ModbusRTUMaster
-
-# RTU Master setup
-# act as host, get Modbus data via RTU from a client device
-# ModbusRTUMaster can make serial requests to a client device to get/set data
-rtu_pins = (25, 26)         # (TX, RX)
-slave_addr = 10             # bus address of client
-host = ModbusRTUMaster(
-    baudrate=9600,          # optional, default 9600
-    data_bits=8,            # optional, default 8
-    stop_bits=1,            # optional, default 1
-    parity=None,            # optional, default None
-    pins=rtu_pins)
-
-# TCP Master setup
-# act as host, get Modbus data via TCP from a client device
-# ModbusTCPMaster can make TCP requests to a client device to get/set data
-host = ModbusTCPMaster(
-    slave_ip=192.168.178.34,
-    slave_port=180,
-    timeout=5)              # optional, default 5
-
-# READ COILS
+from umodbus.serial import ModbusRTU
+
+host = ModbusRTU(
+    addr=1,             # address of this Master/Host on bus
+    # baudrate=9600,    # optional, default 9600
+    # data_bits=8,      # optional, default 8
+    # stop_bits=1,      # optional, default 1
+    # parity=None,      # optional, default None
+    pins=(25, 26)       # (TX, RX)
+)
+
+# address of the target/client/slave device on the bus
+slave_addr = 10
 coil_address = 123
+coil_qty = 1
+
 coil_status = host.read_coils(
     slave_addr=slave_addr,
     starting_addr=coil_address,
-    coil_qty=1)
+    coil_qty=coil_qty)
 print('Status of coil {}: {}'.format(coil_status, coil_address))
-
-# 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))
-
-# READ HREGS
-hreg_address = 93
-register_value = host.read_holding_registers(
-    slave_addr=slave_addr,
-    starting_addr=hreg_address,
-    register_qty=1,
-    signed=False)
-print('Status of hreg {}: {}'.format(hreg_address, register_value))
-
-# 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))
-
-# READ ISTS
-ist_address = 67
-input_status = host.read_discrete_inputs(
-    slave_addr=slave_addr,
-    starting_addr=ist_address,
-    input_qty=1)
-print('Status of ist {}: {}'.format(ist_address, input_status))
-
-# READ IREGS
-ireg_address = 10
-register_value = host.read_input_registers(
-                    slave_addr=slave_addr,
-                    starting_addr=ireg_address,
-                    register_qty=2,
-                    signed=False)
-print('Status of ireg {}: {}'.format(ireg_address, register_value))
 ```
 
-### Slave implementation
-
-Act as client, provide Modbus data via RTU or TCP to a host device.
-
-See [Modbus TCP Client example](examples/tcp_client_example.py) and
-[Modbus RTU Client example](examples/rtu_client_example.py)
+### Install additional MicroPython packages
 
-Both examples are using [example register definitions](examples/example.json)
+To use this package with the provided [`boot.py`][ref-package-boot-file] and
+[`main.py`][ref-package-boot-file] file, additional modules are required,
+which are not part of this repo/package. To install these modules on the
+device, connect to a network and install them via `upip` as follows
 
-Use the provided example scripts [read RTU](examples/read_registers_rtu.sh) or
-[read TCP](examples/read_registers_tcp.sh) to read the data from the devices.
-This requires the [modules submodule][ref-github-be-python-modules] to be
-cloned as well and the required packages being installed as described in the
-modules README file. For further details read the [SETUP](SETUP.md) guide.
+```python
+import upip
+upip.install('micropython-brainelectronics-helpers')
+```
 
-### Register configuration
+Check also the README of the
+[brainelectronics MicroPython modules][ref-github-be-mircopython-modules]
+and the [SETUP guide](SETUP.md)
 
-The available registers are defined by a JSON file, placed inside the
-`/pyboard/registers` folder on the board and selected in [`main.py`](main.py).
+## Usage
 
-As an [example the registers](registers/modbusRegisters-MyEVSE.json) of a
-[brainelectronics MyEVSE][ref-myevse-be], [MyEVSE on Tindie][ref-myevse-tindie]
-board is provided with this repo.
+See [USAGE](USAGE.md)
 
 ## Supported Modbus functions
 
@@ -228,13 +175,16 @@ of this library.
 
 * **sfera-labs** - *Initial work* - [giampiero7][ref-sferalabs-exo-sense]
 * **pycom** - *Initial Modbus work* - [pycom-modbus][ref-pycom-modbus]
+* **pfalcon** - *Initial MicroPython unittest module* - [micropython-unittest][ref-pfalcon-unittest]:
 
 <!-- Links -->
 [ref-sferalabs-exo-sense]: https://github.com/sfera-labs/exo-sense-py-modbus
 [ref-pycom-modbus]: https://github.com/pycom/pycom-modbus
+[ref-rtd-micropython-modbus]: https://micropython-modbus.readthedocs.io/en/latest/
 [ref-remote-upy-shell]: https://github.com/dhylands/rshell
+[ref-examples-folder]: https://github.com/brainelectronics/micropython-modbus/tree/develop/examples
+[ref-package-boot-file]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/boot.py
+[ref-package-main-file]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/main.py
 [ref-github-be-mircopython-modules]: https://github.com/brainelectronics/micropython-modules
-[ref-github-be-python-modules]: https://github.com/brainelectronics/python-modules
-[ref-myevse-be]: https://brainelectronics.de/
-[ref-myevse-tindie]: https://www.tindie.com/stores/brainelectronics/
 [ref-giampiero7]: https://github.com/giampiero7
+[ref-pfalcon-unittest]: https://github.com/pfalcon/pycopy-lib/blob/56ebf2110f3caa63a3785d439ce49b11e13c75c0/unittest/unittest.py
diff --git a/USAGE.md b/USAGE.md
deleted file mode 100644
index c497f94..0000000
--- a/USAGE.md
+++ /dev/null
@@ -1,198 +0,0 @@
-# Usage
-
-Using and testing this `micropython-modbus` library
-
----------------
-
-<!-- MarkdownTOC -->
-
-- [Development environment](#development-environment)
-	- [TCP](#tcp)
-		- [Read data](#read-data)
-		- [Write data](#write-data)
-- [MicroPython](#micropython)
-	- [TCP](#tcp-1)
-		- [Client](#client)
-		- [Host](#host)
-	- [RTU](#rtu)
-
-<!-- /MarkdownTOC -->
-
-The onwards described steps assume a successful setup as described in
-[SETUP.md](SETUP.md)
-
-## Development environment
-
-This section describes the necessary steps on the computer to get ready to
-test and run the examples.
-
-```bash
-# Linux/Mac
-source .venv/bin/activate
-```
-
-On a Windows based system activate the virtual environment like this
-
-```
-.venv\Scripts\activate.bat
-```
-
-The onwards mentioned commands shall be performed inside the previously
-activated virtual environment.
-
-### TCP
-
-Read and write the Modbus register data from a MicroPython device with the
-[brainelectronics ModbusWrapper][ref-github-be-modbus-wrapper] provided with
-the [modules submodule](modules)
-
-#### Read data
-
-```bash
-python modules/read_device_info_registers.py \
---file=examples/example.json \
---connection=tcp \
---address=192.168.178.69 \
---port=502 \
---print \
---pretty \
---debug \
---verbose=3
-```
-
-Or use the even more convenient wrapper script for the wrapper.
-
-```bash
-cd examples
-sh read_registers_tcp.sh 192.168.178.69 example.json 502
-```
-
-#### Write data
-
-```bash
-python modules/write_device_info_registers.py \
---file=examples/set-example.json \
---connection=tcp \
---address=192.168.178.69 \
---port=502 \
---print \
---pretty \
---debug \
---verbose=3
-```
-
-Or use the even more convenient wrapper script for the wrapper.
-
-```bash
-cd examples
-sh write_registers_tcp.sh 192.168.178.69 set-example.json 502
-```
-
-## MicroPython
-
-This section describes the necessary steps on the MicroPython device to get
-ready to test and run the examples.
-
-```bash
-# Linux/Mac
-source .venv/bin/activate
-
-rshell -p /dev/tty.SLAB_USBtoUART --editor nano
-```
-
-On a Windows based system activate the virtual environment and enter the
-remote shell like this
-
-```
-.venv\Scripts\activate.bat
-
-rshell -p COM9
-```
-
-The onwards mentioned commands shall be performed inside the previously entered
-remote shell.
-
-### TCP
-
-Get two network capable boards up and running, collecting and setting data on
-each other.
-
-Adjust the WiFi network name (SSID) and password to be able to connect to your
-personal network or remove that section if a wired network connection is used.
-
-#### Client
-
-The client, former known as slave, provides some dummy registers which can be
-read and updated by another device.
-
-```bash
-cp examples/tcp_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!
-Waiting for WiFi connection...
-Waiting for WiFi connection...
-Connected to WiFi.
-('192.168.178.69', '255.255.255.0', '192.168.178.1', '192.168.178.1')
-Setting up registers ...
-Register setup done
-Serving as TCP client on 192.168.178.69:502
-```
-
-#### Host
-
-The host, former known as master, requests and updates some dummy registers of
-another device.
-
-```bash
-cp examples/tcp_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!
-Waiting for WiFi connection...
-Waiting for WiFi connection...
-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]
-Result of setting COIL 123: True
-Status of COIL 123: [False, False, False, False, False, False, False, 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 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.
->>>
-```
-
-<!--
-### RTU
-
-Get two UART/RS485 capable boards up and running, collecting and setting data
-on each other.
--->
-
-<!-- Links -->
-[ref-github-be-modbus-wrapper]: https://github.com/brainelectronics/be-modbus-wrapper
diff --git a/changelog.md b/changelog.md
index 4683ee5..f83909e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -15,6 +15,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 <!-- ## [Unreleased] -->
 
 ## Released
+## [2.0.0] - 2022-12-03
+### Added
+- Perform MicroPython based unittests on every `Test` workflow run
+- Add usage description of docker based MicroPython unittest framework in [USAGE](USAGE.md)
+- Add [docker compose file](docker-compose.yaml) based in MicroPython 1.18 image
+- Add [TCP client Dockerfile](Dockerfile.client), [TCP host Dockerfile](Dockerfile.host), [unittest Dockerfile](Dockerfile.tests) and [TCP unittest specific Dockerfile](Dockerfile.test_tcp_example). All based on MicroPython 1.18 image
+- Add initial test, testing the unittest itself
+- Add [unittest](mpy_unittest.py) implementation based on pfalcon's [micropython-unittest](https://github.com/pfalcon/pycopy-lib/blob/56ebf2110f3caa63a3785d439ce49b11e13c75c0/unittest/unittest.py)
+- Docstrings available for all functions of [functions.py](umodbus/functions.py), see #27
+- Typing hints available for all functions of [functions.py](umodbus/functions.py), [serial.py](umodbus/serial.py) and [tcp.py](umodbus/tcp.py), see #27
+- Unittest for [functions.py](umodbus/functions.py), see #16
+- Unittest for [const.py](umodbus/const.py), see #16
+- [.readthedocs.yaml](.readthedocs.yaml) for Read The Docs, contributes to #26
+
+### Changed
+- Use default values for all registers defined in the [example JSON](registers/example.json)
+- [TCP host example](examples/tcp_host_example.py) and [TCP client example](examples/tcp_client_example.py) define a static IP address and skip further WiFi setup steps in case a Docker usage is detected by a failing import of the `network` module, contributes to #16
+- Define all Modbus function codes as `const()` to avoid external modifications, contributes to #18
+- Remove dependency to `Serial` and `requests` from `umodbus.modbus`, see #18
+- `ModbusRTU` class is part of [serial.py](umodbus/serial.py), see #18
+- `ModbusTCP` class is part of [tcp.py](umodbus/tcp.py), see #18
+- `ModbusRTU` and `ModbusTCP` classes and related functions removed from [modbus.py](umodbus/modbus.py), see #18
+- Imports changed from:
+    - `from umodbus.modbus import ModbusRTU` to `from umodbus.serial import ModbusRTU`
+    - `from umodbus.modbus import ModbusTCP` to `from umodbus.tcp import ModbusTCP`
+- `read_coils` and `read_discrete_inputs` return a list with the same length as the requested quantity instead of always 8, see #12 and #25
+- Common functions `bytes_to_bool` and `to_short` moved to [functions.py](umodbus/functions.py)
+- Use HTTPS URL instead of SSH for submodule
+- Cleanup of root [README](README.md), content moved to [SETUP](docs/SETUP.md) and [USAGE](docs/USAGE.md), contributes to #30
+- Moved [SETUP](docs/SETUP.md) and [USAGE](docs/USAGE.md) into [docs](docs) folder, see #26 contributes to #30
+- Use `False` or `0` as default values for registers without a specific initial value in [modbus.py](umodbus/modbus.py)
+
+### Fixed
+- `read_coils` returns list with amount of requested coils, see #12
+- `read_holding_registers` returns list with amount of requested registers, see #25
+
 ## [1.2.0] - 2022-11-13
 ### Added
 - [TCP host example script](examples/tcp_host_example.py)
@@ -28,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 - Add more info to [TCP client example script](examples/tcp_client_example.py)
-- Update [modules submodule](modules) to `1.3.0`
+- Update [modules submodule](https://github.com/brainelectronics/python-modules/tree/43bad716b7db27db07c94c2d279cee57d0c8c753) to `1.3.0`
 - Line breaks are no longer used in this changelog for enumerations
 - Issues are referenced as `#123` instead of `[#123][ref-issue-123]` to avoid explicit references at the bottom or some other location in the file
 - Scope of contents permissions in release and test release workflow is now `write` to use auto release creation
@@ -115,8 +151,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/1.2.0...develop
+[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.0.0...develop
 
+[2.0.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.0.0
 [1.2.0]: https://github.com/brainelectronics/micropython-modbus/tree/1.2.0
 [1.1.1]: https://github.com/brainelectronics/micropython-modbus/tree/1.1.1
 [1.1.0]: https://github.com/brainelectronics/micropython-modbus/tree/1.1.0
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..549527f
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,55 @@
+#
+# 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
+    container_name: micropython-client
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+    expose:
+      - "502"
+    ports:
+      - "502:502" # reach "micropython-client" at 172.24.0.2:502, see networks
+    networks:
+      my_bridge:
+        # fix IPv4 address to be known and in the MicroPython scripts
+        # https://docs.docker.com/compose/compose-file/#ipv4_address
+        ipv4_address: 172.24.0.2
+
+  micropython-host:
+    build:
+      context: .
+      dockerfile: Dockerfile.host
+    container_name: micropython-host
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+    depends_on:
+      - micropython-client
+    networks:
+      my_bridge:
+        # fix IPv4 address to be known and in the MicroPython scripts
+        # https://docs.docker.com/compose/compose-file/#ipv4_address
+        ipv4_address: 172.24.0.3
+
+networks:
+  my_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.24.0.0/16
+          gateway: 172.24.0.1
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100755
index 0000000..a6e4a9a
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,3 @@
+# CONTRIBUTING
+
+Guideline to contribute to this package
diff --git a/SETUP.md b/docs/SETUP.md
similarity index 52%
rename from SETUP.md
rename to docs/SETUP.md
index 3b4b068..f5ea1f2 100644
--- a/SETUP.md
+++ b/docs/SETUP.md
@@ -4,18 +4,6 @@ Setup the development environment and the MicroPython board
 
 ---------------
 
-<!-- MarkdownTOC -->
-
-- [Development environment](#development-environment)
-	- [Update submodule](#update-submodule)
-	- [Install required tools](#install-required-tools)
-- [MicroPython](#micropython)
-	- [Flash firmware](#flash-firmware)
-	- [Install package with pip](#install-package-with-pip)
-	- [Without network connection](#without-network-connection)
-
-<!-- /MarkdownTOC -->
-
 ## Development environment
 
 This section describes the necessary steps on the computer to get ready to
@@ -68,7 +56,7 @@ ready to test and run the examples.
 ### Flash firmware
 
 Flash the [MicroPython firmware][ref-upy-firmware-download] to the MicroPython
-board with this call
+board with this call in case a ESP32 is used.
 
 ```bash
 esptool.py --chip esp32 --port /dev/tty.SLAB_USBtoUART erase_flash
@@ -86,7 +74,8 @@ station.connect('SSID', 'PASSWORD')
 station.isconnected()
 ```
 
-and install this lib on the MicroPython device like this
+and install this lib with all its dependencies on the MicroPython device like
+this
 
 ```python
 import upip
@@ -95,8 +84,8 @@ upip.install('micropython-modbus')
 
 ### Without network connection
 
-Copy the module to the MicroPython board and import them as shown below
-using [Remote MicroPython shell][ref-remote-upy-shell]
+Copy all files of the [umodbus module][ref-umodbus-module] to the MicroPython
+board using [Remote MicroPython shell][ref-remote-upy-shell]
 
 Open the remote shell with the following command. Additionally use `-b 115200`
 in case no CP210x is used but a CH34x.
@@ -114,7 +103,55 @@ mkdir /pyboard/lib/umodbus
 cp umodbus/* /pyboard/lib/umodbus
 ```
 
+As this package depends on [`micropython-urequests`][ref-urequests] to perform
+TCP requests those files have to be copied as well to the MicroPython board.
+This is of course only necessary if TCP connection are used, in case only
+serial (RTU )Modbus communication is used this step can be skipped.
+
+### Additional MicroPython packages
+
+To use this package with the provided [`boot.py`][ref-package-boot-file] and
+[`main.py`][ref-package-boot-file] file, additional modules are required,
+which are not part of this repo/package.
+
+```bash
+rshell -p /dev/tty.SLAB_USBtoUART --editor nano
+```
+
+#### Install additional package with pip
+
+Again connect to a network and install the additional package on the
+MicroPython device with
+
+```python
+import upip
+upip.install('micropython-modbus')
+```
+
+#### Without network connection
+
+To install the additional modules on the device, download the
+[brainelectronics MicroPython Helpers repo][ref-github-be-mircopython-modules]
+and copy them to the device.
+
+Perform the following command to copy all files and folders to the device
+
+```bash
+mkdir /pyboard/lib/be_helpers
+
+cp be_helpers/* /pyboard/lib/be_helpers
+```
+
+Additionally check the latest instructions of the
+[brainelectronics MicroPython modules][ref-github-be-mircopython-modules]
+README for further instructions.
+
 <!-- Links -->
 [ref-github-be-python-modules]: https://github.com/brainelectronics/python-modules
 [ref-upy-firmware-download]: https://micropython.org/download/
 [ref-remote-upy-shell]: https://github.com/dhylands/rshell
+[ref-umodbus-module]: https://github.com/brainelectronics/micropython-modbus/tree/develop/umodbus
+[ref-urequests]: https://micropython.org/pi/urequests/urequests-0.6.tar.gz
+[ref-package-boot-file]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/boot.py
+[ref-package-main-file]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/main.py
+[ref-github-be-mircopython-modules]: https://github.com/brainelectronics/micropython-modules
diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md
new file mode 100644
index 0000000..8f8c57c
--- /dev/null
+++ b/docs/UPGRADE.md
@@ -0,0 +1,145 @@
+# Upgrade Guide
+
+Detailed upgrade guide for upgrading between breaking versions
+
+---------------
+
+## Intro
+
+As this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
+this document thereby describes the necessary steps to upgrade between two
+major versions.
+
+## Upgrade from major version 1 to 2
+
+### Overview
+
+This is a compressed extraction of the changelog
+
+> - Remove dependency to `Serial` and `requests` from `umodbus.modbus`, see [#18](https://github.com/brainelectronics/micropython-modbus/issues/18)
+> - `ModbusRTU` class is part of [serial.py](umodbus/serial.py), see [#18](https://github.com/brainelectronics/micropython-modbus/issues/18)
+> - `ModbusTCP` class is part of [tcp.py](umodbus/tcp.py), see [#18](https://github.com/brainelectronics/micropython-modbus/issues/18)
+> - `ModbusRTU` and `ModbusTCP` classes and related functions removed from [modbus.py](umodbus/modbus.py), see [#18](https://github.com/brainelectronics/micropython-modbus/issues/18)
+> - Imports changed from:
+>     - `from umodbus.modbus import ModbusRTU` to `from umodbus.serial import ModbusRTU`
+>     - `from umodbus.modbus import ModbusTCP` to `from umodbus.tcp import ModbusTCP`
+>  - `read_coils` and `read_discrete_inputs` return a list with the same length as the requested quantity instead of always 8, see [#12](https://github.com/brainelectronics/micropython-modbus/issues/12) and [#25](https://github.com/brainelectronics/micropython-modbus/issues/25)
+> - `read_holding_registers` returns list with amount of requested registers, see [#25](https://github.com/brainelectronics/micropython-modbus/issues/25)
+
+### Steps to be performed
+
+#### Update imports
+
+The way of importing `ModbusRTU` and `ModbusTCP` changed. Update the imports
+according to the following table. For further details check [#18](https://github.com/brainelectronics/micropython-modbus/issues/18)
+
+| Version 1 | Version 2 |
+| --------- | --------- |
+| `from umodbus.modbus import ModbusRTU` | `from umodbus.serial import ModbusRTU` |
+| `from umodbus.modbus import ModbusTCP` | `from umodbus.tcp import ModbusTCP` |
+
+#### Return values changed
+
+The functions `read_coils`, `read_discrete_inputs` and `read_holding_registers`
+return now a list with the same length as the requested register quantity.
+
+##### Coil registers
+
+All major version 1 releases of this package returned a list with 8 elements
+on a coil register request.
+
+```python
+# example usage only, non productive code example
+
+# reading one coil returned a list of 8 boolean elements
+>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
+[True, False, False, False, False, False, False, False]
+# expectation is [True]
+
+# reading 3 coils returned a list of 8 boolean elements
+>>> host.read_coils(slave_addr=10, starting_addr=126, coil_qty=3)
+[False, False, False, False, False, False, False, False]
+# expectation is [False, True, False]
+```
+
+With the fixes of major version 2 a list with the expected length is returned
+
+```python
+# example usage only, non productive code example
+
+# reading one coil returns a list of 1 boolean element
+>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
+[True]
+
+# reading 3 coils returns a list of 3 boolean elements
+>>> host.read_coils(slave_addr=10, starting_addr=126, coil_qty=3)
+[False, True, False]
+```
+
+##### Discrete input registers
+
+All major version 1 releases of this package returned a list with 8 elements
+on a discrete input register request.
+
+```python
+# example usage only, non productive code example
+
+# reading one discrete input register returned a list of 8 boolean elements
+>>> host.read_discrete_inputs(slave_addr=10, starting_addr=123, input_qty=1)
+[True, False, False, False, False, False, False, False]
+# expectation is [True]
+
+# reading 3 discrete input register returned a list of 8 boolean elements
+>>> host.read_discrete_inputs(slave_addr=10, starting_addr=126, input_qty=3)
+[False, False, False, False, False, False, False, False]
+# expectation is [False, True, False]
+```
+
+With the fixes of major version 2 a list with the expected length is returned
+
+```python
+# example usage only, non productive code example
+
+# reading one discrete input register returns a list of 1 boolean element
+>>> host.read_discrete_inputs(slave_addr=10, starting_addr=123, input_qty=1)
+[True]
+
+# reading 3 discrete input registers returns a list of 3 boolean elements
+>>> host.read_discrete_inputs(slave_addr=10, starting_addr=126, input_qty=3)
+[False, True, False]
+```
+
+##### Holding registers
+
+In all major version 1 releases of this package returned a tuple with only one
+element on a holding register request.
+
+```python
+# example usage only, non productive code example
+
+# reading one register only worked as expected
+>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
+(19,)
+# expectation is (19,)
+
+# reading multiple registers did not work as expected
+# register values of register 93 + 94 should be returned
+>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
+(19,)
+# expectation is (19, 29)
+```
+
+With the fixes of major version 2 a list with the expected length is returned
+
+```python
+# example usage only, non productive code example
+
+# reading one register only worked as expected
+>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
+(19,)
+
+# reading multiple registers did not work as expected
+# register values of register 93 + 94 should be returned
+>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=2, signed=False)
+(19, 29)
+```
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..5a08c1d
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,506 @@
+# Usage
+
+Overview to use and test this `micropython-modbus` library
+
+---------------
+
+The onwards described steps assume a successful setup as described in
+[SETUP.md](SETUP.md)
+
+## MicroPython
+
+This section describes the necessary steps on the MicroPython device to get
+ready to test and run the examples.
+
+```bash
+# Linux/Mac
+source .venv/bin/activate
+
+rshell -p /dev/tty.SLAB_USBtoUART --editor nano
+```
+
+On a Windows based system activate the virtual environment and enter the
+remote shell like this
+
+```
+.venv\Scripts\activate.bat
+
+rshell -p COM9
+```
+
+The onwards mentioned commands shall be performed inside the previously entered
+remote shell.
+
+### Register configuration
+
+The available registers can be defined by a JSON file, placed inside the
+`/pyboard/registers` folder or any other location on the board and loaded in
+`main.py` or by defining a dictionary.
+
+As an [example the registers][ref-registers-MyEVSE] of a
+[brainelectronics MyEVSE][ref-myevse-be], [MyEVSE on Tindie][ref-myevse-tindie]
+board and others are provided with this repo.
+
+#### Structure
+
+If only an interaction with a single register is intended no dictionary needs
+to be defined of course. The onwards explanations assume a bigger setup of
+registers on the same target/client/slave device.
+
+The JSON file/dictionary shall follow the following pattern/structure
+
+```python
+{
+    "COILS": {          # this key shall contain all coils
+        "COIL_NAME": {  # custom name of a coil
+            "register": 42,     # register address of the coil
+            "len": 1,           # amount of registers to request aka quantity
+            "val": 0,           # used to set a register
+            # the onwards mentioned keys are optional
+            "description": "Optional description of the coil",
+            "range": "[0, 1]",  # may provide a range of the value, only for documentation purpose
+            "unit": "BOOL"      # may provide a unit of the value, only for documentation purpose
+        }
+    },
+    "HREGS": {          # this key shall contain all holding registers
+        "HREG_NAME": {  # custom name of a holding register
+            "register": 93,     # register address of the holding register
+            "len": 1,           # amount of registers to request aka quantity
+            "val": 19,          # used to set a register
+            "description": "Optional description of the holding register",
+            "range": "[0, 65535]",
+            "unit": "Hz"
+        },
+    },
+    "ISTS": {           # this key shall contain all static input registers
+        "ISTS_NAME": {  # custom name of a static input register
+            "register": 67,     # register address of the static input register
+            "len": 1,           # amount of registers to request aka quantity
+            "val": 0,           # used to set a register, not possible for ISTS
+            "description": "Optional description of the static input register",
+            "range": "[0, 1]",
+            "unit": "activated"
+        }
+    },
+    "IREGS": {          # this key shall contain all input registers
+        "IREG_NAME": {  # custom name of an input register
+            "register": 10,     # register address of the input register
+            "len": 1,           # amount of registers to request aka quantity
+            "val": 60001,       # used to set a register, not possible for IREGS
+            "description": "Optional description of the static input register",
+            "range": "[0, 65535]",
+            "unit": "millivolt"
+        }
+    }
+}
+```
+
+If not all register types are used they can be of course removed from the
+JSON file/dictionary. The smallest possible definition for reading a coil
+would look like
+
+```python
+{
+    "COILS": {          # this key shall contain all coils
+        "COIL_NAME": {  # custom name of a coil
+            "register": 42,     # register address of the coil
+            "len": 1            # amount of registers to request aka quantity
+        }
+    }
+}
+```
+
+In order to act as client/slave device the same structure can be used. If no
+`val` element is found in the structure the default values are
+
+| Type | Function Code | Default value |
+| ---- | ------------- | ------------- |
+| COILS | 0x01 | False (0x0) |
+| ISTS | 0x02 | False (0x0) |
+| HREGS | 0x03 | 0 |
+| IREGS | 0x04 | 0 |
+
+The value of multiple registers can be set like this
+
+```python
+{
+    "HREGS": {          # this key shall contain all holding registers
+        "HREG_NAME": {  # custom name of a holding register
+            "register": 93,     # register address of the holding register
+            "len": 3,           # amount of registers to request aka quantity
+            "val": [29, 38, 0]  # used to set a register
+        }
+    }
+}
+```
+
+> :warning: As of version 2.0.0 of this package it is not possible to request
+only the holding register 94, which would hold `38` in the above example.
+This is a bug (non implemented feature) of the client/slave implementation.
+For further details check [#35](https://github.com/brainelectronics/micropython-modbus/issues/35)
+
+#### Detailed key explanation
+
+The onwards described key explanations are valid for COIL, HREG, IST and IREG
+
+##### Register
+
+The key `register` defines the register to request or manipulate.
+
+According to the Modbus specification the register address has to be in the
+range of 0x0000 to 0xFFFF (65535) to be valid.
+
+##### Length
+
+The key `len` defines the amout of registers to be requested starting from/with
+the defined `register` address.
+
+According to the Modbus specification the length or amount depends on the type
+of the register as summarized in the table below.
+
+| Type | Function Code | Valid range |
+| ---- | ------------- | ----------- |
+| COILS | 0x01 | 0x1 to 0x7D0 (2000) |
+| ISTS | 0x02 | 0x1 to 0x7D0 (2000) |
+| HREGS | 0x03 | 0x1 to 0x7D (125) |
+| IREGS | 0x04 | 0x1 to 0x7D (125) |
+
+In order to read 5 coils starting at 124 use the following dictionary aka
+config
+
+```python
+{
+    "COILS": {          # this key shall contain all coils
+        "COIL_NAME": {  # custom name of a coil
+            "register": 124,    # register address of the coil
+            "len": 5            # amount of registers to request aka quantity
+        }
+    }
+}
+```
+
+The output will be a list of 5 elements like `[True, False, False, True, True]`
+depending on the actual device coil states of course.
+
+
+
+##### Value
+
+The key `val` defines the value of registers to be set on the target/client
+device.
+
+According to the Modbus specification the value (range) depends on the type
+of the register as summarized in the table below.
+
+| Type | Function Code | Valid value | Comment |
+| ---- | ------------- | ----------- | ------- |
+| COILS | 0x05 | 0x0000 or 0xFF00 | This package maps `0` or `False` to `0x0000` and `1` or `True` to `0xFF00` |
+| HREGS | 0x06 | 0x0000 to 0xFFFF (65535) |  |
+
+##### Optional description
+
+The optional key `description` can be used to provide an additional
+description of the register. This might be helpful if the register name is not
+meaninful enough or for any other reason of course.
+
+##### Optional range
+
+The optional key `range` can be used to indicate the possible value range of
+this specific target. For example a holding register for setting a PWM output
+might only support a range of 0 to 100. This might be especially helpful with
+the optional [`unit`](#optional-unit) key.
+
+###### Optional unit
+
+The optional key `unit` can be used to provide further details about the unit
+of the register. In case of the PWM output register example of the
+[optional range key](#optional-range) the recommended value for this key could
+be `percent`.
+
+### TCP
+
+Get two network capable boards up and running, collecting and setting data on
+each other.
+
+Adjust the WiFi network name (SSID) and password to be able to connect to your
+personal network or remove that section if a wired network connection is used.
+
+#### Client
+
+The client, former known as slave, provides some dummy registers which can be
+read and updated by another device.
+
+```bash
+cp examples/tcp_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!
+Waiting for WiFi connection...
+Waiting for WiFi connection...
+Connected to WiFi.
+('192.168.178.69', '255.255.255.0', '192.168.178.1', '192.168.178.1')
+Setting up registers ...
+Register setup done
+Serving as TCP client on 192.168.178.69:502
+```
+
+#### Host
+
+The host, former known as master, requests and updates some dummy registers of
+another device.
+
+```bash
+cp examples/tcp_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!
+Waiting for WiFi connection...
+Waiting for WiFi connection...
+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]
+Result of setting COIL 123: True
+Status of COIL 123: [False, False, False, False, False, False, False, 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 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.
+>>>
+```
+
+<!--
+### RTU
+
+Get two UART/RS485 capable boards up and running, collecting and setting data
+on each other.
+-->
+
+### TCP-RTU bridge
+
+This example implementation shows how to act as bridge between an RTU (serial)
+connected device and another external TCP device.
+
+For further details about a TCP-RTU bridge implementation check the header
+comment of [`main.py`][ref-package-main-file].
+
+## Classic development environment
+
+This section describes the necessary steps on the computer to get ready to
+test and run the examples.
+
+```bash
+# Linux/Mac
+source .venv/bin/activate
+```
+
+On a Windows based system activate the virtual environment like this
+
+```
+.venv\Scripts\activate.bat
+```
+
+The onwards mentioned commands shall be performed inside the previously
+activated virtual environment.
+
+### TCP
+
+Read and write the Modbus register data from a MicroPython device with the
+[brainelectronics ModbusWrapper][ref-github-be-modbus-wrapper] provided with
+the [modules submodule][ref-modules-folder]
+
+#### Read data
+
+```bash
+python modules/read_device_info_registers.py \
+--file=registers/example.json \
+--connection=tcp \
+--address=192.168.178.69 \
+--port=502 \
+--print \
+--pretty \
+--debug \
+--verbose=3
+```
+
+Or use the even more convenient wrapper script for the wrapper.
+
+```bash
+cd examples
+sh read_registers_tcp.sh 192.168.178.69 ../registers/example.json 502
+```
+
+#### Write data
+
+```bash
+python modules/write_device_info_registers.py \
+--file=registers/set-example.json \
+--connection=tcp \
+--address=192.168.178.69 \
+--port=502 \
+--print \
+--pretty \
+--debug \
+--verbose=3
+```
+
+Or use the even more convenient wrapper script for the wrapper.
+
+```bash
+cd examples
+sh write_registers_tcp.sh 192.168.178.69 ../registers/set-example.json 502
+```
+
+## Docker development environment
+
+### Pull container
+
+Checkout the available
+[MicroPython containers](https://hub.docker.com/r/micropython/unix/tags)
+
+```bash
+docker pull micropython/unix:v1.18
+```
+
+### Spin up container
+
+#### Simple container
+
+Use this command for your first tests or to run some MicroPython commands in
+a simple REPL
+
+```bash
+docker run -it \
+--name micropython-1.18 \
+--network=host \
+--entrypoint bash \
+micropython/unix:v1.18
+```
+
+#### Enter MicroPython REPL
+
+Inside the container enter the REPL by running `micropython-dev`. The console
+should now look similar to this
+
+```
+root@debian:/home#
+MicroPython v1.18 on 2022-01-17; linux version
+Use Ctrl-D to exit, Ctrl-E for paste mode
+>>>
+```
+
+#### Manually run unittests
+
+In order to manually execute only a specific set of tests use the following
+command inside the container
+
+```bash
+# run all unittests defined in "tests" directory and exit with status result
+micropython-dev -c "import unittest; unittest.main('tests')"
+
+# run all tests of "TestAbsoluteTruth" defined in tests/test_absolute_truth.py
+# and exit with status result
+micropython-dev -c "import unittest; unittest.main(name='tests.test_absolute_truth', fromlist=['TestAbsoluteTruth'])"
+```
+
+#### Custom container for unittests
+
+```bash
+docker build \
+--tag micropython-test \
+--file Dockerfile.tests .
+```
+
+The unittests are executed during the building process. It will exit with a
+non-zero status in case of a unittest failure.
+
+The return value can be collected by `echo $?` (on Linux based systems), which
+will be either `0` in case all tests passed, or `1` if one or multiple tests
+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 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.
+
+```bash
+docker compose up --build --exit-code-from micropython-host
+```
+
+The option `--build` can be skipped on the second run, to avoid rebuilds of
+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
+```
+
+## Documentation
+
+The documentation is automatically generated on every merge to the develop
+branch and available [here][ref-rtd-micropython-modbus]
+
+### Install required packages
+
+```bash
+# create and activate virtual environment
+python3 -m venv .venv
+source .venv/bin/activate
+
+# install and upgrade required packages
+pip install -U -r docs/requirements.txt
+```
+
+### Create documentation
+
+Some usefull checks have been disabled in the `docs/conf.py` file. Please
+check the documentation build output locally before opening a PR.
+
+```bash
+# perform link checks
+sphinx-build docs/ docs/build/linkcheck -d docs/build/docs_doctree/ --color -blinkcheck -j auto -W
+
+# create documentation
+sphinx-build docs/ docs/build/html/ -d docs/build/docs_doctree/ --color -bhtml -j auto -W
+```
+
+The created documentation can be found at [`docs/build/html`](docs/build/html).
+
+<!-- Links -->
+[ref-registers-MyEVSE]: https://github.com/brainelectronics/micropython-modbus/blob/c45d6cc334b4adf0e0ffd9152c8f08724e1902d9/registers/modbusRegisters-MyEVSE.json
+[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-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/docs/changelog_link.rst b/docs/changelog_link.rst
new file mode 100755
index 0000000..e446ab8
--- /dev/null
+++ b/docs/changelog_link.rst
@@ -0,0 +1,3 @@
+
+.. include:: ../changelog.md
+   :parser: myst_parser.sphinx_
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100755
index 0000000..a225ebb
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,92 @@
+# Configuration file for the Sphinx documentation builder.
+
+import os
+import sys
+from pathlib import Path
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+sys.path.insert(0, os.path.abspath('../'))
+here = Path(__file__).parent.resolve()
+
+try:
+    import umodbus
+except ImportError:
+    raise SystemExit("umodbus has to be importable")
+else:
+    # Inject mock modules so that we can build the
+    # documentation without having the real stuff available
+    from mock import Mock
+
+    for mod_name in ['micropython', 'machine', 'urequests']:
+        sys.modules[mod_name] = Mock()
+        print("Mocked {}".format(mod_name))
+
+# load elements of version.py
+exec(open(here / '..' / 'umodbus' / 'version.py').read())
+
+# -- Project information
+
+project = 'micropython-modbus'
+copyright = '2022, brainelectronics'
+author = 'brainelectronics'
+
+version = __version__
+release = version
+
+# -- General configuration
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'myst_parser',
+    'sphinx.ext.autodoc',
+    'sphinx.ext.autosectionlabel',
+    'sphinx.ext.autosummary',
+    'sphinx.ext.doctest',
+    'sphinx.ext.duration',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.viewcode',
+]
+autosectionlabel_prefix_document = True
+
+# The suffix of source filenames.
+source_suffix = ['.rst', '.md']
+
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3/', None),
+    'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
+}
+intersphinx_disabled_domains = ['std']
+suppress_warnings = [
+    # throws an error due to not found reference targets to files not in docs/
+    'ref.myst',
+    # throws an error due to multiple "Added" labels in "changelog.md"
+    'autosectionlabel.*'
+]
+
+# A list of regular expressions that match URIs that should not be checked
+# when doing a linkcheck build.
+linkcheck_ignore = [
+    # tag 2.0.0 did not exist during docs introduction
+    'https://github.com/brainelectronics/micropython-modbus/tree/2.0.0',
+    # RTD page did not exist during docs introduction
+    'https://micropython-modbus.readthedocs.io/en/latest/',
+]
+
+templates_path = ['_templates']
+
+# -- Options for HTML output
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# -- Options for EPUB output
+epub_show_urls = 'footnote'
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100755
index 0000000..d45fdcc
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,22 @@
+micropython-modbus
+===================================
+
+Contents
+--------
+
+.. toctree::
+   :maxdepth: 1
+
+   readme_link
+   SETUP
+   USAGE
+   CONTRIBUTING
+   umodbus
+   UPGRADE
+   changelog_link
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
diff --git a/docs/readme_link.rst b/docs/readme_link.rst
new file mode 100755
index 0000000..dd42123
--- /dev/null
+++ b/docs/readme_link.rst
@@ -0,0 +1,3 @@
+
+.. include:: ../README.md
+   :parser: myst_parser.sphinx_
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100755
index 0000000..40e473f
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,16 @@
+# use fixed versions
+#
+# fix docutils to a version working for all
+docutils >=0.14,<0.18
+
+# sphinx 5.3.0 requires Jinja2 >=3.0 and docutils >=0.14,<0.20
+sphinx >=5.0.0,<6
+
+# sphinx-rtd-theme >=1.0.0 would require docutils <0.18
+sphinx-rtd-theme >=1.0.0,<2
+
+# replaces outdated and no longer maintained m2rr
+myst-parser >= 0.18.1,<1
+
+# mock imports of "micropython"
+mock >=4.0.3,<5
diff --git a/docs/umodbus.rst b/docs/umodbus.rst
new file mode 100755
index 0000000..8ad7a94
--- /dev/null
+++ b/docs/umodbus.rst
@@ -0,0 +1,47 @@
+micropython-modbus API
+=======================
+
+.. autosummary::
+   :toctree: generated
+
+umodbus.common module
+---------------------------------
+
+.. automodule:: umodbus.common
+   :members:
+   :show-inheritance:
+
+umodbus.const module
+---------------------------------
+
+.. automodule:: umodbus.const
+   :members:
+   :show-inheritance:
+
+umodbus.functions module
+---------------------------------
+
+.. automodule:: umodbus.functions
+   :members:
+   :show-inheritance:
+
+umodbus.modbus module
+---------------------------------
+
+.. automodule:: umodbus.modbus
+   :members:
+   :show-inheritance:
+
+umodbus.serial module
+---------------------------------
+
+.. automodule:: umodbus.serial
+   :members:
+   :show-inheritance:
+
+umodbus.tcp module
+---------------------------------
+
+.. automodule:: umodbus.tcp
+   :members:
+   :show-inheritance:
diff --git a/examples/example.json b/examples/example.json
deleted file mode 100644
index b819f70..0000000
--- a/examples/example.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-    "COILS": {
-        "EXAMPLE_COIL": {
-            "register": 123,
-            "len": 1,
-            "description": "Example COILS register, Coils (setter+getter) [0, 1]",
-            "range": "",
-            "unit": ""
-        }
-    },
-    "HREGS": {
-        "EXAMPLE_HREG": {
-            "register": 93,
-            "len": 1,
-            "description": "Example HREGS register, Hregs (setter+getter) [0, 65535]",
-            "range": "",
-            "unit": ""
-        }
-    },
-    "ISTS": {
-        "EXAMPLE_ISTS": {
-            "register": 67,
-            "len": 1,
-            "description": "Example ISTS register, Ists (only getter) [0, 1]",
-            "range": "",
-            "unit": ""
-        }
-    },
-    "IREGS": {
-        "EXAMPLE_IREG": {
-            "register": 10,
-            "len": 1,
-            "description": "Example IREGS register, Iregs (only getter) [0, 65535]",
-            "range": "",
-            "unit": ""
-        }
-    }
-}
\ No newline at end of file
diff --git a/examples/read_registers_rtu.sh b/examples/read_registers_rtu.sh
index f96a518..a5a19ec 100644
--- a/examples/read_registers_rtu.sh
+++ b/examples/read_registers_rtu.sh
@@ -2,8 +2,8 @@
 # title         :read_registers_rtu.sh
 # description   :Read Modbus registers via Serial RTU based on register JSON
 # author        :brainelectronics
-# date          :20220708
-# version       :0.1.0
+# date          :20221203
+# version       :0.1.1
 # usage         :sh read_registers_rtu.sh \
 #                /dev/tty.SLAB_USBtoUART \
 #                example.json \
@@ -18,7 +18,7 @@ register_file_path=$2
 modbus_unit=$3
 
 if [[ -z "$register_file_path" ]]; then
-    register_file_path=example.json
+    register_file_path=../registers/example.json
     echo "No register file given, using default at $register_file_path"
 fi
 
diff --git a/examples/read_registers_tcp.sh b/examples/read_registers_tcp.sh
index 030f434..2994e9a 100644
--- a/examples/read_registers_tcp.sh
+++ b/examples/read_registers_tcp.sh
@@ -2,8 +2,8 @@
 # title         :read_registers_tcp.sh
 # description   :Read Modbus registers via TCP based on register JSON
 # author        :brainelectronics
-# date          :20220708
-# version       :0.1.0
+# date          :20221203
+# version       :0.1.1
 # usage         :sh read_registers_tcp.sh \
 #                192.168.178.188 \
 #                example.json \
@@ -18,7 +18,7 @@ register_file_path=$2
 modbus_port=$3
 
 if [[ -z "$register_file_path" ]]; then
-    register_file_path=example.json
+    register_file_path=../registers/example.json
     echo "No register file given, using default at $register_file_path"
 fi
 
diff --git a/examples/rtu_client_example.py b/examples/rtu_client_example.py
index b0016bb..e15d568 100644
--- a/examples/rtu_client_example.py
+++ b/examples/rtu_client_example.py
@@ -15,7 +15,7 @@
 """
 
 # import modbus client classes
-from umodbus.modbus import ModbusRTU
+from umodbus.serial import ModbusRTU
 
 # ===============================================
 # RTU Slave setup
diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py
index 48793e9..789cf11 100644
--- a/examples/tcp_client_example.py
+++ b/examples/tcp_client_example.py
@@ -14,40 +14,53 @@
 """
 
 # system packages
-import network
 import time
 
 # import modbus client classes
-from umodbus.modbus import ModbusTCP
+from umodbus.tcp import ModbusTCP
+
+IS_DOCKER_MICROPYTHON = False
+try:
+    import network
+except ImportError:
+    IS_DOCKER_MICROPYTHON = True
+    import json
+
 
 # ===============================================
-# connect to a network
-station = network.WLAN(network.STA_IF)
-if station.active() and station.isconnected():
-    station.disconnect()
+if IS_DOCKER_MICROPYTHON is False:
+    # connect to a network
+    station = network.WLAN(network.STA_IF)
+    if station.active() and station.isconnected():
+        station.disconnect()
+        time.sleep(1)
+    station.active(False)
     time.sleep(1)
-station.active(False)
-time.sleep(1)
-station.active(True)
+    station.active(True)
 
-station.connect('SSID', 'PASSWORD')
-time.sleep(1)
+    # station.connect('SSID', 'PASSWORD')
+    station.connect('TP-LINK_FBFC3C', 'C1FBFC3C')
+    time.sleep(1)
 
-while True:
-    print('Waiting for WiFi connection...')
-    if station.isconnected():
-        print('Connected to WiFi.')
-        print(station.ifconfig())
-        break
-    time.sleep(2)
+    while True:
+        print('Waiting for WiFi connection...')
+        if station.isconnected():
+            print('Connected to WiFi.')
+            print(station.ifconfig())
+            break
+        time.sleep(2)
 
 # ===============================================
 # TCP Slave setup
 tcp_port = 502              # port to listen to
-# set IP address of the MicroPython device explicitly
-local_ip = '192.168.4.1'    # IP address
-# or get it from the system after a connection to the network has been made
-local_ip = station.ifconfig()[0]
+
+if IS_DOCKER_MICROPYTHON:
+    local_ip = '172.24.0.2'     # static Docker IP address
+else:
+    # set IP address of the MicroPython device explicitly
+    # local_ip = '192.168.4.1'    # IP address
+    # or get it from the system after a connection to the network has been made
+    local_ip = station.ifconfig()[0]
 
 # ModbusTCP can get TCP requests from a host device to provide/set data
 client = ModbusTCP()
@@ -62,6 +75,11 @@
 # 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,
@@ -91,13 +109,12 @@
     }
 }
 
-"""
 # 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
@@ -108,9 +125,17 @@
 
 print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port))
 
+reset_data_register = \
+    register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']
+
 while True:
     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 TCP client...')
         break
diff --git a/examples/tcp_host_example.py b/examples/tcp_host_example.py
index 8932818..80c5577 100644
--- a/examples/tcp_host_example.py
+++ b/examples/tcp_host_example.py
@@ -14,40 +14,52 @@
 """
 
 # system packages
-import network
 import time
 
 # import modbus host classes
-# from umodbus.modbus import ModbusTCP
 from umodbus.tcp import TCP as ModbusTCPMaster
 
+IS_DOCKER_MICROPYTHON = False
+try:
+    import network
+except ImportError:
+    IS_DOCKER_MICROPYTHON = True
+    import sys
+
+
 # ===============================================
-# connect to a network
-station = network.WLAN(network.STA_IF)
-if station.active() and station.isconnected():
-    station.disconnect()
+if IS_DOCKER_MICROPYTHON is False:
+    # connect to a network
+    station = network.WLAN(network.STA_IF)
+    if station.active() and station.isconnected():
+        station.disconnect()
+        time.sleep(1)
+    station.active(False)
     time.sleep(1)
-station.active(False)
-time.sleep(1)
-station.active(True)
+    station.active(True)
 
-station.connect('SSID', 'PASSWORD')
-time.sleep(1)
+    # station.connect('SSID', 'PASSWORD')
+    station.connect('TP-LINK_FBFC3C', 'C1FBFC3C')
+    time.sleep(1)
 
-while True:
-    print('Waiting for WiFi connection...')
-    if station.isconnected():
-        print('Connected to WiFi.')
-        print(station.ifconfig())
-        break
-    time.sleep(2)
+    while True:
+        print('Waiting for WiFi connection...')
+        if station.isconnected():
+            print('Connected to WiFi.')
+            print(station.ifconfig())
+            break
+        time.sleep(2)
 
 # ===============================================
 # TCP Slave setup
 slave_tcp_port = 502            # port to listen to
 slave_addr = 10                 # bus address of client
+
 # set IP address of the MicroPython device acting as client (slave)
-slave_ip = '192.168.178.69'     # IP address
+if IS_DOCKER_MICROPYTHON:
+    slave_ip = '172.24.0.2'     # static Docker IP address
+else:
+    slave_ip = '192.168.178.69'     # IP address
 
 # TCP Master setup
 # act as host, get Modbus data via TCP from a client device
@@ -61,6 +73,11 @@
 # 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,
@@ -184,6 +201,22 @@
 print('Status of IREG {}: {}'.format(ireg_address, register_value))
 time.sleep(1)
 
+# 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/examples/write_registers_tcp.sh b/examples/write_registers_tcp.sh
index eb3e1bc..16278e0 100644
--- a/examples/write_registers_tcp.sh
+++ b/examples/write_registers_tcp.sh
@@ -2,8 +2,8 @@
 # title         :write_registers_tcp.sh
 # description   :Write Modbus registers via TCP based on register JSON
 # author        :brainelectronics
-# date          :20221112
-# version       :0.1.0
+# date          :20221203
+# version       :0.1.1
 # usage         :sh write_registers_tcp.sh \
 #                192.168.178.69 \
 #                set-example.json \
@@ -18,7 +18,7 @@ register_file_path=$2
 modbus_port=$3
 
 if [[ -z "$register_file_path" ]]; then
-    register_file_path=set-example.json
+    register_file_path=../registers/set-example.json
     echo "No register file given, using default at $register_file_path"
 fi
 
diff --git a/registers/example.json b/registers/example.json
new file mode 100644
index 0000000..a353010
--- /dev/null
+++ b/registers/example.json
@@ -0,0 +1,106 @@
+{
+    "COILS": {
+        "RESET_REGISTER_DATA_COIL": {
+            "register": 42,
+            "len": 1,
+            "val": 0,
+            "description": "Set this COIL to true to reset all register values back to the default state/value",
+            "range": "",
+            "unit": ""
+        },
+        "EXAMPLE_COIL": {
+            "register": 123,
+            "len": 1,
+            "val": 1,
+            "description": "Example COILS register, Coils (setter+getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        },
+        "EXAMPLE_COIL_OFF": {
+            "register": 124,
+            "len": 1,
+            "val": 0,
+            "description": "Example COILS register, Coils (setter+getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        },
+        "EXAMPLE_COIL_MIXED": {
+            "register": 125,
+            "len": 2,
+            "val": [1, 0],
+            "description": "Example COILS registers with length of 2, Coils (setter+getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        },
+        "ANOTHER_EXAMPLE_COIL": {
+            "register": 126,
+            "len": 3,
+            "val": [0, 1, 0],
+            "description": "Example COILS registers with length of 3, Coils (setter+getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        }
+    },
+    "HREGS": {
+        "EXAMPLE_HREG": {
+            "register": 93,
+            "len": 1,
+            "val": 19,
+            "description": "Example HREGS register, Hregs (setter+getter) [0, 65535]",
+            "range": "",
+            "unit": ""
+        },
+        "ANOTHER_EXAMPLE_HREG": {
+            "register": 94,
+            "len": 3,
+            "val": [29, 38, 0],
+            "description": "Example HREGS registers with length of 3, Hregs (setter+getter) [0, 65535]",
+            "range": "",
+            "unit": ""
+        }
+    },
+    "ISTS": {
+        "EXAMPLE_ISTS": {
+            "register": 67,
+            "len": 1,
+            "val": 0,
+            "description": "Example ISTS register, Ists (only getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        },
+        "EXAMPLE_ISTS_MIXED": {
+            "register": 68,
+            "len": 2,
+            "val": [1, 0],
+            "description": "Example ISTS registers with length of 2, Ists (only getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        },
+        "ANOTHER_EXAMPLE_ISTS": {
+            "register": 69,
+            "len": 3,
+            "val": [0, 1, 0],
+            "description": "Example ISTS registers with length of 3, Ists (only getter) [0, 1]",
+            "range": "",
+            "unit": ""
+        }
+    },
+    "IREGS": {
+        "EXAMPLE_IREG": {
+            "register": 10,
+            "len": 1,
+            "val": 60001,
+            "description": "Example IREGS register, Iregs (only getter) [0, 65535]",
+            "range": "",
+            "unit": ""
+        },
+        "ANOTHER_EXAMPLE_IREG": {
+            "register": 11,
+            "len": 3,
+            "val": [59123, 0, 390],
+            "description": "Example IREGS registers with length of 3, Iregs (only getter) [0, 65535]",
+            "range": "",
+            "unit": ""
+        }
+    }
+}
\ No newline at end of file
diff --git a/examples/set-example.json b/registers/set-example.json
similarity index 100%
rename from examples/set-example.json
rename to registers/set-example.json
diff --git a/setup.py b/setup.py
index 4b07149..eb3c907 100644
--- a/setup.py
+++ b/setup.py
@@ -35,5 +35,5 @@
     license='MIT',
     cmdclass={'sdist': sdist_upip.sdist},
     packages=['umodbus'],
-    install_requires=[]
+    install_requires=['micropython-urequests']
 )
diff --git a/umodbus/const.py b/umodbus/const.py
index 3b38e21..3ebb228 100644
--- a/umodbus/const.py
+++ b/umodbus/const.py
@@ -8,50 +8,53 @@
 # available at https://www.pycom.io/opensource/licensing
 #
 
+from micropython import const
+
 # function codes
-READ_COILS = 0x01                   # COILS, [0, 1]
-READ_DISCRETE_INPUTS = 0x02         # ISTS,  [0, 1]
-READ_HOLDING_REGISTERS = 0x03       # HREGS, [0, 65535]
-READ_INPUT_REGISTER = 0x04          # IREGS, [0, 65535]
+# defined as const(), see https://github.com/micropython/micropython/issues/573
+READ_COILS = const(0x01)                # COILS, [0, 1]
+READ_DISCRETE_INPUTS = const(0x02)      # ISTS,  [0, 1]
+READ_HOLDING_REGISTERS = const(0x03)    # HREGS, [0, 65535]
+READ_INPUT_REGISTER = const(0x04)       # IREGS, [0, 65535]
 
-WRITE_SINGLE_COIL = 0x05            # COILS, [0, 1]
-WRITE_SINGLE_REGISTER = 0x06        # HREGS, [0, 65535]
-WRITE_MULTIPLE_COILS = 0x0F         # COILS, [0, 1]
-WRITE_MULTIPLE_REGISTERS = 0x10     # HREGS, [0, 65535]
+WRITE_SINGLE_COIL = const(0x05)         # COILS, [0, 1]
+WRITE_SINGLE_REGISTER = const(0x06)     # HREGS, [0, 65535]
+WRITE_MULTIPLE_COILS = const(0x0F)      # COILS, [0, 1]
+WRITE_MULTIPLE_REGISTERS = const(0x10)  # HREGS, [0, 65535]
 
-MASK_WRITE_REGISTER = 0x16
-READ_WRITE_MULTIPLE_REGISTERS = 0x17
+MASK_WRITE_REGISTER = const(0x16)
+READ_WRITE_MULTIPLE_REGISTERS = const(0x17)
 
-READ_FIFO_QUEUE = 0x18
+READ_FIFO_QUEUE = const(0x18)
 
-READ_FILE_RECORD = 0x14
-WRITE_FILE_RECORD = 0x15
+READ_FILE_RECORD = const(0x14)
+WRITE_FILE_RECORD = const(0x15)
 
-READ_EXCEPTION_STATUS = 0x07
-DIAGNOSTICS = 0x08
-GET_COM_EVENT_COUNTER = 0x0B
-GET_COM_EVENT_LOG = 0x0C
-REPORT_SERVER_ID = 0x11
-READ_DEVICE_IDENTIFICATION = 0x2B
+READ_EXCEPTION_STATUS = const(0x07)
+DIAGNOSTICS = const(0x08)
+GET_COM_EVENT_COUNTER = const(0x0B)
+GET_COM_EVENT_LOG = const(0x0C)
+REPORT_SERVER_ID = const(0x11)
+READ_DEVICE_IDENTIFICATION = const(0x2B)
 
 # exception codes
-ILLEGAL_FUNCTION = 0x01
-ILLEGAL_DATA_ADDRESS = 0x02
-ILLEGAL_DATA_VALUE = 0x03
-SERVER_DEVICE_FAILURE = 0x04
-ACKNOWLEDGE = 0x05
-SERVER_DEVICE_BUSY = 0x06
-MEMORY_PARITY_ERROR = 0x08
-GATEWAY_PATH_UNAVAILABLE = 0x0A
-DEVICE_FAILED_TO_RESPOND = 0x0B
+ILLEGAL_FUNCTION = const(0x01)
+ILLEGAL_DATA_ADDRESS = const(0x02)
+ILLEGAL_DATA_VALUE = const(0x03)
+SERVER_DEVICE_FAILURE = const(0x04)
+ACKNOWLEDGE = const(0x05)
+SERVER_DEVICE_BUSY = const(0x06)
+MEMORY_PARITY_ERROR = const(0x08)
+GATEWAY_PATH_UNAVAILABLE = const(0x0A)
+DEVICE_FAILED_TO_RESPOND = const(0x0B)
 
 # PDU constants
-CRC_LENGTH = 0x02
-ERROR_BIAS = 0x80
-RESPONSE_HDR_LENGTH = 0x02
-ERROR_RESP_LEN = 0x05
-FIXED_RESP_LEN = 0x08
-MBAP_HDR_LENGTH = 0x07
+CRC_LENGTH = const(0x02)
+ERROR_BIAS = const(0x80)
+RESPONSE_HDR_LENGTH = const(0x02)
+ERROR_RESP_LEN = const(0x05)
+FIXED_RESP_LEN = const(0x08)
+MBAP_HDR_LENGTH = const(0x07)
 
 CRC16_TABLE = (
     0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601,
@@ -85,17 +88,17 @@
     0x4100, 0x81C1, 0x8081, 0x4040
 )
 
-""" Code to generate the CRC-16 lookup table:
-def generate_crc16_table():
-    crc_table = []
-    for byte in range(256):
-        crc = 0x0000
-        for _ in range(8):
-            if (byte ^ crc) & 0x0001:
-                crc = (crc >> 1) ^ 0xa001
-            else:
-                crc >>= 1
-            byte >>= 1
-        crc_table.append(crc)
-    return crc_table
-"""
+
+# Code to generate the CRC-16 lookup table:
+# def generate_crc16_table():
+#     crc_table = []
+#     for byte in range(256):
+#         crc = 0x0000
+#         for _ in range(8):
+#             if (byte ^ crc) & 0x0001:
+#                 crc = (crc >> 1) ^ 0xa001
+#             else:
+#                 crc >>= 1
+#             byte >>= 1
+#         crc_table.append(crc)
+#     return crc_table
diff --git a/umodbus/functions.py b/umodbus/functions.py
index 1309826..c55d006 100644
--- a/umodbus/functions.py
+++ b/umodbus/functions.py
@@ -14,37 +14,105 @@
 # custom packages
 from . import const as Const
 
+# typing not natively supported on MicroPython
+from .typing import List, Optional, Union
 
-def read_coils(starting_address, quantity):
+
+def read_coils(starting_address: int, quantity: int) -> bytes:
+    """
+    Create Modbus Protocol Data Unit for reading coils.
+
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      quantity:          Quantity of coils
+    :type       quantity:          int
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     if not (1 <= quantity <= 2000):
-        raise ValueError('invalid number of coils')
+        raise ValueError('Invalid number of coils')
 
     return struct.pack('>BHH', Const.READ_COILS, starting_address, quantity)
 
 
-def read_discrete_inputs(starting_address, quantity):
+def read_discrete_inputs(starting_address: int, quantity: int) -> bytes:
+    """
+    Create Modbus Protocol Data Unit for reading discrete inputs.
+
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      quantity:          Quantity of coils
+    :type       quantity:          int
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     if not (1 <= quantity <= 2000):
-        raise ValueError('invalid number of discrete inputs')
+        raise ValueError('Invalid number of discrete inputs')
 
-    return struct.pack('>BHH', Const.READ_DISCRETE_INPUTS, starting_address, quantity)
+    return struct.pack('>BHH',
+                       Const.READ_DISCRETE_INPUTS,
+                       starting_address,
+                       quantity)
 
 
-def read_holding_registers(starting_address, quantity):
+def read_holding_registers(starting_address: int, quantity: int) -> bytes:
+    """
+    Create Modbus Protocol Data Unit for reading holding registers.
+
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      quantity:          Quantity of coils
+    :type       quantity:          int
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     if not (1 <= quantity <= 125):
-        raise ValueError('invalid number of holding registers')
+        raise ValueError('Invalid number of holding registers')
+
+    return struct.pack('>BHH',
+                       Const.READ_HOLDING_REGISTERS,
+                       starting_address,
+                       quantity)
 
-    return struct.pack('>BHH', Const.READ_HOLDING_REGISTERS, starting_address, quantity)
 
+def read_input_registers(starting_address: int, quantity: int) -> bytes:
+    """
+    Create Modbus Protocol Data Unit for reading input registers.
 
-def read_input_registers(starting_address, quantity):
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      quantity:          Quantity of coils
+    :type       quantity:          int
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     if not (1 <= quantity <= 125):
-        raise ValueError('invalid number of input registers')
+        raise ValueError('Invalid number of input registers')
+
+    return struct.pack('>BHH',
+                       Const.READ_INPUT_REGISTER,
+                       starting_address,
+                       quantity)
 
-    return struct.pack('>BHH', Const.READ_INPUT_REGISTER, starting_address, quantity)
 
+def write_single_coil(output_address: int,
+                      output_value: Union[int, bool]) -> bytes:
+    """
+    Create Modbus message to update single coil
 
-def write_single_coil(output_address, output_value):
-    if output_value not in [0x0000, 0xFF00, True]:
+    :param      output_address:  The output address
+    :type       output_address:  int
+    :param      output_value:    The output value
+    :type       output_value:    Union[int, bool]
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
+    if output_value not in [0x0000, 0xFF00, False, True, 0, 1]:
         raise ValueError('Illegal coil value')
 
     if output_value not in [0x0000, 0xFF00]:
@@ -53,53 +121,148 @@ def write_single_coil(output_address, output_value):
         else:
             output_value = 0x0000
 
-    return struct.pack('>BHH', Const.WRITE_SINGLE_COIL, output_address, output_value)
+    return struct.pack('>BHH',
+                       Const.WRITE_SINGLE_COIL,
+                       output_address,
+                       output_value)
+
+
+def write_single_register(register_address: int,
+                          register_value: int,
+                          signed: bool = True) -> bytes:
+    """
+    Create Modbus message to writes a single register
 
+    :param      register_address:  The register address
+    :type       register_address:  int
+    :param      register_value:    The register value
+    :type       register_value:    int
+    :param      signed:            Flag whether data is signed or not
+    :type       signed:            bool
 
-def write_single_register(register_address, register_value, signed=True):
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     fmt = 'h' if signed else 'H'
 
-    return struct.pack('>BH' + fmt, Const.WRITE_SINGLE_REGISTER, register_address, register_value)
+    return struct.pack('>BH' + fmt,
+                       Const.WRITE_SINGLE_REGISTER,
+                       register_address,
+                       register_value)
+
+
+def write_multiple_coils(starting_address: int,
+                         value_list: List[int, bool]) -> bytes:
+    """
+    Create Modbus message to update multiple coils
 
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      value_list:        The list of output values
+    :type       value_list:        List[int, bool]
 
-def write_multiple_coils(starting_address, value_list):
-    sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)]
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
+    if not (1 <= len(value_list) <= 0x07B0):
+        raise ValueError('Invalid quantity of outputs')
+
+    sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)]    # noqa: E501
 
     output_value = []
     for index, byte in enumerate(sectioned_list):
-        output = sum(v << i for i, v in enumerate(byte))
+        # see https://github.com/brainelectronics/micropython-modbus/issues/22
+        # output = sum(v << i for i, v in enumerate(byte))
+        output = 0
+        for bit in byte:
+            output = (output << 1) | bit
         output_value.append(output)
 
     fmt = 'B' * len(output_value)
 
-    return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_COILS, starting_address, len(value_list), ((len(value_list) - 1) // 8) + 1, *output_value)
-
-
-def write_multiple_registers(starting_address, register_values, signed=True):
+    return struct.pack('>BHHB' + fmt,
+                       Const.WRITE_MULTIPLE_COILS,
+                       starting_address,
+                       len(value_list),     # quantity of outputs
+                       ((len(value_list) - 1) // 8) + 1,    # byte count
+                       *output_value)
+
+
+def write_multiple_registers(starting_address: int,
+                             register_values: List[int],
+                             signed: bool = True) -> bytes:
+    """
+    Create Modbus message to update multiple coils
+
+    :param      starting_address:  The starting address
+    :type       starting_address:  int
+    :param      register_values:   The list of output value
+    :type       register_values:   List[int, bool]
+    :param      signed:            Flag whether data is signed or not
+    :type       signed:            bool
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
     quantity = len(register_values)
 
     if not (1 <= quantity <= 123):
-        raise ValueError('invalid number of registers')
+        raise ValueError('Invalid number of registers')
 
     fmt = ('h' if signed else 'H') * quantity
-    return struct.pack('>BHHB' + fmt, Const.WRITE_MULTIPLE_REGISTERS, starting_address, quantity, quantity * 2, *register_values)
+    return struct.pack('>BHHB' + fmt,
+                       Const.WRITE_MULTIPLE_REGISTERS,
+                       starting_address,
+                       quantity,
+                       quantity * 2,
+                       *register_values)
+
+
+def validate_resp_data(data: bytes,
+                       function_code: int,
+                       address: int,
+                       value: int = None,
+                       quantity: int = None,
+                       signed: bool = True) -> bool:
+    """
+    Validate the response data.
+
+    :param      data:           The data
+    :type       data:           bytes
+    :param      function_code:  The function code
+    :type       function_code:  int
+    :param      address:        The address
+    :type       address:        int
+    :param      value:          The value
+    :type       value:          int
+    :param      quantity:       The quantity
+    :type       quantity:       int
+    :param      signed:         Indicates if signed
+    :type       signed:         bool
+
+    :returns:   True if valid, False otherwise
+    :rtype:     bool
+    """
+    fmt = '>H' + ('h' if signed else 'H')
 
-
-def validate_resp_data(data,
-                       function_code,
-                       address,
-                       value=None,
-                       quantity=None,
-                       signed=True):
     if function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]:
-        fmt = '>H' + ('h' if signed else 'H')
         resp_addr, resp_value = struct.unpack(fmt, data)
 
+        # if bool(True) or int(1) is used as "output_value" of
+        # "write_single_coil" it will be internally converted to int(0xFF00),
+        # see Modbus specification, which is actually int(65280).
+        # Due to the non binary, but real value comparison of "value" and
+        # "resp_value", it would never match without the next two lines
+        # see #21
+        if function_code == Const.WRITE_SINGLE_COIL:
+            resp_value = bool(resp_value)
+            value = bool(value)
+
         if (address == resp_addr) and (value == resp_value):
             return True
-
-    elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]:
-        resp_addr, resp_qty = struct.unpack('>HH', data)
+    elif function_code in [Const.WRITE_MULTIPLE_COILS,
+                           Const.WRITE_MULTIPLE_REGISTERS]:
+        resp_addr, resp_qty = struct.unpack(fmt, data)
 
         if (address == resp_addr) and (quantity == resp_qty):
             return True
@@ -107,24 +270,32 @@ def validate_resp_data(data,
     return False
 
 
-def response(function_code,
-             request_register_addr,
-             request_register_qty,
+def response(function_code: int,
+             request_register_addr: int,
+             request_register_qty: int,
              request_data,
              value_list=None,
              signed=True):
     if function_code in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]:
-        output_value = []
-        sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)]
+        sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)]    # noqa: E501
 
+        output_value = []
         for index, byte in enumerate(sectioned_list):
-            output = sum(v << i for i, v in enumerate(byte))
+            # see https://github.com/brainelectronics/micropython-modbus/issues/22
+            # output = sum(v << i for i, v in enumerate(byte))
+            output = 0
+            for bit in byte:
+                output = (output << 1) | bit
             output_value.append(output)
 
         fmt = 'B' * len(output_value)
-        return struct.pack('>BB' + fmt, function_code, ((len(value_list) - 1) // 8) + 1, *output_value)
+        return struct.pack('>BB' + fmt,
+                           function_code,
+                           ((len(value_list) - 1) // 8) + 1,
+                           *output_value)
 
-    elif function_code in [Const.READ_HOLDING_REGISTERS, Const.READ_INPUT_REGISTER]:
+    elif function_code in [Const.READ_HOLDING_REGISTERS,
+                           Const.READ_INPUT_REGISTER]:
         quantity = len(value_list)
 
         if not (0x0001 <= quantity <= 0x007D):
@@ -137,26 +308,121 @@ def response(function_code,
             for s in signed:
                 fmt += 'h' if s else 'H'
 
-        return struct.pack('>BB' + fmt, function_code, quantity * 2, *value_list)
+        return struct.pack('>BB' + fmt,
+                           function_code,
+                           quantity * 2,
+                           *value_list)
+
+    elif function_code in [Const.WRITE_SINGLE_COIL,
+                           Const.WRITE_SINGLE_REGISTER]:
+        return struct.pack('>BHBB',
+                           function_code,
+                           request_register_addr,
+                           *request_data)
+
+    elif function_code in [Const.WRITE_MULTIPLE_COILS,
+                           Const.WRITE_MULTIPLE_REGISTERS]:
+        return struct.pack('>BHH',
+                           function_code,
+                           request_register_addr,
+                           request_register_qty)
+
+
+def exception_response(function_code: int, exception_code: int) -> bytes:
+    """
+    Create Modbus exception response
+
+    :param      function_code:   The function code
+    :type       function_code:   int
+    :param      exception_code:  The exception code
+    :type       exception_code:  int
+
+    :returns:   Packed Modbus message
+    :rtype:     bytes
+    """
+    return struct.pack('>BB', Const.ERROR_BIAS + function_code, exception_code)
 
-    elif function_code in [Const.WRITE_SINGLE_COIL, Const.WRITE_SINGLE_REGISTER]:
-        return struct.pack('>BHBB', function_code, request_register_addr, *request_data)
 
-    elif function_code in [Const.WRITE_MULTIPLE_COILS, Const.WRITE_MULTIPLE_REGISTERS]:
-        return struct.pack('>BHH', function_code, request_register_addr, request_register_qty)
+def bytes_to_bool(byte_list: bytes, bit_qty: Optional[int] = 1) -> List[bool]:
+    """
+    Convert bytes to list of boolean values
 
+    :param      byte_list:  The byte list
+    :type       byte_list:  bytes
+    :param      bit_qty:    Amount of bits received
+    :type       bit_qty:    Optional[int]
 
-def exception_response(function_code, exception_code):
-    return struct.pack('>BB', Const.ERROR_BIAS + function_code, exception_code)
+    :returns:   Boolean representation
+    :rtype:     List[bool]
+    """
+    # evil hack for missing keyword support in MicroPython format()
+    fmt = '{:0' + str(bit_qty) + 'b}'
+
+    bool_list = [bool(int(x)) for x in fmt.format(list(byte_list)[0])]
+
+    # invert list due to byte order
+    return bool_list[::-1]
+
+
+def to_short(byte_array: bytes, signed: bool = True) -> bytes:
+    """
+    Convert bytes to tuple of integer values
+
+    :param      byte_array:  The byte array
+    :type       byte_array:  bytes
+    :param      signed:      Indicates if signed
+    :type       signed:      bool
 
+    :returns:   Integer representation
+    :rtype:     bytes
+    """
+    response_quantity = int(len(byte_array) / 2)
+    fmt = '>' + (('h' if signed else 'H') * response_quantity)
 
-def float_to_bin(num):
-    return bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:].zfill(32)
+    return struct.unpack(fmt, byte_array)
 
 
-def bin_to_float(binary):
+def float_to_bin(num: float) -> bin:
+    """
+    Convert floating point value to binary
+
+    See IEEE 754
+
+    :param      num:  The number
+    :type       num:  float
+
+    :returns:   Binary representation
+    :rtype:     bin
+    """
+    # no "zfill" available in MicroPython
+    # return bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:].zfill(32)
+
+    return '{:0>{w}}'.format(
+        bin(struct.unpack('!I', struct.pack('!f', num))[0])[2:],
+        w=32)
+
+
+def bin_to_float(binary: str) -> float:
+    """
+    Convert binary string to floating point value
+
+    :param      binary:  The binary string
+    :type       binary:  str
+
+    :returns:   Converted floating point value
+    :rtype:     float
+    """
     return struct.unpack('!f', struct.pack('!I', int(binary, 2)))[0]
 
 
-def int_to_bin(num):
+def int_to_bin(num: int) -> str:
+    """
+    Convert integer to binary
+
+    :param      num:  The number
+    :type       num:  int
+
+    :returns:   Binary representation of given input
+    :rtype:     str
+    """
     return "{0:b}".format(num)
diff --git a/umodbus/modbus.py b/umodbus/modbus.py
index 7f425ac..2ffbc49 100644
--- a/umodbus/modbus.py
+++ b/umodbus/modbus.py
@@ -9,10 +9,6 @@
 import time
 
 # custom packages
-from .serial import Serial
-from .tcp import TCPServer
-from . import const as ModbusConst
-from urequests import request
 
 # typing not natively supported on MicroPython
 from .typing import List, Optional
@@ -31,7 +27,7 @@ def __init__(self, itf, addr_list: list):
         for reg_type in self._available_register_types:
             self._register_dict[reg_type] = dict()
         self._default_vals = dict(zip(self._available_register_types,
-                                      [True, 999, 999, True]))
+                                      [False, 0, 0, False]))
 
         # registers which can be set by remote device
         self._changeable_register_types = ['COILS', 'HREGS']
@@ -475,143 +471,6 @@ def _remove_changed_register(self,
 
         return result
 
-    def process(self) -> bool:
-        """
-        Process the modbus requests.
-
-        :returns:   Result of processing, True on success, False otherwise
-        :rtype:     bool
-        """
-        reg_type = None
-        req_type = None
-
-        request = self._itf.get_request(unit_addr_list=self._addr_list,
-                                        timeout=0)
-        if request is None:
-            return False
-
-        if request.function == ModbusConst.READ_COILS:
-            # Coils (setter+getter) [0, 1]
-            # function 01 - read single register
-            reg_type = 'COILS'
-            req_type = 'READ'
-        elif request.function == ModbusConst.READ_DISCRETE_INPUTS:
-            # Ists (only getter) [0, 1]
-            # function 02 - read input status (discrete inputs/digital input)
-            reg_type = 'ISTS'
-            req_type = 'READ'
-        elif request.function == ModbusConst.READ_HOLDING_REGISTERS:
-            # Hregs (setter+getter) [0, 65535]
-            # function 03 - read holding register
-            reg_type = 'HREGS'
-            req_type = 'READ'
-        elif request.function == ModbusConst.READ_INPUT_REGISTER:
-            # Iregs (only getter) [0, 65535]
-            # function 04 - read input registers
-            reg_type = 'IREGS'
-            req_type = 'READ'
-        elif request.function == ModbusConst.WRITE_SINGLE_COIL:
-            # Coils (setter+getter) [0, 1]
-            # function 05 - write single register
-            reg_type = 'COILS'
-            req_type = 'WRITE'
-        elif request.function == ModbusConst.WRITE_SINGLE_REGISTER:
-            # Hregs (setter+getter) [0, 65535]
-            # function 06 - write holding register
-            reg_type = 'HREGS'
-            req_type = 'WRITE'
-        else:
-            request.send_exception(ModbusConst.ILLEGAL_FUNCTION)
-
-        if reg_type:
-            if req_type == 'READ':
-                self._process_read_access(request=request, reg_type=reg_type)
-            elif req_type == 'WRITE':
-                self._process_write_access(request=request, reg_type=reg_type)
-
-        return True
-
-    def _create_response(self, request: request, reg_type: str):
-        """
-        Create a response.
-
-        :param      request:   The request
-        :type       request:   request
-        :param      reg_type:  The register type
-        :type       reg_type:  str
-
-        :returns:   Values of this register
-        :rtype:     Union[bool, int, List[int], List[bool]]
-        """
-        if type(self._register_dict[reg_type][request.register_addr]) is list:
-            return self._register_dict[reg_type][request.register_addr]
-        else:
-            return [self._register_dict[reg_type][request.register_addr]]
-
-    def _process_read_access(self, request: request, reg_type: str) -> None:
-        """
-        Process read access to register
-
-        :param      request:   The request
-        :type       request:   request
-        :param      reg_type:  The register type
-        :type       reg_type:  str
-        """
-        if request.register_addr in self._register_dict[reg_type]:
-            vals = self._create_response(request=request, reg_type=reg_type)
-            request.send_response(vals)
-        else:
-            request.send_exception(ModbusConst.ILLEGAL_DATA_ADDRESS)
-
-    def _process_write_access(self, request: request, reg_type: str) -> None:
-        """
-        Process write access to register
-
-        :param      request:   The request
-        :type       request:   request
-        :param      reg_type:  The register type
-        :type       reg_type:  str
-        """
-        address = request.register_addr
-        val = 0
-        valid_register = False
-
-        if address in self._register_dict[reg_type]:
-            if reg_type == 'COILS':
-                val = request.data[0]
-                if val == 0x00:
-                    val = False
-                    valid_register = True
-
-                    request.send_response()
-
-                    self.set_coil(address=address, value=val)
-                elif val == 0xFF:
-                    val = True
-                    valid_register = True
-
-                    request.send_response()
-
-                    self.set_coil(address=address, value=val)
-                else:
-                    request.send_exception(ModbusConst.ILLEGAL_DATA_VALUE)
-            elif reg_type == 'HREGS':
-                valid_register = True
-                val = request.data_as_registers(signed=False)[0]
-
-                request.send_response()
-
-                self.set_hreg(address=address, value=val)
-            else:
-                pass
-
-            if valid_register:
-                self._set_changed_register(reg_type=reg_type,
-                                           address=address,
-                                           value=val)
-        else:
-            request.send_exception(ModbusConst.ILLEGAL_DATA_ADDRESS)
-
     def setup_registers(self,
                         registers: dict = dict(),
                         use_default_vals: Optional[bool] = False) -> None:
@@ -654,46 +513,3 @@ def setup_registers(self,
                             pass
                 else:
                     pass
-
-
-class ModbusRTU(Modbus):
-    def __init__(self,
-                 addr,
-                 baudrate=9600,
-                 data_bits=8,
-                 stop_bits=1,
-                 parity=None,
-                 pins=None,
-                 ctrl_pin=None):
-        super().__init__(
-            # set itf to Serial object, addr_list to [addr]
-            Serial(uart_id=1,
-                   baudrate=baudrate,
-                   data_bits=data_bits,
-                   stop_bits=stop_bits,
-                   parity=parity,
-                   pins=pins,
-                   ctrl_pin=ctrl_pin),
-            [addr]
-        )
-
-
-class ModbusTCP(Modbus):
-    def __init__(self):
-        super().__init__(
-            # set itf to TCPServer object, addr_list to None
-            TCPServer(),
-            None
-        )
-
-    def bind(self,
-             local_ip: str,
-             local_port: int = 502,
-             max_connections: int = 10) -> None:
-        self._itf.bind(local_ip, local_port, max_connections)
-
-    def get_bound_status(self) -> bool:
-        try:
-            return self._itf.get_is_bound()
-        except Exception:
-            return False
diff --git a/umodbus/serial.py b/umodbus/serial.py
index cf070aa..0d19a44 100644
--- a/umodbus/serial.py
+++ b/umodbus/serial.py
@@ -20,17 +20,43 @@
 from . import functions
 from .common import Request
 from .common import ModbusException
+from .modbus import Modbus
 
+# typing not natively supported on MicroPython
+from .typing import List, Optional, Union
 
-class Serial(object):
+
+class ModbusRTU(Modbus):
     def __init__(self,
-                 uart_id=1,
+                 addr,
                  baudrate=9600,
                  data_bits=8,
                  stop_bits=1,
                  parity=None,
                  pins=None,
                  ctrl_pin=None):
+        super().__init__(
+            # set itf to Serial object, addr_list to [addr]
+            Serial(uart_id=1,
+                   baudrate=baudrate,
+                   data_bits=data_bits,
+                   stop_bits=stop_bits,
+                   parity=parity,
+                   pins=pins,
+                   ctrl_pin=ctrl_pin),
+            [addr]
+        )
+
+
+class Serial(object):
+    def __init__(self,
+                 uart_id: int = 1,
+                 baudrate: int = 9600,
+                 data_bits: int = 8,
+                 stop_bits: int = 1,
+                 parity=None,
+                 pins: List[int, int] = None,
+                 ctrl_pin: int = None):
         self._uart = UART(uart_id,
                           baudrate=baudrate,
                           bits=data_bits,
@@ -61,21 +87,6 @@ def _calculate_crc16(self, data):
 
         return struct.pack('<H', crc)
 
-    def _bytes_to_bool(self, byte_list):
-        bool_list = []
-
-        for index, byte in enumerate(byte_list):
-            bool_list.extend([bool(byte & (1 << n)) for n in range(8)])
-
-        return bool_list
-
-    def _to_short(self, byte_array, signed=True):
-        response_quantity = int(len(byte_array) / 2)
-
-        fmt = '>' + (('h' if signed else 'H') * response_quantity)
-
-        return struct.unpack(fmt, byte_array)
-
     def _exit_read(self, response):
         if response[1] >= Const.ERROR_BIAS:
             if len(response) < Const.ERROR_RESP_LEN:
@@ -89,7 +100,13 @@ def _exit_read(self, response):
 
         return True
 
-    def _uart_read(self):
+    def _uart_read(self) -> bytearray:
+        """
+        Read up to 40 bytes from UART
+
+        :returns:   Read content
+        :rtype:     bytearray
+        """
         response = bytearray()
 
         for x in range(1, 40):
@@ -105,7 +122,7 @@ def _uart_read(self):
 
         return response
 
-    def _uart_read_frame(self, timeout=None):
+    def _uart_read_frame(self, timeout: Optional[int] = None):
         received_bytes = bytearray()
 
         # set timeout to at least twice the time between two frames in case the
@@ -145,7 +162,17 @@ def _uart_read_frame(self, timeout=None):
         # return the result in case the overall timeout has been reached
         return received_bytes
 
-    def _send(self, modbus_pdu, slave_addr):
+    def _send(self, modbus_pdu: bytes, slave_addr: int) -> None:
+        """
+        Send Modbus frame via UART
+
+        If a flow control pin has been setup, it will be controller accordingly
+
+        :param      modbus_pdu:  The modbus Protocol Data Unit
+        :type       modbus_pdu:  bytes
+        :param      slave_addr:  The slave address
+        :type       slave_addr:  int
+        """
         serial_pdu = bytearray()
         serial_pdu.append(slave_addr)
         serial_pdu.extend(modbus_pdu)
@@ -164,22 +191,63 @@ def _send(self, modbus_pdu, slave_addr):
             time.sleep_us(self._t35chars)
             self._ctrlPin(0)
 
-    def _send_receive(self, modbus_pdu, slave_addr, count):
+    def _send_receive(self,
+                      modbus_pdu: bytes,
+                      slave_addr: int,
+                      count: bool) -> bytes:
+        """
+        Send a modbus message and receive the reponse.
+
+        :param      modbus_pdu:  The modbus Protocol Data Unit
+        :type       modbus_pdu:  bytes
+        :param      slave_addr:  The slave address
+        :type       slave_addr:  int
+        :param      count:       The count
+        :type       count:       bool
+
+        :returns:   Validated response content
+        :rtype:     bytes
+        """
         # flush the Rx FIFO
         self._uart.read()
 
-        self._send(modbus_pdu, slave_addr)
-
-        return self._validate_resp_hdr(self._uart_read(), slave_addr, modbus_pdu[0], count)
-
-    def _validate_resp_hdr(self, response, slave_addr, function_code, count):
+        self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr)
+
+        return self._validate_resp_hdr(response=self._uart_read(),
+                                       slave_addr=slave_addr,
+                                       function_code=modbus_pdu[0],
+                                       count=count)
+
+    def _validate_resp_hdr(self,
+                           response: bytearray,
+                           slave_addr: int,
+                           function_code: int,
+                           count: bool) -> bytes:
+        """
+        Validate the response header.
+
+        :param      response:       The response
+        :type       response:       bytearray
+        :param      slave_addr:     The slave address
+        :type       slave_addr:     int
+        :param      function_code:  The function code
+        :type       function_code:  int
+        :param      count:          The count
+        :type       count:          bool
+
+        :returns:   Modbus response content
+        :rtype:     bytes
+        """
         if len(response) == 0:
             raise OSError('no data received from slave')
 
         resp_crc = response[-Const.CRC_LENGTH:]
-        expected_crc = self._calculate_crc16(response[0:len(response) - Const.CRC_LENGTH])
+        expected_crc = self._calculate_crc16(
+            response[0:len(response) - Const.CRC_LENGTH]
+        )
 
-        if ((resp_crc[0] is not expected_crc[0]) or (resp_crc[1] is not expected_crc[1])):
+        if ((resp_crc[0] is not expected_crc[0]) or
+                (resp_crc[1] is not expected_crc[1])):
             raise OSError('invalid response CRC')
 
         if (response[0] != slave_addr):
@@ -189,139 +257,199 @@ def _validate_resp_hdr(self, response, slave_addr, function_code, count):
             raise ValueError('slave returned exception code: {:d}'.
                              format(response[2]))
 
-        hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else Const.RESPONSE_HDR_LENGTH
+        hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else \
+            Const.RESPONSE_HDR_LENGTH
 
         return response[hdr_length:len(response) - Const.CRC_LENGTH]
 
-    def read_coils(self, slave_addr, starting_addr, coil_qty):
-        modbus_pdu = functions.read_coils(starting_addr, coil_qty)
+    def read_coils(self,
+                   slave_addr: int,
+                   starting_addr: int,
+                   coil_qty: int) -> List[bool]:
+        modbus_pdu = functions.read_coils(starting_address=starting_addr,
+                                          quantity=coil_qty)
 
-        resp_data = self._send_receive(modbus_pdu, slave_addr, True)
-        status_pdu = self._bytes_to_bool(resp_data)
+        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, starting_addr, input_qty):
-        modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty)
+    def read_discrete_inputs(self,
+                             slave_addr: int,
+                             starting_addr: int,
+                             input_qty: int) -> List[bool]:
+        modbus_pdu = functions.read_discrete_inputs(
+            starting_address=starting_addr,
+            quantity=input_qty)
 
-        resp_data = self._send_receive(modbus_pdu, slave_addr, True)
-        status_pdu = self._bytes_to_bool(resp_data)
+        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,
-                               starting_addr,
-                               register_qty,
-                               signed=True):
-        modbus_pdu = functions.read_holding_registers(starting_addr, register_qty)
-
-        resp_data = self._send_receive(modbus_pdu, slave_addr, True)
-        register_value = self._to_short(resp_data, signed)
+                               slave_addr: int,
+                               starting_addr: int,
+                               register_qty: int,
+                               signed: bool = True) -> List[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,
-                             starting_addr,
-                             register_qty,
-                             signed=True):
-        modbus_pdu = functions.read_input_registers(starting_addr,
-                                                    register_qty)
-
-        resp_data = self._send_receive(modbus_pdu, slave_addr, True)
-        register_value = self._to_short(resp_data, signed)
+                             slave_addr: int,
+                             starting_addr: int,
+                             register_qty: int,
+                             signed: bool = True) -> List[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, output_address, output_value):
-        modbus_pdu = functions.write_single_coil(output_address, output_value)
-
-        resp_data = self._send_receive(modbus_pdu, slave_addr, False)
-        operation_status = functions.validate_resp_data(resp_data,
-                                                        Const.WRITE_SINGLE_COIL,
-                                                        output_address,
-                                                        value=output_value,
-                                                        signed=False)
+    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,
-                              register_address,
-                              register_value,
-                              signed=True):
+                              slave_addr: int,
+                              register_address: int,
+                              register_value: int,
+                              signed=True) -> bool:
         modbus_pdu = functions.write_single_register(register_address,
                                                      register_value,
                                                      signed)
 
-        resp_data = self._send_receive(modbus_pdu, slave_addr, False)
-        operation_status = functions.validate_resp_data(resp_data,
-                                                        Const.WRITE_SINGLE_REGISTER,
-                                                        register_address,
-                                                        value=register_value,
-                                                        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_SINGLE_REGISTER,
+            address=register_address,
+            value=register_value,
+            signed=signed)
 
         return operation_status
 
     def write_multiple_coils(self,
-                             slave_addr,
-                             starting_address,
-                             output_values):
-        modbus_pdu = functions.write_multiple_coils(starting_address,
-                                                    output_values)
-
-        resp_data = self._send_receive(modbus_pdu, slave_addr, False)
-        operation_status = functions.validate_resp_data(resp_data,
-                                                        Const.WRITE_MULTIPLE_COILS,
-                                                        starting_address,
-                                                        quantity=len(output_values))
+                             slave_addr: int,
+                             starting_address: int,
+                             output_values: List[int, bool]) -> 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,
-                                 starting_address,
-                                 register_values,
-                                 signed=True):
-        modbus_pdu = functions.write_multiple_registers(starting_address,
-                                                        register_values,
-                                                        signed)
-
-        resp_data = self._send_receive(modbus_pdu, slave_addr, False)
-        operation_status = functions.validate_resp_data(resp_data,
-                                                        Const.WRITE_MULTIPLE_REGISTERS,
-                                                        starting_address,
-                                                        quantity=len(register_values))
+                                 slave_addr: int,
+                                 starting_address: int,
+                                 register_values: List[int],
+                                 signed=True) -> 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)
+        )
 
         return operation_status
 
     def send_response(self,
-                      slave_addr,
-                      function_code,
-                      request_register_addr,
-                      request_register_qty,
-                      request_data,
-                      values=None,
-                      signed=True):
-        modbus_pdu = functions.response(function_code,
-                                        request_register_addr,
-                                        request_register_qty,
-                                        request_data,
-                                        values,
-                                        signed)
-        self._send(modbus_pdu, slave_addr)
+                      slave_addr: int,
+                      function_code: int,
+                      request_register_addr: int,
+                      request_register_qty: int,
+                      request_data: list,
+                      values: Optional[list] = None,
+                      signed: bool = True) -> None:
+        modbus_pdu = functions.response(
+            function_code=function_code,
+            request_register_addr=request_register_addr,
+            request_register_qty=request_register_qty,
+            request_data=request_data,
+            value_list=values,
+            signed=signed
+        )
+        self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr)
 
     def send_exception_response(self,
-                                slave_addr,
-                                function_code,
-                                exception_code):
-        modbus_pdu = functions.exception_response(function_code,
-                                                  exception_code)
-        self._send(modbus_pdu, slave_addr)
+                                slave_addr: int,
+                                function_code: int,
+                                exception_code: int) -> None:
+        modbus_pdu = functions.exception_response(
+            function_code=function_code,
+            exception_code=exception_code)
+        self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr)
 
-    def get_request(self, unit_addr_list, timeout=None):
-        req = self._uart_read_frame(timeout)
+    def get_request(self, unit_addr_list: list, timeout: Optional[int] = None):
+        req = self._uart_read_frame(timeout=timeout)
 
         if len(req) < 8:
             return None
@@ -337,11 +465,12 @@ def get_request(self, unit_addr_list, timeout=None):
             return None
 
         try:
-            request = Request(self, req_no_crc)
+            request = Request(interface=self, data=req_no_crc)
         except ModbusException as e:
-            self.send_exception_response(req[0],
-                                         e.function_code,
-                                         e.exception_code)
+            self.send_exception_response(
+                slave_addr=req[0],
+                function_code=e.function_code,
+                exception_code=e.exception_code)
             return None
 
         return request
diff --git a/umodbus/tcp.py b/umodbus/tcp.py
index fd9235d..3979b95 100644
--- a/umodbus/tcp.py
+++ b/umodbus/tcp.py
@@ -9,7 +9,7 @@
 #
 
 # system packages
-import random
+# import random
 import struct
 import socket
 import time
@@ -19,11 +19,178 @@
 from . import const as Const
 from .common import Request
 from .common import ModbusException
+from .modbus import Modbus
+from urequests import request
+
+# typing not natively supported on MicroPython
+from .typing import List, Optional, Tuple, Union
+
+
+class ModbusTCP(Modbus):
+    def __init__(self):
+        super().__init__(
+            # set itf to TCPServer object, addr_list to None
+            TCPServer(),
+            None
+        )
+
+    def bind(self,
+             local_ip: str,
+             local_port: int = 502,
+             max_connections: int = 10) -> None:
+        self._itf.bind(local_ip, local_port, max_connections)
+
+    def get_bound_status(self) -> bool:
+        try:
+            return self._itf.get_is_bound()
+        except Exception:
+            return False
+
+    def process(self) -> bool:
+        """
+        Process the modbus requests.
+
+        :returns:   Result of processing, True on success, False otherwise
+        :rtype:     bool
+        """
+        reg_type = None
+        req_type = None
+
+        request = self._itf.get_request(unit_addr_list=self._addr_list,
+                                        timeout=0)
+        if request is None:
+            return False
+
+        if request.function == Const.READ_COILS:
+            # Coils (setter+getter) [0, 1]
+            # function 01 - read single register
+            reg_type = 'COILS'
+            req_type = 'READ'
+        elif request.function == Const.READ_DISCRETE_INPUTS:
+            # Ists (only getter) [0, 1]
+            # function 02 - read input status (discrete inputs/digital input)
+            reg_type = 'ISTS'
+            req_type = 'READ'
+        elif request.function == Const.READ_HOLDING_REGISTERS:
+            # Hregs (setter+getter) [0, 65535]
+            # function 03 - read holding register
+            reg_type = 'HREGS'
+            req_type = 'READ'
+        elif request.function == Const.READ_INPUT_REGISTER:
+            # Iregs (only getter) [0, 65535]
+            # function 04 - read input registers
+            reg_type = 'IREGS'
+            req_type = 'READ'
+        elif request.function == Const.WRITE_SINGLE_COIL:
+            # Coils (setter+getter) [0, 1]
+            # function 05 - write single register
+            reg_type = 'COILS'
+            req_type = 'WRITE'
+        elif request.function == Const.WRITE_SINGLE_REGISTER:
+            # Hregs (setter+getter) [0, 65535]
+            # function 06 - write holding register
+            reg_type = 'HREGS'
+            req_type = 'WRITE'
+        else:
+            request.send_exception(Const.ILLEGAL_FUNCTION)
+
+        if reg_type:
+            if req_type == 'READ':
+                self._process_read_access(request=request, reg_type=reg_type)
+            elif req_type == 'WRITE':
+                self._process_write_access(request=request, reg_type=reg_type)
+
+        return True
+
+    def _create_response(self, request: request, reg_type: str):
+        """
+        Create a response.
+
+        :param      request:   The request
+        :type       request:   request
+        :param      reg_type:  The register type
+        :type       reg_type:  str
+
+        :returns:   Values of this register
+        :rtype:     Union[bool, int, List[int], List[bool]]
+        """
+        if type(self._register_dict[reg_type][request.register_addr]) is list:
+            return self._register_dict[reg_type][request.register_addr]
+        else:
+            return [self._register_dict[reg_type][request.register_addr]]
+
+    def _process_read_access(self, request: request, reg_type: str) -> None:
+        """
+        Process read access to register
+
+        :param      request:   The request
+        :type       request:   request
+        :param      reg_type:  The register type
+        :type       reg_type:  str
+        """
+        if request.register_addr in self._register_dict[reg_type]:
+            vals = self._create_response(request=request, reg_type=reg_type)
+            request.send_response(vals)
+        else:
+            request.send_exception(Const.ILLEGAL_DATA_ADDRESS)
+
+    def _process_write_access(self, request: request, reg_type: str) -> None:
+        """
+        Process write access to register
+
+        :param      request:   The request
+        :type       request:   request
+        :param      reg_type:  The register type
+        :type       reg_type:  str
+        """
+        address = request.register_addr
+        val = 0
+        valid_register = False
+
+        if address in self._register_dict[reg_type]:
+            if reg_type == 'COILS':
+                val = request.data[0]
+                if val == 0x00:
+                    val = False
+                    valid_register = True
+
+                    request.send_response()
+
+                    self.set_coil(address=address, value=val)
+                elif val == 0xFF:
+                    val = True
+                    valid_register = True
+
+                    request.send_response()
+
+                    self.set_coil(address=address, value=val)
+                else:
+                    request.send_exception(Const.ILLEGAL_DATA_VALUE)
+            elif reg_type == 'HREGS':
+                valid_register = True
+                val = request.data_as_registers(signed=False)[0]
+
+                request.send_response()
+
+                self.set_hreg(address=address, value=val)
+            else:
+                pass
+
+            if valid_register:
+                self._set_changed_register(reg_type=reg_type,
+                                           address=address,
+                                           value=val)
+        else:
+            request.send_exception(Const.ILLEGAL_DATA_ADDRESS)
 
 
 class TCP(object):
-    def __init__(self, slave_ip, slave_port=502, timeout=5):
+    def __init__(self,
+                 slave_ip: str,
+                 slave_port: int = 502,
+                 timeout: float = 5.0):
         self._sock = socket.socket()
+        self.trans_id_ctr = 0
 
         # print(socket.getaddrinfo(slave_ip, slave_port))
         # [(2, 1, 0, '192.168.178.47', ('192.168.178.47', 502))]
@@ -31,167 +198,340 @@ def __init__(self, slave_ip, slave_port=502, timeout=5):
 
         self._sock.settimeout(timeout)
 
-    def _create_mbap_hdr(self, slave_id, modbus_pdu):
+    def _create_mbap_hdr(self,
+                         slave_id: int,
+                         modbus_pdu: bytes) -> Tuple[bytes, int]:
+        """
+        Create a Modbus header.
+
+        :param      slave_id:    The slave identifier
+        :type       slave_id:    int
+        :param      modbus_pdu:  The modbus Protocol Data Unit
+        :type       modbus_pdu:  bytes
+
+        :returns:   Modbus header and unique transaction ID
+        :rtype:     Tuple[bytes, int]
+        """
         # only available on WiPy
         # trans_id = machine.rng() & 0xFFFF
         # use builtin function to generate random 24 bit integer
-        trans_id = random.getrandbits(24) & 0xFFFF
+        # trans_id = random.getrandbits(24) & 0xFFFF
+        # use incrementing counter as it's faster
+        trans_id = self.trans_id_ctr
+        self.trans_id_ctr += 1
 
-        mbap_hdr = struct.pack('>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_id)
+        mbap_hdr = struct.pack(
+            '>HHHB', trans_id, 0, len(modbus_pdu) + 1, slave_id)
 
         return mbap_hdr, trans_id
 
-    def _bytes_to_bool(self, byte_list):
-        bool_list = []
-        for index, byte in enumerate(byte_list):
-            bool_list.extend([bool(byte & (1 << n)) for n in range(8)])
-
-        return bool_list
-
-    def _to_short(self, byte_array, signed=True):
-        response_quantity = int(len(byte_array) / 2)
-        fmt = '>' + (('h' if signed else 'H') * response_quantity)
-
-        return struct.unpack(fmt, byte_array)
-
     def _validate_resp_hdr(self,
-                           response,
-                           trans_id,
-                           slave_id,
-                           function_code,
-                           count=False):
-        rec_tid, rec_pid, rec_len, rec_uid, rec_fc = struct.unpack('>HHHBB', response[:Const.MBAP_HDR_LENGTH + 1])
+                           response: bytearray,
+                           trans_id: int,
+                           slave_id: int,
+                           function_code: int,
+                           count: bool = False) -> bytes:
+        """
+        Validate the response header.
+
+        :param      response:       The response
+        :type       response:       bytearray
+        :param      trans_id:       The transaction identifier
+        :type       trans_id:       int
+        :param      slave_id:       The slave identifier
+        :type       slave_id:       int
+        :param      function_code:  The function code
+        :type       function_code:  int
+        :param      count:          The count
+        :type       count:          bool
+
+        :returns:   Modbus response content
+        :rtype:     bytes
+        """
+        rec_tid, rec_pid, rec_len, rec_uid, rec_fc = struct.unpack(
+            '>HHHBB', response[:Const.MBAP_HDR_LENGTH + 1])
+
         if (trans_id != rec_tid):
-            raise ValueError('wrong transaction Id')
+            raise ValueError('wrong transaction ID')
 
         if (rec_pid != 0):
-            raise ValueError('invalid protocol Id')
+            raise ValueError('invalid protocol ID')
 
         if (slave_id != rec_uid):
-            raise ValueError('wrong slave Id')
+            raise ValueError('wrong slave ID')
 
         if (rec_fc == (function_code + Const.ERROR_BIAS)):
             raise ValueError('slave returned exception code: {:d}'.
                              format(rec_fc))
 
-        hdr_length = (Const.MBAP_HDR_LENGTH + 2) if count else (Const.MBAP_HDR_LENGTH + 1)
+        hdr_length = (Const.MBAP_HDR_LENGTH + 2) if count else \
+            (Const.MBAP_HDR_LENGTH + 1)
 
         return response[hdr_length:]
 
-    def _send_receive(self, slave_id, modbus_pdu, count):
-        mbap_hdr, trans_id = self._create_mbap_hdr(slave_id, modbus_pdu)
+    def _send_receive(self,
+                      slave_id: 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      modbus_pdu:  The modbus PDU
+        :type       modbus_pdu:  bytes
+        :param      count:       The count
+        :type       count:       bool
+
+        :returns:   Modbus data
+        :rtype:     bytes
+        """
+        mbap_hdr, trans_id = self._create_mbap_hdr(slave_id=slave_id,
+                                                   modbus_pdu=modbus_pdu)
         self._sock.send(mbap_hdr + modbus_pdu)
 
         response = self._sock.recv(256)
-        modbus_data = self._validate_resp_hdr(response,
-                                              trans_id,
-                                              slave_id,
-                                              modbus_pdu[0],
-                                              count)
+        modbus_data = self._validate_resp_hdr(response=response,
+                                              trans_id=trans_id,
+                                              slave_id=slave_id,
+                                              function_code=modbus_pdu[0],
+                                              count=count)
 
         return modbus_data
 
-    def read_coils(self, slave_addr, starting_addr, coil_qty):
-        modbus_pdu = functions.read_coils(starting_addr, coil_qty)
-
-        response = self._send_receive(slave_addr, modbus_pdu, True)
-        status_pdu = self._bytes_to_bool(response)
+    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, starting_addr, input_qty):
-        modbus_pdu = functions.read_discrete_inputs(starting_addr, input_qty)
-
-        response = self._send_receive(slave_addr, modbus_pdu, True)
-        status_pdu = self._bytes_to_bool(response)
+    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,
-                               starting_addr,
-                               register_qty,
-                               signed=True):
-        modbus_pdu = functions.read_holding_registers(starting_addr,
-                                                      register_qty)
-
-        response = self._send_receive(slave_addr, modbus_pdu, True)
-        register_value = self._to_short(response, signed)
+                               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,
-                             starting_addr,
-                             register_qty,
-                             signed=True):
-        modbus_pdu = functions.read_input_registers(starting_addr,
-                                                    register_qty)
-
-        response = self._send_receive(slave_addr, modbus_pdu, True)
-        register_value = self._to_short(response, signed)
+                             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, output_address, output_value):
-        modbus_pdu = functions.write_single_coil(output_address, output_value)
-
-        response = self._send_receive(slave_addr, modbus_pdu, False)
-        operation_status = functions.validate_resp_data(response,
-                                                        Const.WRITE_SINGLE_COIL,
-                                                        output_address,
-                                                        value=output_value,
-                                                        signed=False)
+    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,
-                              register_address,
-                              register_value,
-                              signed=True):
-        modbus_pdu = functions.write_single_register(register_address,
-                                                     register_value,
-                                                     signed)
-
-        response = self._send_receive(slave_addr, modbus_pdu, False)
-        operation_status = functions.validate_resp_data(response,
-                                                        Const.WRITE_SINGLE_REGISTER,
-                                                        register_address,
-                                                        value=register_value,
-                                                        signed=signed)
+                              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,
-                             starting_address,
-                             output_values):
-        modbus_pdu = functions.write_multiple_coils(starting_address,
-                                                    output_values)
-
-        response = self._send_receive(slave_addr, modbus_pdu, False)
-        operation_status = functions.validate_resp_data(response,
-                                                        Const.WRITE_MULTIPLE_COILS,
-                                                        starting_address,
-                                                        quantity=len(output_values))
+                             slave_addr: int,
+                             starting_address: int,
+                             output_values: List[int, bool]) -> 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,
-                                 starting_address,
-                                 register_values,
-                                 signed=True):
-        modbus_pdu = functions.write_multiple_registers(starting_address,
-                                                        register_values,
-                                                        signed)
-
-        response = self._send_receive(slave_addr, modbus_pdu, False)
-        operation_status = functions.validate_resp_data(response,
-                                                        Const.WRITE_MULTIPLE_REGISTERS,
-                                                        starting_address,
-                                                        quantity=len(register_values))
+                                 slave_addr: int,
+                                 starting_address: int,
+                                 register_values: List[int],
+                                 signed=True) -> 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),
+            # this fixes
+            # https://github.com/brainelectronics/micropython-modbus/issues/23
+            # signed=signed
+        )
 
         return operation_status
 
@@ -202,10 +542,13 @@ def __init__(self):
         self._client_sock = None
         self._is_bound = False
 
-    def get_is_bound(self):
+    def get_is_bound(self) -> bool:
         return self._is_bound
 
-    def bind(self, local_ip, local_port=502, max_connections=10):
+    def bind(self,
+             local_ip: str,
+             local_port: int = 502,
+             max_connections: int = 10):
         if self._client_sock:
             self._client_sock.close()
 
@@ -222,20 +565,20 @@ def bind(self, local_ip, local_port=502, max_connections=10):
 
         self._is_bound = True
 
-    def _send(self, modbus_pdu, slave_addr):
+    def _send(self, modbus_pdu: bytes, slave_addr: int) -> None:
         size = len(modbus_pdu)
         fmt = 'B' * size
         adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu)
         self._client_sock.send(adu)
 
     def send_response(self,
-                      slave_addr,
-                      function_code,
-                      request_register_addr,
-                      request_register_qty,
-                      request_data,
-                      values=None,
-                      signed=True):
+                      slave_addr: int,
+                      function_code: int,
+                      request_register_addr: int,
+                      request_register_qty: int,
+                      request_data: list,
+                      values: Optional[list] = None,
+                      signed: bool = True) -> None:
         modbus_pdu = functions.response(function_code,
                                         request_register_addr,
                                         request_register_qty,
@@ -245,14 +588,16 @@ def send_response(self,
         self._send(modbus_pdu, slave_addr)
 
     def send_exception_response(self,
-                                slave_addr,
-                                function_code,
-                                exception_code):
+                                slave_addr: int,
+                                function_code: int,
+                                exception_code: int) -> None:
         modbus_pdu = functions.exception_response(function_code,
                                                   exception_code)
         self._send(modbus_pdu, slave_addr)
 
-    def _accept_request(self, accept_timeout, unit_addr_list):
+    def _accept_request(self,
+                        accept_timeout: float,
+                        unit_addr_list: list) -> None:
         self._sock.settimeout(accept_timeout)
         new_client_sock = None
 
@@ -310,7 +655,7 @@ def _accept_request(self, accept_timeout, unit_addr_list):
                                              e.exception_code)
                 return None
 
-    def get_request(self, unit_addr_list=None, timeout=None):
+    def get_request(self, unit_addr_list=None, timeout=None) -> None:
         if self._sock is None:
             raise Exception('Modbus TCP server not bound')
 
diff --git a/umodbus/typing.py b/umodbus/typing.py
index 9719929..7b14274 100644
--- a/umodbus/typing.py
+++ b/umodbus/typing.py
@@ -10,6 +10,14 @@
 """
 
 
+class _Subscriptable():
+    def __getitem__(self, item):
+        return None
+
+
+_subscriptable = _Subscriptable()
+
+
 class Any:
     pass
 
@@ -22,12 +30,10 @@ class ClassVar:
     pass
 
 
-class Union:
-    pass
+Union = _subscriptable
 
 
-class Optional:
-    pass
+Optional = _subscriptable
 
 
 class Generic:
@@ -114,12 +120,10 @@ class ByteString:
     pass
 
 
-class Tuple:
-    pass
+Tuple = _subscriptable
 
 
-class List:
-    pass
+List = _subscriptable
 
 
 class Deque:
@@ -162,8 +166,7 @@ class AsyncContextManager:
     pass
 
 
-class Dict:
-    pass
+Dict = _subscriptable
 
 
 class DefaultDict:


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fe760a9..e07ac30 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,6 +33,15 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi
+    - name: Execute tests
+      run: |
+        docker build --tag micropython-test --file Dockerfile.tests .
+    - name: Run Client/Host TCP example
+      run: |
+        docker compose up --build --exit-code-from micropython-host
+    - name: Run Client/Host TCP test
+      run: |
+        docker compose -f docker-compose-tcp-test.yaml up --build --exit-code-from micropython-host
     - name: Build package
       run: |
         changelog2version \
diff --git a/Dockerfile.test_tcp_example b/Dockerfile.test_tcp_example
new file mode 100644
index 0000000..e37b139
--- /dev/null
+++ b/Dockerfile.test_tcp_example
@@ -0,0 +1,16 @@
+# Build image
+# $ docker build -t micropython-tcp-example -f Dockerfile.test_tcp_example .
+# if a command fails, it will exit with non-zero code
+#
+# Run image, only possible if all tests passed
+# $ docker run -it --rm --name micropython-tcp-example micropython-tcp-example
+
+FROM micropython/unix:v1.18
+
+# use "volumes" in docker-compose file to remove need of rebuilding
+# COPY ./ /home
+# COPY umodbus /root/.micropython/lib/umodbus
+# COPY mpy_unittest.py /root/.micropython/lib/mpy_unittest.py
+
+RUN micropython-dev -m upip install micropython-ulogging
+RUN micropython-dev -m upip install micropython-urequests
diff --git a/Dockerfile.tests b/Dockerfile.tests
new file mode 100644
index 0000000..c4d37c2
--- /dev/null
+++ b/Dockerfile.tests
@@ -0,0 +1,20 @@
+# Build image
+# $ docker build -t micropython-test -f Dockerfile.tests .
+# if a unittest fails, it will exit with non-zero code
+#
+# Run image, only possible if all tests passed
+# $ docker run -it --rm --name micropython-test micropython-test
+
+FROM micropython/unix:v1.18
+
+COPY ./ /home
+# keep examples and tests registers JSON file easily in sync
+COPY registers/example.json /home/tests/test-registers.json
+COPY umodbus /root/.micropython/lib/umodbus
+COPY mpy_unittest.py /root/.micropython/lib/mpy_unittest.py
+
+RUN micropython-dev -m upip install micropython-ulogging
+RUN micropython-dev -m upip install micropython-urequests
+RUN micropython-dev -c "import mpy_unittest as unittest; unittest.main('tests')"
+
+ENTRYPOINT ["/bin/bash"]
diff --git a/docker-compose-tcp-test.yaml b/docker-compose-tcp-test.yaml
new file mode 100644
index 0000000..25589dd
--- /dev/null
+++ b/docker-compose-tcp-test.yaml
@@ -0,0 +1,62 @@
+#
+# build all non-image containers
+# $ docker-compose -f docker-compose-tcp-test.yaml build
+# can be combined into one command to also start it afterwards
+# $ docker-compose -f docker-compose-tcp-test.yaml up --build
+#
+
+version: "3.8"
+
+services:
+  micropython-client:
+    build:
+      context: .
+      dockerfile: Dockerfile.client
+    container_name: micropython-client
+    volumes:
+      - ./:/home
+      - ./registers:/home/registers
+      - ./umodbus:/root/.micropython/lib/umodbus
+    expose:
+      - "502"
+    ports:
+      - "502:502" # reach "micropython-client" at 172.24.0.2:502, see networks
+    networks:
+      my_bridge:
+        # fix IPv4 address to be known and in the MicroPython scripts
+        # https://docs.docker.com/compose/compose-file/#ipv4_address
+        ipv4_address: 172.24.0.2
+
+  micropython-host:
+    build:
+      context: .
+      dockerfile: Dockerfile.test_tcp_example
+    container_name: micropython-host
+    volumes:
+      - ./:/home
+      - ./umodbus:/root/.micropython/lib/umodbus
+      - ./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_tcp_example', fromlist=['TestTcpExample'])"
+    networks:
+      my_bridge:
+        # fix IPv4 address to be known and in the MicroPython scripts
+        # https://docs.docker.com/compose/compose-file/#ipv4_address
+        ipv4_address: 172.24.0.3
+
+networks:
+  my_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.24.0.0/16
+          gateway: 172.24.0.1
diff --git a/mpy_unittest.py b/mpy_unittest.py
new file mode 100644
index 0000000..1472d9f
--- /dev/null
+++ b/mpy_unittest.py
@@ -0,0 +1,433 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+
+# Taken from pfalcon's pycopy-lib, see
+# https://github.com/pfalcon/pycopy-lib/blob/56ebf2110f3caa63a3785d439ce49b11e13c75c0/unittest/unittest.py
+#
+# Copyright (c) 2014-2021 Paul Sokolovsky
+# Copyright (c) 2014-2020 pycopy-lib contributors
+
+# Copyright (c) 2022 brainelectronics
+# Added:
+# - New properties in TestResult class
+#   - errors
+#   - failures
+#   - skipped
+#   - testsRun
+# - All tests or a specific TestCase can be executed
+# - sys exit status can be enabled (default) or disabled
+# - assertNotIn, assertNotIsInstance, assertLess, assertGreater
+# - Shebang header
+#
+# Fixed:
+# - All flake8 warnings
+#
+
+import sys
+try:
+    import io
+    import traceback
+except ImportError:
+    import uio as io
+    traceback = None
+
+
+class SkipTest(Exception):
+    pass
+
+
+class AssertRaisesContext:
+
+    def __init__(self, exc):
+        self.expected = exc
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, tb):
+        self.exception = exc_value
+        if exc_type is None:
+            assert False, "%r not raised" % self.expected
+        if issubclass(exc_type, self.expected):
+            return True
+        return False
+
+
+class NullContext:
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, a, b, c):
+        pass
+
+
+class TestCase:
+    """
+    This class describes a test case.
+
+    https://docs.python.org/3/library/unittest.html
+    """
+    def __init__(self):
+        pass
+
+    def addCleanup(self, func, *args, **kwargs):
+        if not hasattr(self, "_cleanups"):
+            self._cleanups = []
+        self._cleanups.append((func, args, kwargs))
+
+    def doCleanups(self):
+        if hasattr(self, "_cleanups"):
+            while self._cleanups:
+                func, args, kwargs = self._cleanups.pop()
+                func(*args, **kwargs)
+
+    def subTest(self, msg=None, **params):
+        return NullContext()
+
+    def skipTest(self, reason):
+        raise SkipTest(reason)
+
+    def fail(self, msg=''):
+        assert False, msg
+
+    def assertEqual(self, x, y, msg=''):
+        if not msg:
+            msg = "%r vs (expected) %r" % (x, y)
+        assert x == y, msg
+
+    def assertNotEqual(self, x, y, msg=''):
+        if not msg:
+            msg = "%r not expected to be equal %r" % (x, y)
+        assert x != y, msg
+
+    def assertLess(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be < %r" % (x, y)
+        assert x < y, msg
+
+    def assertLessEqual(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be <= %r" % (x, y)
+        assert x <= y, msg
+
+    def assertGreater(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be > %r" % (x, y)
+        assert x > y, msg
+
+    def assertGreaterEqual(self, x, y, msg=None):
+        if msg is None:
+            msg = "%r is expected to be >= %r" % (x, y)
+        assert x >= y, msg
+
+    def assertAlmostEqual(self, x, y, places=None, msg='', delta=None):
+        if x == y:
+            return
+        if delta is not None and places is not None:
+            raise TypeError("specify delta or places not both")
+
+        if delta is not None:
+            if abs(x - y) <= delta:
+                return
+            if not msg:
+                msg = '%r != %r within %r delta' % (x, y, delta)
+        else:
+            if places is None:
+                places = 7
+            if round(abs(y - x), places) == 0:
+                return
+            if not msg:
+                msg = '%r != %r within %r places' % (x, y, places)
+
+        assert False, msg
+
+    def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None):
+        if delta is not None and places is not None:
+            raise TypeError("specify delta or places not both")
+
+        if delta is not None:
+            if not (x == y) and abs(x - y) > delta:
+                return
+            if not msg:
+                msg = '%r == %r within %r delta' % (x, y, delta)
+        else:
+            if places is None:
+                places = 7
+            if not (x == y) and round(abs(y - x), places) != 0:
+                return
+            if not msg:
+                msg = '%r == %r within %r places' % (x, y, places)
+
+        assert False, msg
+
+    def assertIs(self, x, y, msg=''):
+        if not msg:
+            msg = "%r is not %r" % (x, y)
+        assert x is y, msg
+
+    def assertIsNot(self, x, y, msg=''):
+        if not msg:
+            msg = "%r is %r" % (x, y)
+        assert x is not y, msg
+
+    def assertIsNone(self, x, msg=''):
+        if not msg:
+            msg = "%r is not None" % x
+        assert x is None, msg
+
+    def assertIsNotNone(self, x, msg=''):
+        if not msg:
+            msg = "%r is None" % x
+        assert x is not None, msg
+
+    def assertTrue(self, x, msg=''):
+        if not msg:
+            msg = "Expected %r to be True" % x
+        assert x, msg
+
+    def assertFalse(self, x, msg=''):
+        if not msg:
+            msg = "Expected %r to be False" % x
+        assert not x, msg
+
+    def assertIn(self, x, y, msg=''):
+        if not msg:
+            msg = "Expected %r to be in %r" % (x, y)
+        assert x in y, msg
+
+    def assertNotIn(self, x, y, msg=''):
+        if not msg:
+            msg = "Expected %r to be in %r" % (x, y)
+        assert x not in y, msg
+
+    def assertIsInstance(self, x, y, msg=''):
+        assert isinstance(x, y), msg
+
+    def assertNotIsInstance(self, x, y, msg=''):
+        assert not isinstance(x, y), msg
+
+    def assertRaises(self, exc, func=None, *args, **kwargs):
+        if func is None:
+            return AssertRaisesContext(exc)
+
+        try:
+            func(*args, **kwargs)
+        except Exception as e:
+            if isinstance(e, exc):
+                return
+            raise
+
+        assert False, "%r not raised" % exc
+
+    def assertWarns(self, warn):
+        return NullContext()
+
+
+def skip(msg):
+    def _decor(fun):
+        # We just replace original fun with _inner
+        def _inner(self):
+            raise SkipTest(msg)
+        return _inner
+    return _decor
+
+
+def skipIf(cond, msg):
+    if not cond:
+        return lambda x: x
+    return skip(msg)
+
+
+def skipUnless(cond, msg):
+    if cond:
+        return lambda x: x
+    return skip(msg)
+
+
+def expectedFailure(test):
+
+    def test_exp_fail(*args, **kwargs):
+        try:
+            test(*args, **kwargs)
+        except:     # noqa: E722
+            pass
+        else:
+            assert False, "unexpected success"
+
+    return test_exp_fail
+
+
+class TestSuite:
+    def __init__(self):
+        self._tests = []
+
+    def addTest(self, cls):
+        self._tests.append(cls)
+
+    def run(self, result):
+        for c in self._tests:
+            run_suite(c, result)
+        return result
+
+
+class TestRunner:
+    def run(self, suite):
+        res = TestResult()
+        suite.run(res)
+
+        res.printErrors()
+        print("----------------------------------------------------------------------")     # noqa: E501
+        print("Ran {} tests\n".format(res.testsRun))
+        if res.failuresNum > 0 or res.errorsNum > 0:
+            print("FAILED (failures={}, errors={})".format(res.failuresNum,
+                                                           res.errorsNum))
+        else:
+            msg = "OK"
+            if res.skippedNum > 0:
+                msg += " (skipped={})".format(res.skippedNum)
+            print(msg)
+
+        return res
+
+
+TextTestRunner = TestRunner
+
+
+class TestResult:
+    def __init__(self):
+        self.errorsNum = 0
+        self.failuresNum = 0
+        self.skippedNum = 0
+        self._testsRun = 0
+        self._errors = []
+        self._failures = []
+        self._skipped = []
+
+    @property
+    def errors(self):
+        return self._errors
+
+    @property
+    def failures(self):
+        return self._failures
+
+    @property
+    def skipped(self):
+        return self._skipped
+
+    @property
+    def testsRun(self):
+        return self._testsRun
+
+    def wasSuccessful(self):
+        return self.errorsNum == 0 and self.failuresNum == 0
+
+    def printErrors(self):
+        # print()
+        self.printErrorList(self.errors)
+        self.printErrorList(self.failures)
+
+    def printErrorList(self, lst):
+        sep = "----------------------------------------------------------------------"  # noqa: E501
+        for c, e in lst:
+            print("======================================================================")     # noqa: E501
+            print(c)
+            print(sep)
+            print(e)
+
+    def __repr__(self):
+        # Format is compatible with CPython.
+        return ("<unittest.result.TestResult run={} errors={} failures={}>".
+                format(self._testsRun, self.errorsNum, self.failuresNum))
+
+
+def capture_exc(e):
+    buf = io.StringIO()
+    if hasattr(sys, "print_exception"):
+        sys.print_exception(e, buf)
+    elif traceback is not None:
+        traceback.print_exception(None, e, sys.exc_info()[2], file=buf)
+    return buf.getvalue()
+
+
+# TODO: Uncompliant
+def run_suite(c, test_result):
+    if isinstance(c, TestSuite):
+        c.run(test_result)
+        return
+
+    if isinstance(c, type):
+        o = c()
+    else:
+        o = c
+    set_up = getattr(o, "setUp", lambda: None)
+    tear_down = getattr(o, "tearDown", lambda: None)
+    exceptions = []
+
+    def run_one(m):
+        print("{} ({}) ...".format(name, c.__qualname__), end="")
+        set_up()
+        try:
+            test_result._testsRun += 1
+            m()
+            print(" ok")
+        except SkipTest as e:
+            print(" skipped:", e.args[0])
+            test_result.skippedNum += 1
+            test_result._skipped.append((name, e.args[0]))
+        except Exception as ex:
+            ex_str = capture_exc(ex)
+            if isinstance(ex, AssertionError):
+                test_result.failuresNum += 1
+                test_result._failures.append(((name, c), ex_str))
+                print(" FAIL")
+            else:
+                test_result.errorsNum += 1
+                test_result._errors.append(((name, c), ex_str))
+                print(" ERROR")
+            # Uncomment to investigate failure in detail
+            # raise
+        finally:
+            tear_down()
+            o.doCleanups()
+
+    if hasattr(o, "runTest"):
+        name = str(o)
+        run_one(o.runTest)
+        return
+
+    for name in dir(o):
+        if name.startswith("test"):
+            m = getattr(o, name)
+            if not callable(m):
+                continue
+            run_one(m)
+    return exceptions
+
+
+def test_cases(m):
+    for tn in dir(m):
+        c = getattr(m, tn)
+        if (isinstance(c, object) and
+            isinstance(c, type) and
+                issubclass(c, TestCase)):
+            yield c
+
+
+def main(name="__main__", fromlist: bool = list(), do_exit: bool = True):
+    # Import the complete module of only a subset, see
+    # https://docs.python.org/3/library/functions.html#__import__
+    if len(fromlist):
+        m = __import__(name, globals(), locals(), fromlist)
+    else:
+        m = __import__(name) if isinstance(name, str) else name
+    suite = TestSuite()
+    for c in test_cases(m):
+        suite.addTest(c)
+    runner = TestRunner()
+    result = runner.run(suite)
+
+    if do_exit:
+        # Terminate with non zero return code in case of failures
+        sys.exit(result.failuresNum or result.errorsNum)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..efb9b9b
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+
+from .test_absolute_truth import *
+from .test_const import *
+from .test_functions import *
+
+# TestTcpExample is a non static test and requires a running TCP client
+# from .test_tcp_example import *
diff --git a/tests/test_absolute_truth.py b/tests/test_absolute_truth.py
new file mode 100644
index 0000000..dc38082
--- /dev/null
+++ b/tests/test_absolute_truth.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""Unittest for testing the absolute truth"""
+
+# import sys
+import ulogging as logging
+import mpy_unittest as unittest
+
+
+class TestAbsoluteTruth(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
+
+    def test_absolute_truth(self) -> None:
+        """Test the unittest itself"""
+        x = 0
+        y = 1
+        z = 2
+        none_thing = None
+        some_dict = dict()
+        some_list = [x, y, 40, "asdf", z]
+
+        self.assertTrue(True)
+        self.assertFalse(False)
+
+        self.assertEqual(y, 1)
+        assert y == 1
+        with self.assertRaises(AssertionError):
+            self.assertEqual(x, y)
+
+        self.assertNotEqual(x, y)
+        assert x != y
+        with self.assertRaises(AssertionError):
+            self.assertNotEqual(x, x)
+
+        self.assertIs(some_list, some_list)
+        self.assertIsNot(some_list, some_dict)
+
+        self.assertIsNone(none_thing)
+        self.assertIsNotNone(some_dict)
+
+        self.assertIn(y, some_list)
+        self.assertNotIn(12, some_list)
+
+        # self.assertRaises(exc, fun, args, *kwds)
+        with self.assertRaises(ZeroDivisionError):
+            1 / 0
+
+        self.assertIsInstance(some_dict, dict)
+        self.assertNotIsInstance(some_list, dict)
+
+        self.assertGreater(y, x)
+        self.assertGreaterEqual(y, x)
+        self.assertLess(x, y)
+        self.assertLessEqual(x, y)
+
+        self.test_logger.warning('Dummy logger warning')
+
+    def testAssert(self):
+        e1 = None
+        try:
+            def func_under_test(a):
+                assert a > 10
+
+            self.assertRaises(AssertionError, func_under_test, 20)
+        except AssertionError as e:
+            e1 = e
+
+        if not e1 or "not raised" not in e1.args[0]:
+            self.fail("Expected to catch lack of AssertionError from assert \
+                in func_under_test")
+
+    @unittest.skip('Reasoning for skipping this test')
+    def testSkip(self):
+        self.fail('this should be skipped')
+
+    def testSkipNoDecorator(self):
+        do_skip = True
+
+        if do_skip:
+            self.skipTest("External resource triggered skipping this test")
+
+        self.fail('this should be skipped')
+
+    @unittest.skipIf('a' in ['a', 'b'], 'Reasoning for skipping another test')
+    def testSkipIf(self):
+        self.fail('this should be skipped')
+
+    @unittest.skipUnless(42 == 24, 'Reasoning for skipping test 42')
+    def testSkipUnless(self):
+        self.fail('this should be skipped')
+
+    @unittest.expectedFailure
+    def testExpectedFailure(self):
+        self.assertEqual(1, 0)
+
+    def testExpectedFailureNot(self):
+        @unittest.expectedFailure
+        def testInner():
+            self.assertEqual(1, 1)
+        try:
+            testInner()
+        except:     # noqa: E722
+            pass
+        else:
+            self.fail("Unexpected success was not detected")
+
+    @unittest.expectedFailure
+    def testSubTest(self):
+        for i in range(0, 6):
+            with self.subTest(i=i):
+                # will fail on 1, 3, 5
+                # but expect the failure by using the expectedFailure decorator
+                self.assertEqual(i % 2, 0)
+
+    def tearDown(self) -> None:
+        """Run after every test method"""
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_const.py b/tests/test_const.py
new file mode 100644
index 0000000..203637a
--- /dev/null
+++ b/tests/test_const.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""Unittest for testing const definitions of umodbus"""
+
+from umodbus.typing import List
+import ulogging as logging
+import mpy_unittest as unittest
+from umodbus import const as Const
+
+
+class TestConst(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
+
+    def test_function_codes(self) -> None:
+        """Test Modbus function codes"""
+        self.assertEqual(Const.READ_COILS, 0x01)
+        self.assertEqual(Const.READ_DISCRETE_INPUTS, 0x02)
+        self.assertEqual(Const.READ_HOLDING_REGISTERS, 0x03)
+        self.assertEqual(Const.READ_INPUT_REGISTER, 0x04)
+
+        self.assertEqual(Const.WRITE_SINGLE_COIL, 0x05)
+        self.assertEqual(Const.WRITE_SINGLE_REGISTER, 0x06)
+        self.assertEqual(Const.WRITE_MULTIPLE_COILS, 0x0F)
+        self.assertEqual(Const.WRITE_MULTIPLE_REGISTERS, 0x10)
+
+        self.assertEqual(Const.MASK_WRITE_REGISTER, 0x16)
+        self.assertEqual(Const.READ_WRITE_MULTIPLE_REGISTERS, 0x17)
+
+        self.assertEqual(Const.READ_FIFO_QUEUE, 0x18)
+
+        self.assertEqual(Const.READ_FILE_RECORD, 0x14)
+        self.assertEqual(Const.WRITE_FILE_RECORD, 0x15)
+
+        self.assertEqual(Const.READ_EXCEPTION_STATUS, 0x07)
+        self.assertEqual(Const.DIAGNOSTICS, 0x08)
+        self.assertEqual(Const.GET_COM_EVENT_COUNTER, 0x0B)
+        self.assertEqual(Const.GET_COM_EVENT_LOG, 0x0C)
+        self.assertEqual(Const.REPORT_SERVER_ID, 0x11)
+        self.assertEqual(Const.READ_DEVICE_IDENTIFICATION, 0x2B)
+
+    def test_exception_codes(self) -> None:
+        """Test Modbus exception codes"""
+        self.assertEqual(Const.ILLEGAL_FUNCTION, 0x01)
+        self.assertEqual(Const.ILLEGAL_DATA_ADDRESS, 0x02)
+        self.assertEqual(Const.ILLEGAL_DATA_VALUE, 0x03)
+        self.assertEqual(Const.SERVER_DEVICE_FAILURE, 0x04)
+        self.assertEqual(Const.ACKNOWLEDGE, 0x05)
+        self.assertEqual(Const.SERVER_DEVICE_BUSY, 0x06)
+        self.assertEqual(Const.MEMORY_PARITY_ERROR, 0x08)
+        self.assertEqual(Const.GATEWAY_PATH_UNAVAILABLE, 0x0A)
+        self.assertEqual(Const.DEVICE_FAILED_TO_RESPOND, 0x0B)
+
+    def test_pdu_constants(self) -> None:
+        """Test Modbus Protocol Data Unit constants"""
+        self.assertEqual(Const.CRC_LENGTH, 0x02)
+        self.assertEqual(Const.ERROR_BIAS, 0x80)
+        self.assertEqual(Const.RESPONSE_HDR_LENGTH, 0x02)
+        self.assertEqual(Const.ERROR_RESP_LEN, 0x05)
+        self.assertEqual(Const.FIXED_RESP_LEN, 0x08)
+        self.assertEqual(Const.MBAP_HDR_LENGTH, 0x07)
+
+    def test_crc16_table(self):
+        """Test CRC16-Modbus table"""
+        def generate_crc16_table() -> List[int, ...]:
+            crc_table = []
+            for byte in range(256):
+                crc = 0x0000
+                for _ in range(8):
+                    if (byte ^ crc) & 0x0001:
+                        crc = (crc >> 1) ^ 0xa001
+                    else:
+                        crc >>= 1
+                    byte >>= 1
+                crc_table.append(crc)
+            return crc_table
+
+        crc16_table = generate_crc16_table()
+        self.assertEqual(len(crc16_table), 256)
+        self.assertEqual(len(Const.CRC16_TABLE), 256)
+        for idx, ele in enumerate(crc16_table):
+            with self.subTest(ele=ele, idx=idx):
+                self.assertEqual(ele, Const.CRC16_TABLE[idx])
+
+    def tearDown(self) -> None:
+        """Run after every test method"""
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_functions.py b/tests/test_functions.py
new file mode 100644
index 0000000..a70d4af
--- /dev/null
+++ b/tests/test_functions.py
@@ -0,0 +1,501 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""Unittest for testing functions of umodbus"""
+
+import ulogging as logging
+import mpy_unittest as unittest
+from umodbus import functions
+from umodbus import const as Const
+
+
+class TestFunctions(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
+
+    def test_read_coils(self) -> None:
+        """Test creation of Modbus Protocol Data Unit for coil reading"""
+        modbus_pdu = functions.read_coils(starting_address=19, quantity=11)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x01\x00\x13\x00\x0b')
+
+        with self.assertRaises(ValueError):
+            functions.read_coils(starting_address=42, quantity=0)
+
+        with self.assertRaises(ValueError):
+            functions.read_coils(starting_address=69, quantity=2001)
+
+    def test_read_discrete_inputs(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for discrete inputs reading
+        """
+        modbus_pdu = functions.read_discrete_inputs(starting_address=196,
+                                                    quantity=22)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x02\x00\xC4\x00\x16')
+
+        with self.assertRaises(ValueError):
+            functions.read_discrete_inputs(starting_address=42, quantity=0)
+
+        with self.assertRaises(ValueError):
+            functions.read_discrete_inputs(starting_address=69, quantity=2001)
+
+    def test_read_holding_registers(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for holding register reading
+        """
+        modbus_pdu = functions.read_holding_registers(starting_address=107,
+                                                      quantity=3)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x03\x00\x6B\x00\x03')
+
+        with self.assertRaises(ValueError):
+            functions.read_holding_registers(starting_address=42, quantity=0)
+
+        with self.assertRaises(ValueError):
+            functions.read_holding_registers(starting_address=69, quantity=126)
+
+    def test_read_input_registers(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for input registers reading
+        """
+        modbus_pdu = functions.read_input_registers(starting_address=8,
+                                                    quantity=1)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x04\x00\x08\x00\x01')
+
+        with self.assertRaises(ValueError):
+            functions.read_input_registers(starting_address=42, quantity=0)
+
+        with self.assertRaises(ValueError):
+            functions.read_input_registers(starting_address=69, quantity=126)
+
+    def test_write_single_coil(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for single coil writing
+        """
+        for on_state in [1, 0xFF00, True]:
+            with self.subTest(on_state=on_state):
+                modbus_pdu = functions.write_single_coil(output_address=172,
+                                                         output_value=on_state)
+
+                self.assertIsInstance(modbus_pdu, bytes)
+                self.assertEqual(len(modbus_pdu), 5)
+                self.assertEqual(modbus_pdu, b'\x05\x00\xAC\xFF\x00')
+
+        for off_state in [0, 0x0000, False]:
+            with self.subTest(off_state=off_state):
+                modbus_pdu = functions.write_single_coil(output_address=199,
+                                                         output_value=off_state)
+
+                self.assertIsInstance(modbus_pdu, bytes)
+                self.assertEqual(len(modbus_pdu), 5)
+                self.assertEqual(modbus_pdu, b'\x05\x00\xC7\x00\x00')
+
+        for valid_state in [0, 1, 0x0000, 0xFF00, False, True]:
+            with self.subTest(valid_state=valid_state):
+                try:
+                    modbus_pdu = functions.write_single_coil(
+                        output_address=142,
+                        output_value=valid_state)
+
+                    self.assertIsInstance(modbus_pdu, bytes)
+                    self.assertEqual(len(modbus_pdu), 5)
+                except ValueError:
+                    self.fail("write_single_coil() raised unexpected \
+                        ValueError")
+
+        with self.assertRaises(ValueError):
+            functions.write_single_coil(output_address=69, output_value='on')
+
+        with self.assertRaises(ValueError):
+            functions.write_single_coil(output_address=69, output_value=0xFF01)
+
+    def test_write_single_register(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for single registers writing
+        """
+        # test signed value
+        modbus_pdu = functions.write_single_register(register_address=1,
+                                                     register_value=3)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x06\x00\x01\x00\x03')
+
+        # test signed value
+        modbus_pdu = functions.write_single_register(register_address=1,
+                                                     register_value=-3)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x06\x00\x01\xFF\xFD')
+
+        # test unsigned value
+        modbus_pdu = functions.write_single_register(register_address=1,
+                                                     register_value=3,
+                                                     signed=False)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 5)
+        self.assertEqual(modbus_pdu, b'\x06\x00\x01\x00\x03')
+
+    def test_write_multiple_coils(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for multiple coils writing
+        """
+        # write 11 coils starting at coil 20
+        value_list_int = [1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1]
+        #                  27 26 25 24 23 22 21 20  -  -  -  -  -  - 29 28
+        # value_list_int = [1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]
+        value_list_bool = [
+            True, True, False, False, True, True, False, True, False, True,
+            True
+        ]
+
+        for value_list in [value_list_int, value_list_bool]:
+            with self.subTest(value_list=value_list):
+                modbus_pdu = functions.write_multiple_coils(
+                    starting_address=19,
+                    value_list=value_list)
+
+                self.assertIsInstance(modbus_pdu, bytes)
+                self.assertEqual(len(modbus_pdu), 8)
+                self.assertEqual(modbus_pdu,
+                                 b'\x0F\x00\x13\x00\x0B\x02\xCD\x03')
+
+    def test_write_multiple_registers(self) -> None:
+        """
+        Test creation of Modbus Protocol Data Unit for single registers writing
+        """
+        # test signed value
+        register_values = [10, 258]
+        modbus_pdu = functions.write_multiple_registers(
+            starting_address=1,
+            register_values=register_values)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 10)
+        self.assertEqual(modbus_pdu,
+                         b'\x10\x00\x01\x00\x02\x04\x00\x0A\x01\x02')
+
+        # test signed value
+        register_values = [10, -258]
+        modbus_pdu = functions.write_multiple_registers(
+            starting_address=1,
+            register_values=register_values)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 10)
+        self.assertEqual(modbus_pdu,
+                         b'\x10\x00\x01\x00\x02\x04\x00\x0A\xFE\xFE')
+
+        # test unsigned value
+        register_values = [10, 258]
+        modbus_pdu = functions.write_multiple_registers(
+            starting_address=1,
+            register_values=register_values,
+            signed=False)
+
+        self.assertIsInstance(modbus_pdu, bytes)
+        self.assertEqual(len(modbus_pdu), 10)
+        self.assertEqual(modbus_pdu,
+                         b'\x10\x00\x01\x00\x02\x04\x00\x0A\x01\x02')
+
+        register_values = [7] * 124
+        with self.assertRaises(ValueError):
+            functions.write_multiple_registers(starting_address=42,
+                                               register_values=register_values)
+
+    def test_validate_resp_data_single_coil(self) -> None:
+        """Test response data validation of writing single coil"""
+        # test response of writing single coil to ON
+        starting_address = 172
+        output_value = 0xFF00
+        resp_data = b'\x05\x00\xAC\xFF\x00'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_SINGLE_COIL,
+            address=starting_address,
+            value=output_value,
+            signed=False
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+        # test response of writing single coil to OFF
+        starting_address = 199
+        output_value = 0x0000
+        resp_data = b'\x05\x00\xC7\x00\x00'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_SINGLE_COIL,
+            address=starting_address,
+            value=output_value,
+            signed=False
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+    def test_validate_resp_data_single_register(self) -> None:
+        """Test response data validation of writing single register"""
+        # test response of writing single register to 3
+        starting_address = 1
+        output_value = 3
+        resp_data = b'\x06\x00\x01\x00\x03'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_SINGLE_REGISTER,
+            address=starting_address,
+            value=output_value,
+            signed=True
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+        # test response of writing single register to -3
+        starting_address = 1
+        output_value = -3
+        resp_data = b'\x06\x00\x01\xFF\xFD'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_SINGLE_REGISTER,
+            address=starting_address,
+            value=output_value,
+            signed=True
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+        # test response of writing single register to -3 unsigned
+        starting_address = 1
+        output_value = 3
+        resp_data = b'\x06\x00\x01\x00\x03'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_SINGLE_REGISTER,
+            address=starting_address,
+            value=output_value,
+            signed=False
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+    def test_validate_resp_data_multiple_coils(self) -> None:
+        """Test response data validation of writing multiple coils"""
+        # test response of writing multiple coils
+        value_list_int = [1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1]
+        #                  27 26 25 24 23 22 21 20  -  -  -  -  -  - 29 28
+        # value_list_int = [1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]
+        value_list_bool = [
+            True, True, False, False, True, True, False, True, False, True,
+            True
+        ]
+        starting_address = 19
+        resp_data = b'\x0F\x00\x13\x00\x0B'[1:]
+
+        for value_list in [value_list_int, value_list_bool]:
+            with self.subTest(value_list=value_list):
+                result = functions.validate_resp_data(
+                    data=resp_data,
+                    function_code=Const.WRITE_MULTIPLE_COILS,
+                    address=starting_address,
+                    quantity=len(value_list)
+                )
+
+                self.assertIsInstance(result, bool)
+                self.assertTrue(result)
+
+    def test_validate_resp_data_multiple_register(self) -> None:
+        """Test response data validation of writing multiple registers"""
+        starting_address = 1
+
+        # test response of writing multiple registers with signed value
+        register_values = [10, 258]
+        resp_data = b'\x10\x00\x01\x00\x02\x04\x00\x0A\x01\x02'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_MULTIPLE_REGISTERS,
+            address=starting_address,
+            quantity=len(register_values)
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+        # test response of writing multiple registers with signed value
+        register_values = [10, -258]
+        resp_data = b'\x10\x00\x01\x00\x02\x04\x00\x0A\xFE\xFE'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_MULTIPLE_REGISTERS,
+            address=starting_address,
+            quantity=len(register_values)
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+        # test response of writing multiple registers with unsigned value
+        register_values = [10, 258]
+        resp_data = b'\x10\x00\x01\x00\x02\x04\x00\x0A\x01\x02'[1:]
+        result = functions.validate_resp_data(
+            data=resp_data,
+            function_code=Const.WRITE_MULTIPLE_REGISTERS,
+            address=starting_address,
+            quantity=len(register_values),
+            signed=False
+        )
+
+        self.assertIsInstance(result, bool)
+        self.assertTrue(result)
+
+    @unittest.skip('Test not yet implemented')
+    def test_response(self) -> None:
+        pass
+
+    def test_exception_response(self) -> None:
+        """Test exception responses"""
+        function_code = Const.READ_COILS
+        exception_code = Const.ILLEGAL_DATA_ADDRESS
+
+        result = functions.exception_response(function_code=function_code,
+                                              exception_code=exception_code)
+
+        self.assertIsInstance(result, bytes)
+        self.assertEqual(len(result), 2)
+        self.assertEqual(result, b'\x81\x02')
+
+    def test_bytes_to_bool(self) -> None:
+        """Convert bytes list to boolean list"""
+        possibilities = [
+            # response, qty, expectation
+            (b'\x00', 1, [False]),
+            (b'\x01', 1, [True]),
+
+            (b'\x00', 2, [False, False]),
+            (b'\x01', 2, [True, False]),
+            (b'\x02', 2, [False, True]),
+            (b'\x03', 2, [True, True]),
+
+            (b'\x00', 3, [False, False, False]),
+            (b'\x01', 3, [True, False, False]),
+            (b'\x02', 3, [False, True, False]),
+            (b'\x03', 3, [True, True, False]),
+            (b'\x04', 3, [False, False, True]),
+            (b'\x05', 3, [True, False, True]),
+            (b'\x06', 3, [False, True, True]),
+            (b'\x07', 3, [True, True, True]),
+
+            (b'\x00', 4, [False, False, False, False]),
+            # (b'\x05', 4, [False, True, False, True]),
+            (b'\x05', 4, [True, False, True, False]),
+            (b'\x0A', 4, [False, True, False, True]),
+            (b'\x0F', 4, [True, True, True, True]),
+
+            (b'\x0A', 5, [False, True, False, True, False]),
+        ]
+        for pair in possibilities:
+            with self.subTest(pair=pair):
+                byte_list = pair[0]
+                bit_qty = pair[1]
+                expectation = pair[2]
+
+                result = functions.bytes_to_bool(byte_list=byte_list,
+                                                 bit_qty=bit_qty)
+                self.assertIsInstance(result, list)
+                self.assertEqual(len(result), len(expectation))
+                self.assertTrue(all(isinstance(x, bool) for x in result))
+                self.assertEqual(result, expectation)
+
+    def test_to_short(self) -> None:
+        """Convert bytes list to integer tuple"""
+        possibilities = [
+            # response, signed, expectation
+            (b'\x00\x00', True, (0,)),
+            (b'\x00\x14', True, (20,)),
+
+            (b'\x00\x00\x00\x00', True, (0, 0)),
+            (b'\x00\x00\x00\x07', True, (0, 7)),
+            (b'\x00\x17\x00\x00', True, (23, 0)),
+            (b'\x00\x0c\x00\x13', True, (12, 19)),
+
+            (b'\x00\x00\x00\x00\x00\x00', True, (0, 0, 0)),
+            (b'\x00\x09\x00\x00\x00\x00', True, (9, 0, 0)),
+            (b'\x00\x00\x00\x01\x00\x00', True, (0, 1, 0)),
+            (b'\x00\x00\x00\x00\x00\x07', True, (0, 0, 7)),
+            (b'\x00\x1d\x00\x26\x00\x00', True, (29, 38, 0)),
+            (b'\x00\x11\x00\x00\x00\x04', True, (17, 0, 4)),
+            (b'\x09\x29\x00\x25\x11\x5c', True, (2345, 37, 4444)),
+        ]
+        for pair in possibilities:
+            with self.subTest(pair=pair):
+                byte_array = pair[0]
+                signed = pair[1]
+                expectation = pair[2]
+
+                result = functions.to_short(byte_array=byte_array,
+                                            signed=signed)
+                self.assertIsInstance(result, tuple)
+                self.assertEqual(len(result), len(expectation))
+                self.assertTrue(all(isinstance(x, int) for x in result))
+                self.assertEqual(result, expectation)
+
+    def test_float_to_bin(self) -> None:
+        """Test conversion of float to bin according to IEEE 754"""
+        float_val = 10.27
+        expectation = '01000001001001000101000111101100'
+
+        result = functions.float_to_bin(num=float_val)
+        self.assertIsInstance(result, str)
+        self.assertEqual(result, expectation)
+
+    def test_bin_to_float(self) -> None:
+        """Test conversion of binary string to float"""
+        binary = '01000001001001000101000111101100'
+        expectation = 10.27
+
+        result = functions.bin_to_float(binary=binary)
+        self.assertIsInstance(result, float)
+        self.assertAlmostEqual(result, expectation, delta=0.01)
+
+    def test_int_to_bin(self) -> None:
+        """Test conversion of integer to binary"""
+        number = 123
+        expectation = '1111011'
+
+        result = functions.int_to_bin(num=number)
+        self.assertIsInstance(result, str)
+        self.assertEqual(result, expectation)
+
+    def tearDown(self) -> None:
+        """Run after every test method"""
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_tcp_example.py b/tests/test_tcp_example.py
new file mode 100644
index 0000000..32cdd5e
--- /dev/null
+++ b/tests/test_tcp_example.py
@@ -0,0 +1,643 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+"""Unittest for testing functions of umodbus"""
+
+import json
+from random import randint
+import struct
+import ulogging as logging
+import mpy_unittest as unittest
+from umodbus.tcp import TCP as ModbusTCPMaster
+
+
+class TestTcpExample(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_tcp_port = 502     # port of client
+        self._client_addr = 10          # bus address of client
+        self._client_ip = '172.24.0.2'  # static Docker IP address
+        self._host = ModbusTCPMaster(
+            slave_ip=self._client_ip,
+            slave_port=self._client_tcp_port,
+            timeout=5.0)
+
+        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 ModbusTCPMaster and the defined register"""
+        self.assertEqual(self._host.trans_id_ctr, 0)
+        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)
+
+    def test__create_mbap_hdr(self) -> None:
+        """Test creating a Modbus header"""
+        trans_id = randint(1, 1000)     # create a random transaction ID
+        modbus_pdu = b'\x05\x00\x7b\xff\x00'    # WRITE_SINGLE_COIL 123 to True
+        self._host.trans_id_ctr = trans_id
+
+        # 0x00 0x06 is the lenght of the Modbus Protocol Data Unit +1
+        # 0x0A is the cliend address
+        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,
+                                             modbus_pdu=modbus_pdu)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), len(expectation))
+        self.assertEqual(result, expectation)
+        self.assertEqual(self._host.trans_id_ctr, trans_id + 1)
+
+    def test__validate_resp_hdr(self) -> None:
+        """Test response header validation"""
+        # positive path
+        # similar to @params in python
+        parameters = [
+            # (response, transaction ID, function_code, expectation)
+            # reading a single coil
+            (b'\x00\x01\x00\x00\x00\x04\x0a\x01\x01\x01', 1, 1, b'\x01\x01'),
+            (b'\x00\x02\x00\x00\x00\x04\x0a\x01\x01\x01', 2, 1, b'\x01\x01'),
+            # reading an input register
+            (b'\x00\x03\x00\x00\x00\x04\x0a\x02\x01\x00', 3, 2, b'\x01\x00'),
+            # reading a holding register
+            (
+                b'\x00\x04\x00\x00\x00\x05\x0a\x03\x02\x00\x13',
+                4,  # transaction ID
+                3,  # function code
+                b'\x02\x00\x13'
+            ),
+            # setting an input register
+            (
+                b'\x00\x05\x00\x00\x00\x05\x0a\x04\x02\xea\x0a',
+                5,  # transaction ID
+                4,  # function code
+                b'\x02\xea\x0a'
+            ),
+            # setting a single coil
+            (
+                b'\x00\x06\x00\x00\x00\x06\x0a\x05\x00\x7b\x00\x00',
+                6,  # transaction ID
+                5,  # function code
+                b'\x00\x7b\x00\x00'
+            ),
+            # setting a holding register
+            (
+                b'\x00\x07\x00\x00\x00\x06\x0a\x06\x00\x5d\x00\x14',
+                7,  # transaction ID
+                6,  # function code
+                b'\x00\x5d\x00\x14'
+            ),
+        ]
+
+        for pair in parameters:
+            with self.subTest(pair=pair):
+                response = pair[0]
+                trans_id = pair[1]
+                function_code = pair[2]
+                expectation = pair[3]
+
+                result = self._host._validate_resp_hdr(
+                    response=response,
+                    trans_id=trans_id,
+                    slave_id=self._client_addr,
+                    function_code=function_code)
+                self.test_logger.debug('result: {}, expectation: {}'.format(
+                    result, expectation))
+
+                self.assertIsInstance(result, bytes)
+                self.assertEqual(result, expectation)
+
+        # negative path, trigger asserts
+        data = {
+            #               TID                 SID FC
+            'input': b'\x00\x09\x00\x00\x00\x05\x0a\x03\x02\x00\x13',
+            'tid': 9,   # transaction ID
+            'sid': 10,  # slave ID
+            'fid': 3,   # function code, read holding register
+            'response': b'\x02\x00\x13'
+        }
+        # trigger wrong transaction ID assert
+        with self.assertRaises(ValueError):
+            self._host._validate_resp_hdr(
+                response=response,
+                trans_id=data['tid'] + 1,
+                slave_id=data['sid'],
+                function_code=data['fid'])
+
+        # trigger wrong function ID/throw Modbus exception code assert
+        with self.assertRaises(ValueError):
+            self._host._validate_resp_hdr(
+                response=response,
+                trans_id=data['tid'],
+                slave_id=data['sid'],
+                function_code=data['fid'] + 1)
+
+        # trigger wrong slave ID assert
+        with self.assertRaises(ValueError):
+            self._host._validate_resp_hdr(
+                response=response,
+                trans_id=data['tid'],
+                slave_id=data['sid'] + 1,
+                function_code=data['fid'])
+
+    @unittest.skip('Test not yet implemented')
+    def test__send_receive(self) -> None:
+        pass
+
+    def test_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)
+
+    def test_read_coils_multiple(self) -> None:
+        """Test reading multiple coils of client"""
+        coil_address = \
+            self._register_definitions['COILS']['EXAMPLE_COIL_MIXED']['register']     # noqa: E501
+        coil_qty = \
+            self._register_definitions['COILS']['EXAMPLE_COIL_MIXED']['len']
+        expectation_list = list(
+            map(bool,
+                self._register_definitions['COILS']['EXAMPLE_COIL_MIXED']['val']    # noqa: E501
+                )
+        )
+
+        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 {} lenght {}: {}, expectation: {}'.format(
+                coil_address, coil_qty, 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)
+
+        coil_address = \
+            self._register_definitions['COILS']['ANOTHER_EXAMPLE_COIL']['register']     # noqa: E501
+        coil_qty = \
+            self._register_definitions['COILS']['ANOTHER_EXAMPLE_COIL']['len']
+        expectation_list = list(
+            map(bool,
+                self._register_definitions['COILS']['ANOTHER_EXAMPLE_COIL']['val']    # noqa: E501
+                )
+        )
+
+        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 {} lenght {}: {}, expectation: {}'.format(
+                coil_address, coil_qty, 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)
+
+    def test_read_discrete_inputs_single(self) -> None:
+        """Test reading discrete inputs of client"""
+        ist_address = \
+            self._register_definitions['ISTS']['EXAMPLE_ISTS']['register']
+        input_qty = self._register_definitions['ISTS']['EXAMPLE_ISTS']['len']
+        expectation_list = [
+            bool(self._register_definitions['ISTS']['EXAMPLE_ISTS']['val'])
+        ]
+
+        input_status = self._host.read_discrete_inputs(
+            slave_addr=self._client_addr,
+            starting_addr=ist_address,
+            input_qty=input_qty)
+
+        self.test_logger.debug('Status of IST {}: {}, expectation: {}'.
+                               format(ist_address,
+                                      input_status,
+                                      expectation_list))
+        self.assertIsInstance(input_status, list)
+        self.assertEqual(len(input_status), input_qty)
+        self.assertTrue(all(isinstance(x, bool) for x in input_status))
+        self.assertEqual(input_status, expectation_list)
+
+    def test_read_discrete_inputs_multiple(self) -> None:
+        """Test reading multiple discrete inputs of client"""
+        ist_address = \
+            self._register_definitions['ISTS']['EXAMPLE_ISTS_MIXED']['register']     # noqa: E501
+        input_qty = \
+            self._register_definitions['ISTS']['EXAMPLE_ISTS_MIXED']['len']
+        expectation_list = \
+            self._register_definitions['ISTS']['EXAMPLE_ISTS_MIXED']['val']
+
+        input_status = self._host.read_discrete_inputs(
+            slave_addr=self._client_addr,
+            starting_addr=ist_address,
+            input_qty=input_qty)
+
+        self.test_logger.debug(
+            'Status of IST {} length {}: {}, expectation: {}'.format(
+                ist_address, input_qty, input_status, expectation_list))
+        self.assertIsInstance(input_status, list)
+        self.assertEqual(len(input_status), input_qty)
+        self.assertTrue(all(isinstance(x, bool) for x in input_status))
+        # self.assertEqual(input_status, expectation_list)
+
+        ist_address = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['register']     # noqa: E501
+        input_qty = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['len']
+        expectation_list = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['val']
+
+        input_status = self._host.read_discrete_inputs(
+            slave_addr=self._client_addr,
+            starting_addr=ist_address,
+            input_qty=input_qty)
+
+        self.test_logger.debug(
+            'Status of IST {} length {}: {}, expectation: {}'.format(
+                ist_address, input_qty, input_status, expectation_list))
+        self.assertIsInstance(input_status, list)
+        self.assertEqual(len(input_status), input_qty)
+        self.assertTrue(all(isinstance(x, bool) for x in input_status))
+        self.assertEqual(input_status, expectation_list)
+
+    def test_read_holding_registers_single(self) -> None:
+        """Test reading holding registers of client"""
+        hreg_address = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG']['register']
+        register_qty = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG']['len']
+        expectation = \
+            (self._register_definitions['HREGS']['EXAMPLE_HREG']['val'], )
+
+        register_value = self._host.read_holding_registers(
+            slave_addr=self._client_addr,
+            starting_addr=hreg_address,
+            register_qty=register_qty)
+
+        self.test_logger.debug('Status of HREG {}: {}, expectation: {}'.
+                               format(hreg_address,
+                                      register_value,
+                                      expectation))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, expectation)
+
+    def test_read_holding_registers_multiple(self) -> None:
+        """Test reading multiple holding registers of client"""
+        hreg_address = \
+            self._register_definitions['HREGS']['ANOTHER_EXAMPLE_HREG']['register']     # noqa: E501
+        register_qty = \
+            self._register_definitions['HREGS']['ANOTHER_EXAMPLE_HREG']['len']
+        expectation = tuple(
+            self._register_definitions['HREGS']['ANOTHER_EXAMPLE_HREG']['val']
+        )
+
+        register_value = self._host.read_holding_registers(
+            slave_addr=self._client_addr,
+            starting_addr=hreg_address,
+            register_qty=register_qty)
+
+        self.test_logger.debug(
+            'Status of HREG {} length {}: {}, expectation: {}'.format(
+                hreg_address, register_qty, register_value, expectation))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, expectation)
+
+    def test_read_input_registers_single(self) -> None:
+        """Test reading input registers of client"""
+        ireg_address = \
+            self._register_definitions['IREGS']['EXAMPLE_IREG']['register']
+        register_qty = \
+            self._register_definitions['IREGS']['EXAMPLE_IREG']['len']
+        expectation = \
+            (self._register_definitions['IREGS']['EXAMPLE_IREG']['val'], )
+
+        register_value = self._host.read_input_registers(
+            slave_addr=self._client_addr,
+            starting_addr=ireg_address,
+            register_qty=register_qty,
+            signed=False)
+
+        self.test_logger.debug('Status of IREG {}: {}, expectation: {}'.
+                               format(ireg_address,
+                                      register_value,
+                                      expectation))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, expectation)
+
+    def test_read_input_registers_multiple(self) -> None:
+        """Test reading multiple input registers of client"""
+        ireg_address = \
+            self._register_definitions['IREGS']['ANOTHER_EXAMPLE_IREG']['register']     # noqa: E501
+        register_qty = \
+            self._register_definitions['IREGS']['ANOTHER_EXAMPLE_IREG']['len']
+        expectation = tuple(
+            self._register_definitions['IREGS']['ANOTHER_EXAMPLE_IREG']['val']
+        )
+
+        register_value = self._host.read_input_registers(
+            slave_addr=self._client_addr,
+            starting_addr=ireg_address,
+            register_qty=register_qty,
+            signed=False)
+
+        self.test_logger.debug(
+            'Status of IREG {} length {}: {}, expectation: {}'.format(
+                ireg_address, register_qty, register_value, expectation))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, expectation)
+
+    def test_reset_client_data(self) -> None:
+        """Test resettig client data to default"""
+        coil_address = \
+            self._register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']     # noqa: E501
+        coil_qty = \
+            self._register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['len']  # noqa: E501
+
+        operation_status = self._host.write_single_coil(
+            slave_addr=self._client_addr,
+            output_address=coil_address,
+            output_value=True)
+
+        self.test_logger.debug(
+            'Result of setting COIL {} to {}: {}, expectation: {}'.format(
+                coil_address, True, operation_status, [True]))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # The coil value is actually True for a very short time
+
+        # verify setting of state by reading data back again
+        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, [False]))
+        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, [False])
+
+    def test_write_single_coil(self) -> None:
+        """Test updating single coil of client"""
+        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'])
+        ]
+
+        #
+        # Check clean system (client register data is as initially defined)
+        #
+        # verify current state by reading coil states
+        coil_status = self._host.read_coils(
+            slave_addr=self._client_addr,
+            starting_addr=coil_address,
+            coil_qty=coil_qty)
+
+        self.test_logger.debug(
+            'Initial 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)
+
+        #
+        # Test setting coil to True
+        #
+        # update coil state of client with a different than the current state
+        new_coil_val = not expectation_list[0]
+        expectation_list[0] = new_coil_val
+
+        operation_status = self._host.write_single_coil(
+            slave_addr=self._client_addr,
+            output_address=coil_address,
+            output_value=new_coil_val)
+
+        self.test_logger.debug(
+            '1. Result of setting COIL {} to {}: {}, expectation: {}'.format(
+                coil_address, new_coil_val, operation_status, True))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # verify setting of state by reading data back again
+        coil_status = self._host.read_coils(
+            slave_addr=self._client_addr,
+            starting_addr=coil_address,
+            coil_qty=coil_qty)
+
+        self.test_logger.debug('1. 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)
+
+        #
+        # Test setting coil to False
+        #
+        # update coil state of client again with/to original state
+        new_coil_val = not expectation_list[0]
+        expectation_list[0] = new_coil_val
+
+        operation_status = self._host.write_single_coil(
+            slave_addr=self._client_addr,
+            output_address=coil_address,
+            output_value=new_coil_val)
+
+        self.test_logger.debug(
+            '2. Result of setting COIL {} to {}: {}, expectation: {}'.format(
+                coil_address, new_coil_val, operation_status, True))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # verify setting of state by reading data back again
+        coil_status = self._host.read_coils(
+            slave_addr=self._client_addr,
+            starting_addr=coil_address,
+            coil_qty=coil_qty)
+
+        self.test_logger.debug('2. 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)
+
+    def test_write_single_register(self) -> None:
+        """Test updating single holding register of client"""
+        hreg_address = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG']['register']
+        register_qty = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG']['len']
+        expectation = \
+            (self._register_definitions['HREGS']['EXAMPLE_HREG']['val'], )
+
+        #
+        # Check clean system (client register data is as initially defined)
+        #
+        # verify current state by reading holding register data
+        register_value = self._host.read_holding_registers(
+            slave_addr=self._client_addr,
+            starting_addr=hreg_address,
+            register_qty=register_qty)
+
+        self.test_logger.debug(
+            'Initial status of HREG {}: {}, expectation: {}'.format(
+                hreg_address,
+                register_value,
+                expectation))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, expectation)
+
+        #
+        # Test setting holding register to x+1
+        #
+        # update holding register of client with a different than the current
+        # value
+        new_hreg_val = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG']['val'] + 1
+
+        operation_status = self._host.write_single_register(
+            slave_addr=self._client_addr,
+            register_address=hreg_address,
+            register_value=new_hreg_val,
+            signed=False)
+        self.test_logger.debug(
+            '1. Result of setting HREG {} to {}: {}, expectation: {}'.format(
+                hreg_address, new_hreg_val, operation_status, (new_hreg_val, )))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # verify setting of state by reading data back again
+        register_value = self._host.read_holding_registers(
+            slave_addr=self._client_addr,
+            starting_addr=hreg_address,
+            register_qty=register_qty)
+
+        self.test_logger.debug('1. Status of HREG {}: {}, expectation: {}'.
+                               format(hreg_address,
+                                      register_value,
+                                      new_hreg_val))
+        self.assertIsInstance(register_value, tuple)
+        self.assertEqual(len(register_value), register_qty)
+        self.assertTrue(all(isinstance(x, int) for x in register_value))
+        self.assertEqual(register_value, (new_hreg_val, ))
+
+    @unittest.skip('Test not yet implemented')
+    def test_write_multiple_coils(self) -> None:
+        pass
+
+    @unittest.skip('Test not yet implemented')
+    def test_write_multiple_registers(self) -> None:
+        pass
+
+    def tearDown(self) -> None:
+        """Run after every test method"""
+        # reset the client data back to the default values
+        coil_address = \
+            self._register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']     # noqa: E501
+
+        operation_status = self._host.write_single_coil(
+            slave_addr=self._client_addr,
+            output_address=coil_address,
+            output_value=True)
+
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+
+if __name__ == '__main__':
+    unittest.main()