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

Issue:
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`
Reading coils is limited to 8 bits
Related to #12, found during https://github.com/brainelectronics/micropython-modbus/pull/33#issuecomment-1336274272 section "Problem 1" and "Problem 2"

Only up to 8 coils can be read at once. Reading 9 or more return only valid results for the first 8 bit.

**Reason/root cause:**
Only the first integer value of the byte list is taken into account, see https://github.com/brainelectronics/micropython-modbus/blob/2f1e307fa087e2f97fa6031a4bfb5b8d8bdddcd2/umodbus/functions.py#L361 although the format is correctly set to contain the specified amount of bits
Add typing hints
Add typing hints to all files being used in this lib
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 registers - `host.write_multiple_registers()`
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 registers (`host.write_multiple_registers()`)

```
>>> host.write_single_register(slave_addr=10, register_address=93, register_value=44, signed=False)
True
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(44,)
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, -4, 6, -256, 1024], signed=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, 1024], signed=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 

```
Bug: error when trying to write multiple registers - `host.write_multiple_registers()`
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 registers (`host.write_multiple_registers()`)

```
>>> host.write_single_register(slave_addr=10, register_address=93, register_value=44, signed=False)
True
>>> host.read_holding_registers(slave_addr=10, starting_addr=93, register_qty=1, signed=False)
(44,)
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, -4, 6, -256, 1024], signed=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 
>>> host.write_multiple_registers(slave_addr=10, starting_address=93, register_values=[2, 1024], signed=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/serial.py", line 291, in write_multiple_registers
  File "umodbus/serial.py", line 173, in _send_receive
  File "umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>> 

```
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:


Ensure `List`, `Union`, `Tuple` and others are subscritable otherwise an error might be raised. 

```python
class _Subscriptable:

    def __getitem__(self, sub):
        return None

_subscriptable = _Subscriptable()

List = _subscriptable
Union = _subscriptable
# ...
```

See [Modbus Application Protocol](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf) section 6.12


See [Modbus Application Protocol](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf) section 6.12


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
```
@beyonlo could you try my fixes of https://github.com/brainelectronics/micropython-modbus/tree/feature/create-test-framework, this bug should now be fixed. At least this is what my new unittests report
@brainelectronics I can't to test the because this [new version](https://github.com/brainelectronics/micropython-modbus/tree/feature/create-test-framework) has problem to connect:

**ModBus TCP Master  - ESP32_02:**

```Python
>>> from umodbus.tcp import TCP as ModbusTCPMaster
>>> host = ModbusTCPMaster(slave_ip='192.168.1.4', slave_port=502, timeout=5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "umodbus/tcp.py", line 30, in __init__
OSError: [Errno 118] EHOSTUNREACH
>>> 
```

**ModBus TCP Slave  - ESP32_01:**
```Bash
$ mpremote run tcp_client_example.py
Waiting for WiFi connection...
Waiting for WiFi connection...
Waiting for WiFi connection...
Connected to WiFi.
('192.168.1.4', '255.255.255.0', '192.168.1.1', '192.168.1.1')
Setting up registers ...
Register setup done
Serving as TCP client on 192.168.1.4:502
```
**Note:** I tried to use a old version (1.1.0) on for the Master and I have the same error. However I tried to connect via `MODSCAN32` (ModBus TCP Master for windows) and connected with success.


In both, Master and Slave I installed via pip the version 1.2.0rc5.dev28:

```Bash
>> import upip
>>> upip.index_urls = ['https://test.pypi.org/pypi']
>>> upip.install('micropython-modbus')
Installing to: /lib/
Warning: test.pypi.org SSL certificate is not validated
Installing micropython-modbus 1.2.0rc5.dev28 from https://test-files.pythonhosted.org/packages/5d/75/753871c80d40e9d6659e6e7185b0022d26f137265c2c333ad0865079fb89/micropython-modbus-1.2.0rc5.dev28.tar.gz
```

I tried as well, just for sure, download the .zip from that [new version](https://github.com/brainelectronics/micropython-modbus/tree/feature/create-test-framework) and test it, and I have the same error.
Hey @brainelectronics 

I tested the `1.2.0rc5.dev28` using `ModBus RTU` (where do not need `TCP` connection of course), but bug still the same:

**ModBus RTU Slave:**
For the slave I used this [rtu_client_example.py](https://github.com/brainelectronics/micropython-modbus/blob/feature/create-test-framework/examples/rtu_client_example.py)
`$ mpremote run rtu_client_example.py
`

**ModBus RTU Master:**

```Bash
>>> from umodbus.serial import Serial as ModbusRTUMaster
>>> rtu_pins = (18, 17)
>>> host = ModbusRTUMaster(baudrate=9600, data_bits=8, stop_bits=1, parity=None, pins=rtu_pins)
>>>
>>> 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])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/serial.py", line 274, in write_multiple_coils
  File "/lib/umodbus/serial.py", line 173, in _send_receive
  File "/lib/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,1,1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/umodbus/serial.py", line 274, in write_multiple_coils
  File "/lib/umodbus/serial.py", line 173, in _send_receive
  File "/lib/umodbus/serial.py", line 190, in _validate_resp_hdr
ValueError: slave returned exception code: 1
>>>
>>> host.write_single_coil(slave_addr=10, output_address=123, output_value=0)
True
>>> host.read_coils(slave_addr=10, starting_addr=123, coil_qty=1)
[False, False, False, False, False, False, False, False]
```

**Scenario used:**
ESP32-S3_01 (ModBus 1.2.0rc5.dev28) --------  UART ------------- ESP32-S3_02 (ModBus 1.2.0rc5.dev28)

**Installation method: via pip:**
```Bash
>>> import upip
>>> upip.index_urls = ['https://test.pypi.org/pypi']
>>> upip.install('micropython-modbus')
Installing to: /lib/
Warning: test.pypi.org SSL certificate is not validated
Installing micropython-modbus 1.2.0rc5.dev28 from https://test-files.pythonhosted.org/packages/5d/75/753871c80d40e9d6659e6e7185b0022d26f137265c2c333ad0865079fb89/micropython-modbus-1.2.0rc5.dev28.tar.gz
```



Fix patch:
diff --git a/README.md b/README.md
index aaf335e..5496d17 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ 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)
+[Micropython section of USAGE](USAGE.md)
 
 #### TCP
 
@@ -175,7 +175,7 @@ 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]:
+* **pfalcon** - *Initial MicroPython unittest module* - [micropython-unittest][ref-pfalcon-unittest]
 
 <!-- Links -->
 [ref-sferalabs-exo-sense]: https://github.com/sfera-labs/exo-sense-py-modbus
diff --git a/changelog.md b/changelog.md
index f83909e..bdf3d65 100644
--- a/changelog.md
+++ b/changelog.md
@@ -15,6 +15,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 <!-- ## [Unreleased] -->
 
 ## Released
+## [2.1.0] - 2022-12-27
+### Added
+- Typing hints available for all functions of [umodbus](umodbus), see #27
+- Docstrings available for all constants, functions and classes of [umodbus](umodbus/), see #27
+- Test for reading more than 8 coils in a row to verify fix of #36
+- Test for reading single negative holding register value
+- Test for writing multiple coils to verify fix of #22
+- Test for writing multiple registers to verify fix of #23
+- Usage documentation for coil, discrete inputs, holding register and input register usage
+- Modbus TCP IP and port binding can be checked with `is_bound` property in [tcp.py](umodbus/tcp.py)
+
+### Changed
+- Reordered modules of API documentation
+- `data_as_registers` and `data_as_bits` of [common.py](umodbus/common.py) removed
+- Send illegal function code `0x01` if a register other than coil or holding register is requested to be set
+- Simplified `_process_write_access` logic of [tcp.py](umodbus/tcp.py)
+
+### Fixed
+- Typing hints of function input parameters and return values
+- Response data of multiple changed registers (`write_multiple_registers`) is validated with respect to the provided `signed` flag in [serial.py](umodbus/serial.py) and [tcp.py](umodbus/tcp.py), see #23
+- Enable reading more than 8 coils in a row, see #36
+- Writing multiple coils in TCP, see #22
+- Writing multiple registers in TCP, see #23
+- Unit test `test_bytes_to_bool` uses MSB and LSB data correctly
+- Only requested amount of registers are returned by `_process_read_access` logic of [tcp.py](umodbus/tcp.py), see #35
+
 ## [2.0.0] - 2022-12-03
 ### Added
 - Perform MicroPython based unittests on every `Test` workflow run
@@ -151,8 +177,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus)
 
 <!-- Links -->
-[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.0.0...develop
+[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.1.0...develop
 
+[2.1.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.0
 [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
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 5a08c1d..8bf19b4 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -4,8 +4,10 @@ Overview to use and test this `micropython-modbus` library
 
 ---------------
 
-The onwards described steps assume a successful setup as described in
-[SETUP.md](SETUP.md)
+```{note}
+The onwards described steps assume a successful setup as described in the
+[setup chapter](SETUP.md)
+```
 
 ## MicroPython
 
@@ -134,10 +136,15 @@ The value of multiple registers can be set like this
 }
 ```
 
-> :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)
+```{eval-rst}
+.. warning::
+    As of version `2.0.0 <https://github.com/brainelectronics/micropython-modbus/releases/tag/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 `issue #35 <https://github.com/brainelectronics/micropython-modbus/issues/35>`_
+```
 
 #### Detailed key explanation
 
@@ -182,8 +189,6 @@ config
 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
@@ -217,6 +222,308 @@ 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`.
 
+### Register usage
+
+This section describes the usage of the following available functions
+
+ - [0x01 `read_coils`](umodbus.tcp.TCP.read_coils)
+ - [0x02 `read_discrete_inputs`](umodbus.tcp.TCP.read_discrete_inputs)
+ - [0x03 `read_holding_registers`](umodbus.tcp.TCP.read_holding_registers)
+ - [0x04 `read_input_registers`](umodbus.tcp.TCP.read_input_registers)
+ - [0x05 `write_single_coil`](umodbus.tcp.TCP.write_single_coil)
+ - [0x06 `write_single_register`](umodbus.tcp.TCP.write_single_register)
+ - [0x0F `write_multiple_coils`](umodbus.tcp.TCP.write_multiple_coils)
+ - [0x10 `write_multiple_registers`](umodbus.tcp.TCP.write_multiple_registers)
+
+based on TCP togehter with the latest provided
+[examples](https://github.com/brainelectronics/micropython-modbus/tree/develop/examples)
+
+All described functions require a successful setup of a Host communicating
+to/with a Client device which is providing the data and accepting the new data.
+
+```python
+from umodbus.tcp import TCP as ModbusTCPMaster
+
+slave_tcp_port = 502            # port to listen to
+slave_addr = 10                 # bus address of client
+slave_ip = '192.168.178.69'     # IP address, to be adjusted
+
+host = ModbusTCPMaster(
+    slave_ip=slave_ip,
+    slave_port=slave_tcp_port,
+    timeout=5)                  # optional, default 5
+```
+
+#### Coils
+
+Coils represent binary states, which can be get as and set to either `0` (off)
+or `1` (on).
+
+##### Read
+
+```{note}
+The function code `0x01` is used to read from 1 to 2000 contiguous status of
+coils in a remote device.
+```
+
+With the function [`read_coils`](umodbus.tcp.TCP.read_coils) a single coil
+status can be read.
+
+```python
+coil_address = 125
+coil_qty = 2
+coil_status = host.read_coils(
+    slave_addr=slave_addr,
+    starting_addr=coil_address,
+    coil_qty=coil_qty)
+print('Status of COIL {}: {}'.format(coil_address, coil_status))
+# Status of COIL 125:  [True, False]
+```
+
+```{eval-rst}
+.. warning::
+    Please be aware of `bug #35 <https://github.com/brainelectronics/micropython-modbus/issues/35>`_.
+
+    It is not possible to read a specific position within a configured list of
+    multiple coils on a MicroPython Modbus TCP client device. Reading coil 126
+    in the above example will throw an error.
+
+    This bug affects only devices using this package. Other devices work as
+    expected and can be addressed as specified.
+```
+
+##### Write
+
+Coils can be set with `False` or `0` to the `OFF` state and with `True` or `1`
+to the `ON` state.
+
+###### Single
+
+```{note}
+The function code `0x05` is used to write a single output to either `ON` or
+`OFF` in a remote device.
+```
+
+With the function [`write_single_coil`](umodbus.tcp.TCP.write_single_coil)
+a single coil status can be set.
+
+```python
+coil_address = 123
+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 {}: {}'.format(coil_address, operation_status))
+# Result of setting COIL 123: True
+```
+
+```{eval-rst}
+.. warning::
+    Please be aware of `bug #15 <https://github.com/brainelectronics/micropython-modbus/issues/15>`_.
+
+    It is not possible to write to a specific position within a configured
+    list of multiple coils on a MicroPython Modbus TCP client device.
+
+    This bug affects only devices using this package. Other devices work as
+    expected and can be addressed as specified.
+```
+
+###### Multiple
+
+```{note}
+The function code `0x0F` is used to force each coil in a sequence of coils to
+either `ON` or `OFF` in a remote device.
+```
+
+With the function [`write_multiple_coils`](umodbus.tcp.TCP.write_multiple_coils)
+multiple coil states can be set at once.
+
+```python
+coil_address = 126
+new_coil_vals = [1, 1, 0]
+operation_status = self._host.write_multiple_coils(
+            slave_addr=slave_addr,
+            starting_address=coil_address,
+            output_values=new_coil_vals)
+print('Result of setting COIL {}: {}'.format(coil_address, operation_status))
+# Result of setting COIL 126: True
+```
+
+```{eval-rst}
+.. warning::
+    Please be aware of `bug #35 <https://github.com/brainelectronics/micropython-modbus/issues/35>`_.
+
+    It is not possible to write to a specific position within a configured
+    list of multiple coils on a MicroPython Modbus TCP client device. Setting
+    coil `127`, which is `1` in the above example will throw an error.
+
+    This bug affects only devices using this package. Other devices work as
+    expected and can be addressed as specified.
+```
+
+#### Discrete inputs
+
+Discrete inputs represent binary states, which can be get as either `0` (off)
+or `1` (on). Unlike [coils](#coils), these cannot be set.
+
+##### Read
+
+```{note}
+The function code `0x02` is used to read from 1 to 2000 contiguous status of
+discrete inputs in a remote device.
+```
+
+With the function [`read_discrete_inputs`](umodbus.tcp.TCP.read_discrete_inputs)
+discrete inputs can be read.
+
+```python
+ist_address = 68
+input_qty = 2
+input_status = host.read_discrete_inputs(
+    slave_addr=slave_addr,
+    starting_addr=ist_address,
+    input_qty=input_qty)
+print('Status of IST {}: {}'.format(ist_address, input_status))
+# Status of IST 68: [True, False]
+```
+
+#### Holding registers
+
+Holding registers can be get as and set to any value between `0` and `65535`.
+If supported by the client device, data can be marked as signed values to
+represent `-32768` through `32767`.
+
+##### Read
+
+```{note}
+The function code `0x03` is used to read the contents of a contiguous block
+of holding registers in a remote device.
+```
+
+With the function
+[`read_holding_registers`](umodbus.tcp.TCP.read_holding_registers) a single
+holding register can be read.
+
+```python
+hreg_address = 94
+register_qty = 3
+register_value = host.read_holding_registers(
+    slave_addr=slave_addr,
+    starting_addr=hreg_address,
+    register_qty=register_qty,
+    signed=False)
+print('Status of HREG {}: {}'.format(hreg_address, register_value))
+# Status of HREG 94: [29, 38, 0]
+```
+
+```{eval-rst}
+.. warning::
+    Please be aware of `bug #35 <https://github.com/brainelectronics/micropython-modbus/issues/35>`_.
+
+    It is not possible to read a specific position within a configured list of
+    multiple holding registers on a MicroPython Modbus TCP client device.
+    Reading holding register `95`, holding the value `38` in the above example
+    will throw an error.
+
+    This bug affects only devices using this package. Other devices work as
+    expected and can be addressed as specified.
+```
+
+##### Write
+
+Holding registers can be set to `0` through `65535` or `-32768` through `32767`
+in case signed values are used.
+
+###### Single
+
+```{note}
+The function code `0x06` is used to write a single holding register in a
+remote device.
+```
+
+With the function
+[`write_single_register`](umodbus.tcp.TCP.write_single_register) a single
+holding register can be set.
+
+```python
+hreg_address = 93
+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 {}: {}'.format(hreg_address, operation_status))
+# Result of setting HREG 93: True
+```
+
+###### Multiple
+
+```{note}
+The function code `0x10` is used to write a block of contiguous registers
+(1 to 123 registers) in a remote device.
+```
+
+With the function
+[`write_multiple_registers`](umodbus.tcp.TCP.write_multiple_registers)
+holding register can be set at once.
+
+```python
+hreg_address = 94
+new_hreg_vals = [54, -12, 30001]
+operation_status = self._host.write_multiple_registers(
+    slave_addr=slave_addr,
+    starting_address=hreg_address,
+    register_values=new_hreg_vals,
+    signed=True)
+print('Result of setting HREG {}: {}'.format(hreg_address, operation_status))
+# Result of setting HREG 94: True
+```
+
+```{eval-rst}
+.. warning::
+
+    Please be aware of `bug #35 <https://github.com/brainelectronics/micropython-modbus/issues/35>`_.
+
+    It is not possible to write to a specific position within a configured
+    list of multiple holding registers on a MicroPython Modbus TCP client
+    device. Setting holding register `95 + 96` to e.g. `[-12, 30001]` in the
+    above example will throw an error.
+
+    This bug affects only devices using this package. Other devices work as
+    expected and can be addressed as specified.
+```
+
+#### Input registers
+
+Input registers can hold values between `0` and `65535`. If supported by the
+client device, data can be marked as signed values to represent `-32768`
+through `32767`. Unlike [holding registers](#holding-registers), these cannot
+be set.
+
+##### Read
+
+```{note}
+The function code `0x04` is used to read from 1 to 125 contiguous input
+registers in a remote device.
+```
+
+With the function [`read_input_registers`](umodbus.tcp.TCP.read_input_registers)
+input registers can be read.
+
+```python
+ireg_address = 11
+register_qty = 3
+register_value = host.read_input_registers(
+    slave_addr=slave_addr,
+    starting_addr=ireg_address,
+    register_qty=register_qty,
+    signed=False)
+print('Status of IREG {}: {}'.format(ireg_address, register_value))
+# Status of IREG 11: [59123, 0, 390]
+```
+
 ### TCP
 
 Get two network capable boards up and running, collecting and setting data on
diff --git a/docs/umodbus.rst b/docs/umodbus.rst
index 8ad7a94..6738458 100755
--- a/docs/umodbus.rst
+++ b/docs/umodbus.rst
@@ -4,25 +4,28 @@ micropython-modbus API
 .. autosummary::
    :toctree: generated
 
-umodbus.common module
+Modbus Constants
 ---------------------------------
 
-.. automodule:: umodbus.common
+.. automodule:: umodbus.const
    :members:
+   :private-members:
    :show-inheritance:
 
-umodbus.const module
+Common module
 ---------------------------------
 
-.. automodule:: umodbus.const
+.. automodule:: umodbus.common
    :members:
+   :private-members:
    :show-inheritance:
 
-umodbus.functions module
+Common functions
 ---------------------------------
 
 .. automodule:: umodbus.functions
    :members:
+   :private-members:
    :show-inheritance:
 
 umodbus.modbus module
@@ -30,18 +33,21 @@ umodbus.modbus module
 
 .. automodule:: umodbus.modbus
    :members:
+   :private-members:
    :show-inheritance:
 
-umodbus.serial module
+Serial
 ---------------------------------
 
 .. automodule:: umodbus.serial
    :members:
+   :private-members:
    :show-inheritance:
 
-umodbus.tcp module
+TCP
 ---------------------------------
 
 .. automodule:: umodbus.tcp
    :members:
+   :private-members:
    :show-inheritance:
diff --git a/registers/example.json b/registers/example.json
index a353010..a8b7c56 100644
--- a/registers/example.json
+++ b/registers/example.json
@@ -39,9 +39,29 @@
             "description": "Example COILS registers with length of 3, Coils (setter+getter) [0, 1]",
             "range": "",
             "unit": ""
+        },
+        "MANY_COILS": {
+            "register": 150,
+            "len": 19,
+            "val": [
+                1, 0, 1, 1, 0, 0, 1, 1,
+                1, 1, 0, 1, 0, 1, 1, 0,
+                1, 0, 1
+            ],
+            "description": "Example COILS registers with length of 19, representing Modbus_Application_Protocol_V1_1b3 Read Coils example, Coils (setter+getter) [0, 1]",
+            "range": "",
+            "unit": ""
         }
     },
     "HREGS": {
+        "EXAMPLE_HREG_NEGATIVE": {
+            "register": 92,
+            "len": 1,
+            "val": -29,
+            "description": "Example HREGS register, Hregs (setter+getter) [0, 65535]",
+            "range": "",
+            "unit": ""
+        },
         "EXAMPLE_HREG": {
             "register": 93,
             "len": 1,
diff --git a/umodbus/common.py b/umodbus/common.py
index 2c8d0db..59a7b04 100644
--- a/umodbus/common.py
+++ b/umodbus/common.py
@@ -14,9 +14,13 @@
 # custom packages
 from . import const as Const
 
+# typing not natively supported on MicroPython
+from .typing import Optional
+
 
 class Request(object):
-    def __init__(self, interface, data):
+    """Deconstruct request data received via TCP or Serial"""
+    def __init__(self, interface, data: bytearray) -> None:
         self._itf = interface
         self.unit_addr = data[0]
         self.function, self.register_addr = struct.unpack_from('>BH', data, 1)
@@ -65,7 +69,17 @@ def __init__(self, interface, data):
             self.quantity = None
             self.data = data[4:]
 
-    def send_response(self, values=None, signed=True):
+    def send_response(self,
+                      values: Optional[list] = None,
+                      signed: bool = True) -> None:
+        """
+        Send a response via the configured interface.
+
+        :param      values:  The values
+        :type       values:  Optional[list]
+        :param      signed:  Indicates if signed values are used
+        :type       signed:  bool
+        """
         self._itf.send_response(self.unit_addr,
                                 self.function,
                                 self.register_addr,
@@ -74,29 +88,20 @@ def send_response(self, values=None, signed=True):
                                 values,
                                 signed)
 
-    def send_exception(self, exception_code):
+    def send_exception(self, exception_code: int) -> None:
+        """
+        Send an exception response.
+
+        :param      exception_code:  The exception code
+        :type       exception_code:  int
+        """
         self._itf.send_exception_response(self.unit_addr,
                                           self.function,
                                           exception_code)
 
-    def data_as_bits(self):
-        bits = []
-
-        for byte in self.data:
-            for i in range(0, 8):
-                bits.append((byte >> i) & 1)
-
-                if len(bits) == self.quantity:
-                    return bits
-
-    def data_as_registers(self, signed=True):
-        qty = self.quantity if (self.quantity is not None) else 1
-        fmt = ('h' if signed else 'H') * qty
-
-        return struct.unpack('>' + fmt, self.data)
-
 
 class ModbusException(Exception):
-    def __init__(self, function_code, exception_code):
+    """Exception for signaling modbus errors"""
+    def __init__(self, function_code: int, exception_code: int) -> None:
         self.function_code = function_code
         self.exception_code = exception_code
diff --git a/umodbus/const.py b/umodbus/const.py
index 3ebb228..eb940be 100644
--- a/umodbus/const.py
+++ b/umodbus/const.py
@@ -7,55 +7,104 @@
 # see the Pycom Licence v1.0 document supplied with this file, or
 # available at https://www.pycom.io/opensource/licensing
 #
+# Description summary taken from
+# https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
 
 from micropython import const
 
 # function codes
 # defined as const(), see https://github.com/micropython/micropython/issues/573
+#: Read contiguous status of coils
 READ_COILS = const(0x01)                # COILS, [0, 1]
+#: Read contiguous status of discrete inputs
 READ_DISCRETE_INPUTS = const(0x02)      # ISTS,  [0, 1]
+#: Read the contents of a contiguous block of holding registers
 READ_HOLDING_REGISTERS = const(0x03)    # HREGS, [0, 65535]
+#: Read contiguous input registers
 READ_INPUT_REGISTER = const(0x04)       # IREGS, [0, 65535]
 
+#: Write a single coil output status to ON or OFF
 WRITE_SINGLE_COIL = const(0x05)         # COILS, [0, 1]
+#: Write a single holding register
 WRITE_SINGLE_REGISTER = const(0x06)     # HREGS, [0, 65535]
+#: Force each coil in a sequence of coils to either ON or OFF
 WRITE_MULTIPLE_COILS = const(0x0F)      # COILS, [0, 1]
+#: Write a block of contiguous registers
 WRITE_MULTIPLE_REGISTERS = const(0x10)  # HREGS, [0, 65535]
 
+"""
+Modify the contents of a specified holding register using a combination of an
+AND mask, an OR mask, and the register's current contents
+"""
 MASK_WRITE_REGISTER = const(0x16)
+"""
+Perform a combination of one read operation and one write operation in a
+single MODBUS transaction
+"""
 READ_WRITE_MULTIPLE_REGISTERS = const(0x17)
 
+#: Read the contents of a First-In-First-Out (FIFO) queue of register
 READ_FIFO_QUEUE = const(0x18)
 
+#: Perform a file record read
 READ_FILE_RECORD = const(0x14)
+#: Perform a file record write
 WRITE_FILE_RECORD = const(0x15)
 
+#: Read the contents of eight Exception Status outputs
 READ_EXCEPTION_STATUS = const(0x07)
+#: Provide series of tests for checking the communication system (serial only)
 DIAGNOSTICS = const(0x08)
+#: Get status word and an event count from the remote device com event counter
 GET_COM_EVENT_COUNTER = const(0x0B)
+#: Get a status word, event count, message count, and a field of event bytes
 GET_COM_EVENT_LOG = const(0x0C)
+#: Read the description of the type, the current status, and other informations
 REPORT_SERVER_ID = const(0x11)
+#: Encapsulated Interface Transport
 READ_DEVICE_IDENTIFICATION = const(0x2B)
 
 # exception codes
+#: Function code received in query is not an allowable action for the server
 ILLEGAL_FUNCTION = const(0x01)
+#: Data address received in query is not an allowable address for the server
 ILLEGAL_DATA_ADDRESS = const(0x02)
+#: A value contained in the query is not an allowable value for the server
 ILLEGAL_DATA_VALUE = const(0x03)
+"""
+An unrecoverable error occurred while the server was attempting to perform the
+requested action
+"""
 SERVER_DEVICE_FAILURE = const(0x04)
+#: Response is returned to prevent a timeout error from occurring in the client
 ACKNOWLEDGE = const(0x05)
+#: Server is engaged in processing a long duration program command
 SERVER_DEVICE_BUSY = const(0x06)
+#: Server attempted to read record file, but detected a parity error in memory
 MEMORY_PARITY_ERROR = const(0x08)
+"""
+Gateway was unable to allocate an internal communication path from the input
+port to the output port for processing the request
+"""
 GATEWAY_PATH_UNAVAILABLE = const(0x0A)
+#: No response was obtained from the target device
 DEVICE_FAILED_TO_RESPOND = const(0x0B)
 
-# PDU constants
+# Protocol Data Unit (PDU) constants
+#: CRC length
 CRC_LENGTH = const(0x02)
+#: Error code offset
 ERROR_BIAS = const(0x80)
+#: High Data Response length
 RESPONSE_HDR_LENGTH = const(0x02)
+#: Error response length
 ERROR_RESP_LEN = const(0x05)
+#: Fixed response length
 FIXED_RESP_LEN = const(0x08)
+#: Modbus Application Protocol High Data Response length
 MBAP_HDR_LENGTH = const(0x07)
 
+#: CRC16 lookup table
 CRC16_TABLE = (
     0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601,
     0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0,
diff --git a/umodbus/functions.py b/umodbus/functions.py
index c55d006..f1a0269 100644
--- a/umodbus/functions.py
+++ b/umodbus/functions.py
@@ -152,14 +152,14 @@ def write_single_register(register_address: int,
 
 
 def write_multiple_coils(starting_address: int,
-                         value_list: List[int, bool]) -> bytes:
+                         value_list: List[Union[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]
+    :type       value_list:        List[Union[int, bool]]
 
     :returns:   Packed Modbus message
     :rtype:     bytes
@@ -179,12 +179,16 @@ def write_multiple_coils(starting_address: int,
         output_value.append(output)
 
     fmt = 'B' * len(output_value)
+    quantity = len(value_list)
+    byte_count = quantity // 8
+    if quantity % 8:
+        byte_count += 1
 
     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
+                       quantity,
+                       byte_count,
                        *output_value)
 
 
@@ -204,17 +208,18 @@ def write_multiple_registers(starting_address: int,
     :returns:   Packed Modbus message
     :rtype:     bytes
     """
-    quantity = len(register_values)
-
-    if not (1 <= quantity <= 123):
+    if not (1 <= len(register_values) <= 123):
         raise ValueError('Invalid number of registers')
 
+    quantity = len(register_values)
+    byte_count = quantity * 2
     fmt = ('h' if signed else 'H') * quantity
+
     return struct.pack('>BHHB' + fmt,
                        Const.WRITE_MULTIPLE_REGISTERS,
                        starting_address,
                        quantity,
-                       quantity * 2,
+                       byte_count,
                        *register_values)
 
 
@@ -273,9 +278,28 @@ def validate_resp_data(data: bytes,
 def response(function_code: int,
              request_register_addr: int,
              request_register_qty: int,
-             request_data,
-             value_list=None,
-             signed=True):
+             request_data: list,
+             value_list: Optional[list] = None,
+             signed: bool = True) -> bytes:
+    """
+    Construct a Modbus response Protocol Data Unit
+
+    :param      function_code:          The function code
+    :type       function_code:          int
+    :param      request_register_addr:  The request register address
+    :type       request_register_addr:  int
+    :param      request_register_qty:   The request register qty
+    :type       request_register_qty:   int
+    :param      request_data:           The request data
+    :type       request_data:           list
+    :param      value_list:             The values
+    :type       value_list:             Optional[list]
+    :param      signed:                 Indicates if signed
+    :type       signed:                 bool
+
+    :returns:   Protocol data unit
+    :rtype:     bytes
+    """
     if function_code in [Const.READ_COILS, Const.READ_DISCRETE_INPUTS]:
         sectioned_list = [value_list[i:i + 8] for i in range(0, len(value_list), 8)]    # noqa: E501
 
@@ -283,6 +307,7 @@ def response(function_code: int,
         for index, byte in enumerate(sectioned_list):
             # see https://github.com/brainelectronics/micropython-modbus/issues/22
             # output = sum(v << i for i, v in enumerate(byte))
+            # see https://github.com/brainelectronics/micropython-modbus/issues/38
             output = 0
             for bit in byte:
                 output = (output << 1) | bit
@@ -355,13 +380,22 @@ def bytes_to_bool(byte_list: bytes, bit_qty: Optional[int] = 1) -> List[bool]:
     :returns:   Boolean representation
     :rtype:     List[bool]
     """
-    # evil hack for missing keyword support in MicroPython format()
-    fmt = '{:0' + str(bit_qty) + 'b}'
+    bool_list = []
+
+    for index, byte in enumerate(byte_list):
+        this_qty = bit_qty
+
+        if this_qty >= 8:
+            this_qty = 8
+
+        # evil hack for missing keyword support in MicroPython format()
+        fmt = '{:0' + str(this_qty) + 'b}'
+
+        bool_list.extend([bool(int(x)) for x in fmt.format(byte)])
 
-    bool_list = [bool(int(x)) for x in fmt.format(list(byte_list)[0])]
+        bit_qty -= 8
 
-    # invert list due to byte order
-    return bool_list[::-1]
+    return bool_list
 
 
 def to_short(byte_array: bytes, signed: bool = True) -> bytes:
diff --git a/umodbus/modbus.py b/umodbus/modbus.py
index 2ffbc49..a04e476 100644
--- a/umodbus/modbus.py
+++ b/umodbus/modbus.py
@@ -2,7 +2,14 @@
 # -*- coding: UTF-8 -*-
 
 """
-modbus script
+Modbus register abstraction class
+
+Used to add, remove, set and get values or states of a register or coil.
+Additional helper properties and functions like getters for changed registers
+are available as well.
+
+This class is inherited by the Modbus client implementations
+:py:class:`umodbus.serial.ModbusRTU` and :py:class:`umodbus.tcp.ModbusTCP`
 """
 
 # system packages
@@ -11,13 +18,12 @@
 # custom packages
 
 # typing not natively supported on MicroPython
-from .typing import List, Optional
-from .typing import Union
-from .typing import dict_keys
+from .typing import dict_keys, List, Optional, Union
 
 
 class Modbus(object):
-    def __init__(self, itf, addr_list: list):
+    """Modbus register abstraction."""
+    def __init__(self, itf, addr_list: List[int]) -> None:
         self._itf = itf
         self._addr_list = addr_list
 
diff --git a/umodbus/serial.py b/umodbus/serial.py
index 0d19a44..e4fca5a 100644
--- a/umodbus/serial.py
+++ b/umodbus/serial.py
@@ -23,18 +23,36 @@
 from .modbus import Modbus
 
 # typing not natively supported on MicroPython
-from .typing import List, Optional, Union
+from .typing import List, Optional, Tuple, Union
 
 
 class ModbusRTU(Modbus):
+    """
+    Modbus RTU client class
+
+    :param      addr:        The address of this device on the bus
+    :type       addr:        int
+    :param      baudrate:    The baudrate, default 9600
+    :type       baudrate:    int
+    :param      data_bits:   The data bits, default 8
+    :type       data_bits:   int
+    :param      stop_bits:   The stop bits, default 1
+    :type       stop_bits:   int
+    :param      parity:      The parity, default None
+    :type       parity:      Optional[int]
+    :param      pins:        The pins as list [TX, RX]
+    :type       pins:        List[int, int]
+    :param      ctrl_pin:    The control pin
+    :type       ctrl_pin:    int
+    """
     def __init__(self,
-                 addr,
-                 baudrate=9600,
-                 data_bits=8,
-                 stop_bits=1,
-                 parity=None,
-                 pins=None,
-                 ctrl_pin=None):
+                 addr: int,
+                 baudrate: int = 9600,
+                 data_bits: int = 8,
+                 stop_bits: int = 1,
+                 parity: Optional[int] = None,
+                 pins: List[int, int] = None,
+                 ctrl_pin: int = None):
         super().__init__(
             # set itf to Serial object, addr_list to [addr]
             Serial(uart_id=1,
@@ -57,6 +75,24 @@ def __init__(self,
                  parity=None,
                  pins: List[int, int] = None,
                  ctrl_pin: int = None):
+        """
+        Setup Serial/RTU Modbus
+
+        :param      uart_id:     The ID of the used UART
+        :type       uart_id:     int
+        :param      baudrate:    The baudrate, default 9600
+        :type       baudrate:    int
+        :param      data_bits:   The data bits, default 8
+        :type       data_bits:   int
+        :param      stop_bits:   The stop bits, default 1
+        :type       stop_bits:   int
+        :param      parity:      The parity, default None
+        :type       parity:      Optional[int]
+        :param      pins:        The pins as list [TX, RX]
+        :type       pins:        List[int, int]
+        :param      ctrl_pin:    The control pin
+        :type       ctrl_pin:    int
+        """
         self._uart = UART(uart_id,
                           baudrate=baudrate,
                           bits=data_bits,
@@ -79,7 +115,16 @@ def __init__(self,
         else:
             self._t35chars = 1750   # 1750us (approx. 1.75ms)
 
-    def _calculate_crc16(self, data):
+    def _calculate_crc16(self, data: bytearray) -> bytes:
+        """
+        Calculates the CRC16.
+
+        :param      data:        The data
+        :type       data:        bytearray
+
+        :returns:   The crc 16.
+        :rtype:     bytes
+        """
         crc = 0xFFFF
 
         for char in data:
@@ -87,7 +132,16 @@ def _calculate_crc16(self, data):
 
         return struct.pack('<H', crc)
 
-    def _exit_read(self, response):
+    def _exit_read(self, response: bytearray) -> bool:
+        """
+        Return on modbus read error
+
+        :param      response:    The response
+        :type       response:    bytearray
+
+        :returns:   State of basic read response evaluation
+        :rtype:     bool
+        """
         if response[1] >= Const.ERROR_BIAS:
             if len(response) < Const.ERROR_RESP_LEN:
                 return False
@@ -122,7 +176,16 @@ def _uart_read(self) -> bytearray:
 
         return response
 
-    def _uart_read_frame(self, timeout: Optional[int] = None):
+    def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray:
+        """
+        Read a Modbus frame
+
+        :param      timeout:  The timeout
+        :type       timeout:  Optional[int]
+
+        :returns:   Received message
+        :rtype:     bytearray
+        """
         received_bytes = bytearray()
 
         # set timeout to at least twice the time between two frames in case the
@@ -266,6 +329,19 @@ 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)
 
@@ -281,6 +357,19 @@ 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)
@@ -297,7 +386,22 @@ def read_holding_registers(self,
                                slave_addr: int,
                                starting_addr: int,
                                register_qty: int,
-                               signed: bool = True) -> List[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)
@@ -314,7 +418,22 @@ def read_input_registers(self,
                              slave_addr: int,
                              starting_addr: int,
                              register_qty: int,
-                             signed: bool = True) -> List[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)
@@ -364,6 +483,21 @@ def write_single_register(self,
                               register_address: int,
                               register_value: int,
                               signed=True) -> bool:
+        """
+        Update a single register.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      register_address:  The register address
+        :type       register_address:  int
+        :param      register_value:    The register value
+        :type       register_value:    int
+        :param      signed:            Indicates if signed
+        :type       signed:            bool
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
         modbus_pdu = functions.write_single_register(register_address,
                                                      register_value,
                                                      signed)
@@ -383,7 +517,20 @@ def write_single_register(self,
     def write_multiple_coils(self,
                              slave_addr: int,
                              starting_address: int,
-                             output_values: List[int, bool]) -> bool:
+                             output_values: List[Union[int, bool]]) -> bool:
+        """
+        Update multiple coils.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The address of the first coil
+        :type       starting_address:  int
+        :param      output_values:     The output values
+        :type       output_values:     List[Union[int, bool]]
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
         modbus_pdu = functions.write_multiple_coils(
             starting_address=starting_address,
             value_list=output_values)
@@ -404,6 +551,21 @@ def write_multiple_registers(self,
                                  starting_address: int,
                                  register_values: List[int],
                                  signed=True) -> bool:
+        """
+        Update multiple registers.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The starting address
+        :type       starting_address:  int
+        :param      register_values:   The register values
+        :type       register_values:   List[int]
+        :param      signed:            Indicates if signed
+        :type       signed:            bool
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
         modbus_pdu = functions.write_multiple_registers(
             starting_address=starting_address,
             register_values=register_values,
@@ -416,7 +578,8 @@ def write_multiple_registers(self,
             data=resp_data,
             function_code=Const.WRITE_MULTIPLE_REGISTERS,
             address=starting_address,
-            quantity=len(register_values)
+            quantity=len(register_values),
+            signed=signed
         )
 
         return operation_status
@@ -429,6 +592,24 @@ def send_response(self,
                       request_data: list,
                       values: Optional[list] = None,
                       signed: bool = True) -> None:
+        """
+        Send a response to a client.
+
+        :param      slave_addr:             The slave address
+        :type       slave_addr:             int
+        :param      function_code:          The function code
+        :type       function_code:          int
+        :param      request_register_addr:  The request register address
+        :type       request_register_addr:  int
+        :param      request_register_qty:   The request register qty
+        :type       request_register_qty:   int
+        :param      request_data:           The request data
+        :type       request_data:           list
+        :param      values:                 The values
+        :type       values:                 Optional[list]
+        :param      signed:                 Indicates if signed
+        :type       signed:                 bool
+        """
         modbus_pdu = functions.response(
             function_code=function_code,
             request_register_addr=request_register_addr,
@@ -443,12 +624,35 @@ def send_exception_response(self,
                                 slave_addr: int,
                                 function_code: int,
                                 exception_code: int) -> None:
+        """
+        Send an exception response to a client.
+
+        :param      slave_addr:      The slave address
+        :type       slave_addr:      int
+        :param      function_code:   The function code
+        :type       function_code:   int
+        :param      exception_code:  The exception code
+        :type       exception_code:  int
+        """
         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: list, timeout: Optional[int] = None):
+    def get_request(self,
+                    unit_addr_list: List[int],
+                    timeout: Optional[int] = None) -> Union[Request, None]:
+        """
+        Check for request within the specified timeout
+
+        :param      unit_addr_list:  The unit address list
+        :type       unit_addr_list:  Optional[list]
+        :param      timeout:         The timeout
+        :type       timeout:         Optional[int]
+
+        :returns:   A request object or None.
+        :rtype:     Union[Request, None]
+        """
         req = self._uart_read_frame(timeout=timeout)
 
         if len(req) < 8:
diff --git a/umodbus/tcp.py b/umodbus/tcp.py
index 3979b95..380cde7 100644
--- a/umodbus/tcp.py
+++ b/umodbus/tcp.py
@@ -27,6 +27,7 @@
 
 
 class ModbusTCP(Modbus):
+    """Modbus TCP client class"""
     def __init__(self):
         super().__init__(
             # set itf to TCPServer object, addr_list to None
@@ -38,9 +39,25 @@ def bind(self,
              local_ip: str,
              local_port: int = 502,
              max_connections: int = 10) -> None:
+        """
+        Bind IP and port for incomming requests
+
+        :param      local_ip:         IP of this device listening for requests
+        :type       local_ip:         str
+        :param      local_port:       Port of this device
+        :type       local_port:       int
+        :param      max_connections:  Number of maximum connections
+        :type       max_connections:  int
+        """
         self._itf.bind(local_ip, local_port, max_connections)
 
     def get_bound_status(self) -> bool:
+        """
+        Get the IP and port binding status.
+
+        :returns:   The bound status, True if already bound, False otherwise.
+        :rtype:     bool
+        """
         try:
             return self._itf.get_is_bound()
         except Exception:
@@ -81,14 +98,18 @@ def process(self) -> bool:
             # function 04 - read input registers
             reg_type = 'IREGS'
             req_type = 'READ'
-        elif request.function == Const.WRITE_SINGLE_COIL:
+        elif (request.function == Const.WRITE_SINGLE_COIL or
+                request.function == Const.WRITE_MULTIPLE_COILS):
             # Coils (setter+getter) [0, 1]
-            # function 05 - write single register
+            # function 05 - write single coil
+            # function 15 - write multiple coil
             reg_type = 'COILS'
             req_type = 'WRITE'
-        elif request.function == Const.WRITE_SINGLE_REGISTER:
+        elif (request.function == Const.WRITE_SINGLE_REGISTER or
+                request.function == Const.WRITE_MULTIPLE_REGISTERS):
             # Hregs (setter+getter) [0, 65535]
             # function 06 - write holding register
+            # function 16 - write multiple holding register
             reg_type = 'HREGS'
             req_type = 'WRITE'
         else:
@@ -102,7 +123,10 @@ def process(self) -> bool:
 
         return True
 
-    def _create_response(self, request: request, reg_type: str):
+    def _create_response(self,
+                         request: request,
+                         reg_type: str) -> Union[bool, int,
+                                                 List[bool], List[int]]:
         """
         Create a response.
 
@@ -114,10 +138,13 @@ def _create_response(self, request: request, reg_type: str):
         :returns:   Values of this register
         :rtype:     Union[bool, int, List[int], List[bool]]
         """
+        data = []
         if type(self._register_dict[reg_type][request.register_addr]) is list:
-            return self._register_dict[reg_type][request.register_addr]
+            data = self._register_dict[reg_type][request.register_addr]
         else:
-            return [self._register_dict[reg_type][request.register_addr]]
+            data = [self._register_dict[reg_type][request.register_addr]]
+
+        return data[:request.quantity]
 
     def _process_read_access(self, request: request, reg_type: str) -> None:
         """
@@ -149,34 +176,38 @@ def _process_write_access(self, request: request, reg_type: str) -> None:
 
         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()
+                valid_register = True
 
+                if request.function == Const.WRITE_SINGLE_COIL:
+                    val = request.data[0]
+                    if 0x00 < val < 0xFF:
+                        valid_register = False
+                        request.send_exception(Const.ILLEGAL_DATA_VALUE)
+                    else:
+                        val = (val == 0xFF)
+                elif request.function == Const.WRITE_MULTIPLE_COILS:
+                    tmp = int.from_bytes(request.data, "big")
+                    val = [
+                        bool(tmp & (1 << n)) for n in range(request.quantity)
+                    ]
+
+                if valid_register:
                     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]
+                val = list(functions.to_short(byte_array=request.data,
+                                              signed=False))
 
-                request.send_response()
-
-                self.set_hreg(address=address, value=val)
+                if request.function == Const.WRITE_SINGLE_REGISTER:
+                    self.set_hreg(address=address, value=val[0])
+                elif request.function == Const.WRITE_MULTIPLE_REGISTERS:
+                    self.set_hreg(address=address, value=val)
             else:
-                pass
+                # nothing except holding registers or coils can be set
+                request.send_exception(Const.ILLEGAL_FUNCTION)
 
             if valid_register:
+                request.send_response()
                 self._set_changed_register(reg_type=reg_type,
                                            address=address,
                                            value=val)
@@ -185,6 +216,16 @@ def _process_write_access(self, request: request, reg_type: str) -> None:
 
 
 class TCP(object):
+    """
+    TCP class handling socket connections and parsing the Modbus data
+
+    :param      slave_ip:    IP of this device listening for requests
+    :type       slave_ip:    str
+    :param      slave_port:  Port of this device
+    :type       slave_port:  int
+    :param      timeout:     Socket timeout in seconds
+    :type       timeout:     float
+    """
     def __init__(self,
                  slave_ip: str,
                  slave_port: int = 502,
@@ -494,7 +535,20 @@ def write_single_register(self,
     def write_multiple_coils(self,
                              slave_addr: int,
                              starting_address: int,
-                             output_values: List[int, bool]) -> bool:
+                             output_values: List[Union[int, bool]]) -> bool:
+        """
+        Update multiple coils.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The address of the first coil
+        :type       starting_address:  int
+        :param      output_values:     The output values
+        :type       output_values:     List[Union[int, bool]]
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
         modbus_pdu = functions.write_multiple_coils(
             starting_address=starting_address,
             value_list=output_values)
@@ -515,6 +569,21 @@ def write_multiple_registers(self,
                                  starting_address: int,
                                  register_values: List[int],
                                  signed=True) -> bool:
+        """
+        Update multiple registers.
+
+        :param      slave_addr:        The slave address
+        :type       slave_addr:        int
+        :param      starting_address:  The starting address
+        :type       starting_address:  int
+        :param      register_values:   The register values
+        :type       register_values:   List[int]
+        :param      signed:            Indicates if signed
+        :type       signed:            bool
+
+        :returns:   Result of operation
+        :rtype:     bool
+        """
         modbus_pdu = functions.write_multiple_registers(
             starting_address=starting_address,
             register_values=register_values,
@@ -528,27 +597,52 @@ def write_multiple_registers(self,
             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
+            signed=signed
         )
 
         return operation_status
 
 
 class TCPServer(object):
+    """Modbus TCP host class"""
     def __init__(self):
         self._sock = None
         self._client_sock = None
         self._is_bound = False
 
+    @property
+    def is_bound(self) -> bool:
+        """
+        Get the IP and port binding status
+
+        :returns:   True if bound to IP and port, False otherwise
+        :rtype:     bool
+        """
+        return self._is_bound
+
     def get_is_bound(self) -> bool:
+        """
+        Get the IP and port binding status, legacy support.
+
+        :returns:   True if bound to IP and port, False otherwise
+        :rtype:     bool
+        """
         return self._is_bound
 
     def bind(self,
              local_ip: str,
              local_port: int = 502,
              max_connections: int = 10):
+        """
+        Bind IP and port for incomming requests
+
+        :param      local_ip:         IP of this device listening for requests
+        :type       local_ip:         str
+        :param      local_port:       Port of this device
+        :type       local_port:       int
+        :param      max_connections:  Number of maximum connections
+        :type       max_connections:  int
+        """
         if self._client_sock:
             self._client_sock.close()
 
@@ -566,6 +660,14 @@ def bind(self,
         self._is_bound = True
 
     def _send(self, modbus_pdu: bytes, slave_addr: int) -> None:
+        """
+        Send Modbus Protocol Data Unit to slave
+
+        :param      modbus_pdu:  The Modbus Protocol Data Unit
+        :type       modbus_pdu:  bytes
+        :param      slave_addr:  The slave address
+        :type       slave_addr:  int
+        """
         size = len(modbus_pdu)
         fmt = 'B' * size
         adu = struct.pack('>HHHB' + fmt, self._req_tid, 0, size + 1, slave_addr, *modbus_pdu)
@@ -579,6 +681,24 @@ def send_response(self,
                       request_data: list,
                       values: Optional[list] = None,
                       signed: bool = True) -> None:
+        """
+        Send a response to a client.
+
+        :param      slave_addr:             The slave address
+        :type       slave_addr:             int
+        :param      function_code:          The function code
+        :type       function_code:          int
+        :param      request_register_addr:  The request register address
+        :type       request_register_addr:  int
+        :param      request_register_qty:   The request register qty
+        :type       request_register_qty:   int
+        :param      request_data:           The request data
+        :type       request_data:           list
+        :param      values:                 The values
+        :type       values:                 Optional[list]
+        :param      signed:                 Indicates if signed
+        :type       signed:                 bool
+        """
         modbus_pdu = functions.response(function_code,
                                         request_register_addr,
                                         request_register_qty,
@@ -591,13 +711,31 @@ def send_exception_response(self,
                                 slave_addr: int,
                                 function_code: int,
                                 exception_code: int) -> None:
+        """
+        Send an exception response to a client.
+
+        :param      slave_addr:      The slave address
+        :type       slave_addr:      int
+        :param      function_code:   The function code
+        :type       function_code:   int
+        :param      exception_code:  The exception code
+        :type       exception_code:  int
+        """
         modbus_pdu = functions.exception_response(function_code,
                                                   exception_code)
         self._send(modbus_pdu, slave_addr)
 
     def _accept_request(self,
                         accept_timeout: float,
-                        unit_addr_list: list) -> None:
+                        unit_addr_list: list) -> Union[Request, None]:
+        """
+        Accept, read and decode a socket based request
+
+        :param      accept_timeout:  The socket accept timeout
+        :type       accept_timeout:  float
+        :param      unit_addr_list:  The unit address list
+        :type       unit_addr_list:  list
+        """
         self._sock.settimeout(accept_timeout)
         new_client_sock = None
 
@@ -655,7 +793,22 @@ def _accept_request(self,
                                              e.exception_code)
                 return None
 
-    def get_request(self, unit_addr_list=None, timeout=None) -> None:
+    def get_request(self,
+                    unit_addr_list: Optional[list] = None,
+                    timeout: int = None) -> Union[Request, None]:
+        """
+        Check for request within the specified timeout
+
+        :param      unit_addr_list:  The unit address list
+        :type       unit_addr_list:  Optional[list]
+        :param      timeout:         The timeout
+        :type       timeout:         int
+
+        :returns:   A request object or None.
+        :rtype:     Union[Request, None]
+
+        :raises     Exception:       If no socket is configured and bound
+        """
         if self._sock is None:
             raise Exception('Modbus TCP server not bound')
 


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
index a70d4af..23859dd 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -398,23 +398,22 @@ def test_bytes_to_bool(self) -> None:
             (b'\x01', 1, [True]),
 
             (b'\x00', 2, [False, False]),
-            (b'\x01', 2, [True, False]),
-            (b'\x02', 2, [False, True]),
+            (b'\x01', 2, [False, True]),
+            (b'\x02', 2, [True, False]),
             (b'\x03', 2, [True, True]),
 
             (b'\x00', 3, [False, False, False]),
-            (b'\x01', 3, [True, False, False]),
+            (b'\x01', 3, [False, False, True]),
             (b'\x02', 3, [False, True, False]),
-            (b'\x03', 3, [True, True, False]),
-            (b'\x04', 3, [False, False, True]),
+            (b'\x03', 3, [False, True, True]),
+            (b'\x04', 3, [True, False, False]),
             (b'\x05', 3, [True, False, True]),
-            (b'\x06', 3, [False, True, True]),
+            (b'\x06', 3, [True, True, False]),
             (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'\x05', 4, [False, True, False, True]),
+            (b'\x0A', 4, [True, False, True, False]),
             (b'\x0F', 4, [True, True, True, True]),
 
             (b'\x0A', 5, [False, True, False, True, False]),
diff --git a/tests/test_tcp_example.py b/tests/test_tcp_example.py
index 32cdd5e..d5a343b 100644
--- a/tests/test_tcp_example.py
+++ b/tests/test_tcp_example.py
@@ -63,7 +63,7 @@ def test__create_mbap_hdr(self) -> None:
         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
+        # 0x00 0x06 is the length 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)
@@ -237,12 +237,12 @@ def test_read_coils_multiple(self) -> None:
             coil_qty=coil_qty)
 
         self.test_logger.debug(
-            'Status of COIL {} lenght {}: {}, expectation: {}'.format(
+            'Status of COIL {} length {}: {}, 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)
+        self.assertEqual(coil_status, expectation_list)
 
         coil_address = \
             self._register_definitions['COILS']['ANOTHER_EXAMPLE_COIL']['register']     # noqa: E501
@@ -260,13 +260,73 @@ def test_read_coils_multiple(self) -> None:
             coil_qty=coil_qty)
 
         self.test_logger.debug(
-            'Status of COIL {} lenght {}: {}, expectation: {}'.format(
+            'Status of COIL {} length {}: {}, 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']['MANY_COILS']['register']
+        coil_qty = \
+            self._register_definitions['COILS']['MANY_COILS']['len']
+        expectation_list = list(
+            map(bool, self._register_definitions['COILS']['MANY_COILS']['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 {} length {}: {}, 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)
 
+    # Reading coil data bits is reversed
+    # see https://github.com/brainelectronics/micropython-modbus/issues/38
+    #
+    # Read/Write register at some location of definition
+    # see https://github.com/brainelectronics/micropython-modbus/issues/35
+    def test_read_coils_partially(self) -> None:
+        """Test reading coils partially of client"""
+        coil_address = \
+            self._register_definitions['COILS']['MANY_COILS']['register']
+        coil_qty = \
+            self._register_definitions['COILS']['MANY_COILS']['len']
+        expectation_list_full = list(
+            map(bool,
+                self._register_definitions['COILS']['MANY_COILS']['val'])
+        )
+
+        coil_qty_less_8 = randint(2, 7)
+        coil_qty_more_8 = randint(8, coil_qty - 1)
+        possibilities = [coil_qty_less_8, coil_qty_more_8]
+
+        for partial_coil_qty in possibilities:
+            with self.subTest(partial_coil_qty=partial_coil_qty):
+                expectation_list_partial = \
+                    expectation_list_full[:partial_coil_qty]
+
+                coil_status = self._host.read_coils(
+                    slave_addr=self._client_addr,
+                    starting_addr=coil_address,
+                    coil_qty=partial_coil_qty)
+
+                self.test_logger.debug(
+                    'Status of COIL {} length {}/{}: {}, expectation: {}'.
+                    format(coil_address, partial_coil_qty, coil_qty,
+                           coil_status, expectation_list_partial))
+                self.assertIsInstance(coil_status, list)
+                self.assertEqual(len(coil_status), partial_coil_qty)
+                self.assertTrue(all(isinstance(x, bool) for x in coil_status))
+                self.assertEqual(coil_status, expectation_list_partial)
+
     def test_read_discrete_inputs_single(self) -> None:
         """Test reading discrete inputs of client"""
         ist_address = \
@@ -332,8 +392,69 @@ def test_read_discrete_inputs_multiple(self) -> None:
         self.assertTrue(all(isinstance(x, bool) for x in input_status))
         self.assertEqual(input_status, expectation_list)
 
+    # Read/Write register at some location of definition
+    # see https://github.com/brainelectronics/micropython-modbus/issues/35
+    def test_read_discrete_inputs_partially(self) -> None:
+        """Test reading discrete inputs partially of client"""
+        ist_address = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['register']     # noqa: E501
+        input_qty = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['len']
+        expectation_list_full = \
+            self._register_definitions['ISTS']['ANOTHER_EXAMPLE_ISTS']['val']
+
+        input_qty_less = input_qty - 1
+        possibilities = [input_qty_less]
+
+        for partial_input_qty in possibilities:
+            with self.subTest(partial_input_qty=partial_input_qty):
+                expectation_list_partial = \
+                    expectation_list_full[:partial_input_qty]
+
+                input_status = self._host.read_discrete_inputs(
+                    slave_addr=self._client_addr,
+                    starting_addr=ist_address,
+                    input_qty=partial_input_qty)
+
+                self.test_logger.debug(
+                    'Status of IST {} length {}/{}: {}, expectation: {}'.
+                    format(ist_address, partial_input_qty, input_qty,
+                           input_status, expectation_list_partial))
+                self.assertIsInstance(input_status, list)
+                self.assertEqual(len(input_status), partial_input_qty)
+                self.assertTrue(all(isinstance(x, bool) for x in input_status))
+                self.assertEqual(input_status, expectation_list_partial)
+
     def test_read_holding_registers_single(self) -> None:
         """Test reading holding registers of client"""
+        # read holding register with negative value
+        hreg_address = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG_NEGATIVE']['register']    # noqa: E501
+        register_qty = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG_NEGATIVE']['len']
+
+        setup_val = \
+            self._register_definitions['HREGS']['EXAMPLE_HREG_NEGATIVE']['val']
+        # ensure the register value defined in the JSON is really negative
+        self.assertLessEqual(setup_val, -1)
+
+        expectation = (setup_val, )     # tuple is returned
+
+        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)
+
+        # read holding register with positive value
         hreg_address = \
             self._register_definitions['HREGS']['EXAMPLE_HREG']['register']
         register_qty = \
@@ -378,6 +499,40 @@ def test_read_holding_registers_multiple(self) -> None:
         self.assertTrue(all(isinstance(x, int) for x in register_value))
         self.assertEqual(register_value, expectation)
 
+    # Read/Write register at some location of definition
+    # see https://github.com/brainelectronics/micropython-modbus/issues/35
+    def test_read_holding_registers_partially(self) -> None:
+        """Test reading holding registers partially 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_full = tuple(
+            self._register_definitions['HREGS']['ANOTHER_EXAMPLE_HREG']['val']
+        )
+        register_qty_less = register_qty - 1
+        possibilities = [register_qty_less]
+
+        for partial_register_qty in possibilities:
+            with self.subTest(partial_register_qty=partial_register_qty):
+                expectation_tuple_partial = \
+                    expectation_tuple_full[:partial_register_qty]
+
+                register_value = self._host.read_holding_registers(
+                    slave_addr=self._client_addr,
+                    starting_addr=hreg_address,
+                    register_qty=partial_register_qty)
+
+                self.test_logger.debug(
+                    'Status of HREG {} length {}/{}: {}, expectation: {}'.
+                    format(hreg_address, partial_register_qty, register_qty,
+                           register_value, expectation_tuple_partial))
+                self.assertIsInstance(register_value, tuple)
+                self.assertEqual(len(register_value), partial_register_qty)
+                self.assertTrue(all(isinstance(x, int)
+                                for x in register_value))
+                self.assertEqual(register_value, expectation_tuple_partial)
+
     def test_read_input_registers_single(self) -> None:
         """Test reading input registers of client"""
         ireg_address = \
@@ -426,6 +581,41 @@ def test_read_input_registers_multiple(self) -> None:
         self.assertTrue(all(isinstance(x, int) for x in register_value))
         self.assertEqual(register_value, expectation)
 
+    # Read/Write register at some location of definition
+    # see https://github.com/brainelectronics/micropython-modbus/issues/35
+    def test_read_input_registers_partially(self) -> None:
+        """Test reading input registers partially 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_full = tuple(
+            self._register_definitions['IREGS']['ANOTHER_EXAMPLE_IREG']['val']
+        )
+        register_qty_less = register_qty - 1
+        possibilities = [register_qty_less]
+
+        for partial_register_qty in possibilities:
+            with self.subTest(partial_register_qty=partial_register_qty):
+                expectation_tuple_partial = \
+                    expectation_tuple_full[:partial_register_qty]
+
+                register_value = self._host.read_input_registers(
+                    slave_addr=self._client_addr,
+                    starting_addr=ireg_address,
+                    register_qty=partial_register_qty,
+                    signed=False)
+
+                self.test_logger.debug(
+                    'Status of IREG {} length {}/{}: {}, expectation: {}'.
+                    format(ireg_address, partial_register_qty, register_qty,
+                           register_value, expectation_tuple_partial))
+                self.assertIsInstance(register_value, tuple)
+                self.assertEqual(len(register_value), partial_register_qty)
+                self.assertTrue(all(isinstance(x, int)
+                                for x in register_value))
+                self.assertEqual(register_value, expectation_tuple_partial)
+
     def test_reset_client_data(self) -> None:
         """Test resettig client data to default"""
         coil_address = \
@@ -616,13 +806,194 @@ def test_write_single_register(self) -> None:
         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
+        """Test updating multiple coils of client"""
+        # test with less than 8 coils
+        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
+                )
+        )
+
+        #
+        # 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 {} length {}: {}, 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)
+
+        #
+        # Test setting coils to inverted initial states
+        #
+        # update coil states of client with a different than the current state
+        new_coil_vals = [not val for val in expectation_list]
+        expectation_list = new_coil_vals
+
+        operation_status = self._host.write_multiple_coils(
+            slave_addr=self._client_addr,
+            starting_address=coil_address,
+            output_values=new_coil_vals)
+
+        self.test_logger.debug(
+            'Result of setting COIL {} length {} to {}: {}, expectation: {}'.
+            format(
+                coil_address, coil_qty, new_coil_vals, operation_status, True))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # verify setting of states 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 {} length {}: {}, 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)
+
+        # test with more than 8 coils
+        coil_address = \
+            self._register_definitions['COILS']['MANY_COILS']['register']
+        coil_qty = \
+            self._register_definitions['COILS']['MANY_COILS']['len']
+        expectation_list = list(
+            map(bool, self._register_definitions['COILS']['MANY_COILS']['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 {} length {}: {}, 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)
+
+        #
+        # Test setting coils to inverted initial states
+        #
+        # update coil states of client with a different than the current state
+        new_coil_vals = [not val for val in expectation_list]
+        expectation_list = new_coil_vals
+
+        operation_status = self._host.write_multiple_coils(
+            slave_addr=self._client_addr,
+            starting_address=coil_address,
+            output_values=new_coil_vals)
+
+        self.test_logger.debug(
+            'Result of setting COIL {} length {} to {}: {}, expectation: {}'.
+            format(
+                coil_address, coil_qty, new_coil_vals, operation_status, True))
+        self.assertIsInstance(operation_status, bool)
+        self.assertTrue(operation_status)
+
+        # verify setting of states 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 {} length {}: {}, 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))
+        # Reading coil data bits is reversed, see #38
+        # https://github.com/brainelectronics/micropython-modbus/issues/38
+        # self.assertEqual(coil_status, expectation_list)
 
-    @unittest.skip('Test not yet implemented')
     def test_write_multiple_registers(self) -> None:
-        pass
+        """Test updating multiple holding register 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']
+        )
+
+        #
+        # 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 {} 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)
+
+        #
+        # Test setting multiple holding registers to random values
+        #
+        # update holding register of client with a different than the current
+        # value, but at least one negative value
+        new_hreg_vals = (
+            randint(-32768, 32767),
+            randint(-32768, -1),
+            randint(-32768, 32767),
+        )
+
+        operation_status = self._host.write_multiple_registers(
+            slave_addr=self._client_addr,
+            starting_address=hreg_address,
+            register_values=new_hreg_vals,
+            signed=True)
+        self.test_logger.debug(
+            'Result of setting HREG {} length {} to {}: {}, expectation: {}'.
+            format(
+                hreg_address, register_qty, new_hreg_vals, operation_status,
+                new_hreg_vals))
+        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(
+            'Status of HREG {} length {}: {}, expectation: {}'.format(
+                hreg_address, register_qty, register_value, new_hreg_vals))
+        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_vals)
 
     def tearDown(self) -> None:
         """Run after every test method"""