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

Issue:
Feature/add callbacks to register get and set functions
### Added
- Custom callback functions can be registered on client (ModbusRTU or ModbusTCP) side with new parameters `on_set_cb` and `on_get_cb` available from [modbus.py](umodbus/modbus.py) functions `add_coil` and `add_hreg`. Functions `add_ist` and `add_ireg` support only `on_get_cb`, see #31
- Example callback usage shown in [TCP client example](examples/tcp_client_example.py)
- Documentation for callback functions in USAGE

### Changed
- Typing hint `Callable` is now subscriptable
It would be nice if there was an example of ModBus Slave talking to real hardware
This could be a GPIO or I2C device.

I'm guessing that the correct approach is to inherit `ModbusRTU` (or `ModbusTCP`)  and then override functions like `set_ist`, `set_coil`, `get_coil`, etc.
It would be nice if there was an example of ModBus Slave talking to real hardware
This could be a GPIO or I2C device.

I'm guessing that the correct approach is to inherit `ModbusRTU` (or `ModbusTCP`)  and then override functions like `set_ist`, `set_coil`, `get_coil`, etc.
Feature/add callbacks to register get and set functions
### Added
- Custom callback functions can be registered on client (ModbusRTU or ModbusTCP) side with new parameters `on_set_cb` and `on_get_cb` available from [modbus.py](umodbus/modbus.py) functions `add_coil` and `add_hreg`. Functions `add_ist` and `add_ireg` support only `on_get_cb`, see #31
- Example callback usage shown in [TCP client example](examples/tcp_client_example.py)
- Documentation for callback functions in USAGE

### Changed
- Typing hint `Callable` is now subscriptable


Discussion / hints:

Hi @jd-boyd 

Yes, that will be nice. 

Currently, how are you doing to get/set on the real hardware from the Slave, from/to the `register_definitions?` I'm still do not have reached this part, I still doing tests with the `register_definitions,` like [this example](https://github.com/brainelectronics/micropython-modbus/blob/develop/examples/set-example.json), without to integrate with real hardware. I imagine that the best solution for now is a loop reading/writing from real hardware and updating/checking the `register_definitions.` As `register_definitions` is a dict, always that loop find a different value on the hardware, update the `register_definitions` dict, that will be what `Slave` will read. The same for write new value on the hardware, loop check if are there a different value on the `register_definitions` (that was set by Master), and write on the hardware. I really do not know what is the best solution.

Big thank you to @beyonlo. You are the best support! 

@jd-boyd I think you want something like using a coil as a GPIO, whenever it is physically modified the register value should be updated and if it modified by a Modbus Host, it's physical state should change. I might implement something like that on the weekend
Hi @jd-boyd 

Yes, that will be nice. 

Currently, how are you doing to get/set on the real hardware from the Slave, from/to the `register_definitions?` I'm still do not have reached this part, I still doing tests with the `register_definitions,` like [this example](https://github.com/brainelectronics/micropython-modbus/blob/develop/examples/set-example.json), without to integrate with real hardware. I imagine that the best solution for now is a loop reading/writing from real hardware and updating/checking the `register_definitions.` As `register_definitions` is a dict, always that loop find a different value on the hardware, update the `register_definitions` dict, that will be what `Slave` will read. The same for write new value on the hardware, loop check if are there a different value on the `register_definitions` (that was set by Master), and write on the hardware. I really do not know what is the best solution.

Big thank you to @beyonlo. You are the best support! 

@jd-boyd I think you want something like using a coil as a GPIO, whenever it is physically modified the register value should be updated and if it modified by a Modbus Host, it's physical state should change. I might implement something like that on the weekend


Fix patch:
diff --git a/changelog.md b/changelog.md
index fcf8deb..9b76697 100644
--- a/changelog.md
+++ b/changelog.md
@@ -15,6 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 <!-- ## [Unreleased] -->
 
 ## Released
+## [2.3.0] - 2023-01-03
+### Added
+- Custom callback functions can be registered on client (ModbusRTU or ModbusTCP) side with new parameters `on_set_cb` and `on_get_cb` available from [modbus.py](umodbus/modbus.py) functions `add_coil` and `add_hreg`. Functions `add_ist` and `add_ireg` support only `on_get_cb`, see #31
+- Example callback usage shown in [TCP client example](examples/tcp_client_example.py)
+- Documentation for callback functions in USAGE
+
+### Changed
+- Typing hint `Callable` is now subscriptable
+
 ## [2.2.0] - 2023-01-03
 ### Added
 - [Fake machine module](fakes/machine.py) with UART and Pin class to be used on Unix MicroPython container for RTU tests and examples, see #47
@@ -224,8 +233,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.2.0...develop
+[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.0...develop
 
+[2.3.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.0
 [2.2.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.2.0
 [2.1.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.3
 [2.1.2]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.2
diff --git a/docs/USAGE.md b/docs/USAGE.md
index bbec012..b521511 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -61,7 +61,9 @@ The JSON file/dictionary shall follow the following pattern/structure
             # the onwards mentioned keys are optional
             "description": "Optional description of the coil",
             "range": "[0, 1]",  # may provide a range of the value, only for documentation purpose
-            "unit": "BOOL"      # may provide a unit of the value, only for documentation purpose
+            "unit": "BOOL",     # may provide a unit of the value, only for documentation purpose
+            "on_set_cb": my_function,   # callback function executed on the client after a new value has been set
+            "on_get_cb": some_function  # callback function executed on the client after a value has been requested
         }
     },
     "HREGS": {          # this key shall contain all holding registers
@@ -71,7 +73,9 @@ The JSON file/dictionary shall follow the following pattern/structure
             "val": 19,          # used to set a register
             "description": "Optional description of the holding register",
             "range": "[0, 65535]",
-            "unit": "Hz"
+            "unit": "Hz",
+            "on_set_cb": my_function,   # callback function executed on the client after a new value has been set
+            "on_get_cb": some_function  # callback function executed on the client after a value has been requested
         },
     },
     "ISTS": {           # this key shall contain all static input registers
@@ -81,7 +85,8 @@ The JSON file/dictionary shall follow the following pattern/structure
             "val": 0,           # used to set a register, not possible for ISTS
             "description": "Optional description of the static input register",
             "range": "[0, 1]",
-            "unit": "activated"
+            "unit": "activated",
+            "on_get_cb": some_function  # callback function executed on the client after a value has been requested
         }
     },
     "IREGS": {          # this key shall contain all input registers
@@ -91,7 +96,8 @@ The JSON file/dictionary shall follow the following pattern/structure
             "val": 60001,       # used to set a register, not possible for IREGS
             "description": "Optional description of the static input register",
             "range": "[0, 65535]",
-            "unit": "millivolt"
+            "unit": "millivolt",
+            "on_get_cb": some_function  # callback function executed on the client after a value has been requested
         }
     }
 }
@@ -222,6 +228,92 @@ 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`.
 
+###### Optional callbacks
+
+The optional keys `on_set_cb` and `on_get_cb` can be used to register a
+callback function on client side which is executed **after** a new value has
+been set or **before** the response of a requested register value has been
+sent.
+
+```{note}
+Getter callbacks can be registered for all registers with the `on_get_cb`
+parameter whereas the `on_set_cb` parameter is only available for coils and
+holding registers as only those can be set by a external host.
+```
+
+The callback function shall have the following three parameters:
+
+| Parameter  | Type | Description |
+| ---------- | ------ | -------------------|
+| `reg_type` | string | Type of register. `COILS`, `HREGS`, `ISTS`, `IREGS` |
+| `address`  | int | Type of register. `COILS`, `HREGS`, `ISTS`, `IREGS` |
+| `val`      | Union[bool, int, Tuple[bool], Tuple[int], List[bool], List[int]] | Current value of register |
+
+This example functions registered for e.g. coil 123 will output the following
+content after the coil has been requested and afterwards set to a different
+value
+
+```python
+def my_coil_set_cb(reg_type, address, val):
+    print('Custom callback, called on setting {} at {} to: {}'.
+          format(reg_type, address, val))
+
+
+def my_coil_get_cb(reg_type, address, val):
+    print('Custom callback, called on getting {} at {}, currently: {}'.
+          format(reg_type, address, val))
+
+
+# assuming the client  specific setup (port/ID settings, network connections,
+# UART setup) has already been done
+# Check the provided examples for further details
+
+# define some registers, for simplicity only a single coil is used
+register_definitions = {
+    "COILS": {
+        "EXAMPLE_COIL": {
+            "register": 123,
+            "len": 1,
+            "val": 0,
+            "on_get_cb": my_coil_get_cb,
+            "on_set_cb": my_coil_set_cb
+        }
+    }
+}
+
+print('Setting up registers ...')
+# use the defined values of each register type provided by register_definitions
+client.setup_registers(registers=register_definitions)
+# alternatively use dummy default values (True for bool regs, 999 otherwise)
+# client.setup_registers(registers=register_definitions, use_default_vals=True)
+print('Register setup done')
+
+while True:
+    try:
+        result = client.process()
+    except KeyboardInterrupt:
+        print('KeyboardInterrupt, stopping TCP client...')
+        break
+    except Exception as e:
+        print('Exception during execution: {}'.format(e))
+```
+
+```
+Setting up registers ...
+Register setup done
+Custom callback, called on getting COILS at 123, currently: False
+Custom callback, called on setting COILS at 123 to: True
+```
+
+In case only specific registers shall be enhanced with callbacks the specific
+functions can be used individually instead of setting up all registers with the
+[`setup_registers`](umodbus.modbus.Modbus.setup_registers) function.
+
+ - [`add_coil`](umodbus.modbus.Modbus.add_coil)
+ - [`add_hreg`](umodbus.modbus.Modbus.add_hreg)
+ - [`add_ist`](umodbus.modbus.Modbus.add_ist)
+ - [`add_ireg`](umodbus.modbus.Modbus.add_ireg)
+
 ### Register usage
 
 This section describes the usage of the following implemented functions
diff --git a/examples/tcp_client_example.py b/examples/tcp_client_example.py
index 789cf11..5a1cdbd 100644
--- a/examples/tcp_client_example.py
+++ b/examples/tcp_client_example.py
@@ -72,6 +72,60 @@
 if not is_bound:
     client.bind(local_ip=local_ip, local_port=tcp_port)
 
+
+def my_coil_set_cb(reg_type, address, val):
+    print('Custom callback, called on setting {} at {} to: {}'.
+          format(reg_type, address, val))
+
+
+def my_coil_get_cb(reg_type, address, val):
+    print('Custom callback, called on getting {} at {}, currently: {}'.
+          format(reg_type, address, val))
+
+
+def my_holding_register_set_cb(reg_type, address, val):
+    print('Custom callback, called on setting {} at {} to: {}'.
+          format(reg_type, address, val))
+
+
+def my_holding_register_get_cb(reg_type, address, val):
+    print('Custom callback, called on getting {} at {}, currently: {}'.
+          format(reg_type, address, val))
+
+
+def my_discrete_inputs_register_get_cb(reg_type, address, val):
+    print('Custom callback, called on getting {} at {}, currently: {}'.
+          format(reg_type, address, val))
+
+
+def my_inputs_register_get_cb(reg_type, address, val):
+    # usage of global isn't great, but okay for an example
+    global client
+
+    print('Custom callback, called on getting {} at {}, currently: {}'.
+          format(reg_type, address, val))
+
+    # any operation should be as short as possible to avoid response timeouts
+    new_val = val[0] + 1
+
+    # It would be also possible to read the latest ADC value at this time
+    # adc = machine.ADC(12)     # check MicroPython port specific syntax
+    # new_val = adc.read()
+
+    client.set_ireg(address=address, value=new_val)
+    print('Incremented current value by +1 before sending response')
+
+
+def reset_data_registers_cb(reg_type, address, val):
+    # usage of global isn't great, but okay for an example
+    global client
+    global register_definitions
+
+    print('Resetting register data to default values ...')
+    client.setup_registers(registers=register_definitions)
+    print('Default values restored')
+
+
 # commond slave register setup, to be used with the Master example above
 register_definitions = {
     "COILS": {
@@ -116,6 +170,27 @@
     with open('registers/example.json', 'r') as file:
         register_definitions = json.load(file)
 
+# add callbacks for different Modbus functions
+# each register can have a different callback
+# coils and holding register support callbacks for set and get
+register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb
+register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb
+register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \
+    my_holding_register_set_cb
+register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \
+    my_holding_register_get_cb
+
+# discrete inputs and input registers support only get callbacks as they can't
+# be set externally
+register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \
+    my_discrete_inputs_register_get_cb
+register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \
+    my_inputs_register_get_cb
+
+# reset all registers back to their default value with a callback
+register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \
+    reset_data_registers_cb
+
 print('Setting up registers ...')
 # use the defined values of each register type provided by register_definitions
 client.setup_registers(registers=register_definitions)
@@ -125,17 +200,9 @@
 
 print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port))
 
-reset_data_register = \
-    register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']
-
 while True:
     try:
         result = client.process()
-        if reset_data_register in client.coils:
-            if client.get_coil(address=reset_data_register):
-                print('Resetting register data to default values ...')
-                client.setup_registers(registers=register_definitions)
-                print('Default values restored')
     except KeyboardInterrupt:
         print('KeyboardInterrupt, stopping TCP client...')
         break
diff --git a/umodbus/modbus.py b/umodbus/modbus.py
index 4e9dacd..e0e245c 100644
--- a/umodbus/modbus.py
+++ b/umodbus/modbus.py
@@ -21,7 +21,7 @@
 from .common import Request
 
 # typing not natively supported on MicroPython
-from .typing import dict_keys, List, Optional, Union
+from .typing import Callable, dict_keys, List, Optional, Union
 
 
 class Modbus(object):
@@ -106,8 +106,7 @@ def process(self) -> bool:
 
     def _create_response(self,
                          request: Request,
-                         reg_type: str) -> Union[bool, int,
-                                                 List[bool], List[int]]:
+                         reg_type: str) -> Union[List[bool], List[int]]:
         """
         Create a response.
 
@@ -117,13 +116,15 @@ def _create_response(self,
         :type       reg_type:  str
 
         :returns:   Values of this register
-        :rtype:     Union[bool, int, List[int], List[bool]]
+        :rtype:     Union[List[bool], List[int]]
         """
         data = []
-        if type(self._register_dict[reg_type][request.register_addr]) is list:
-            data = self._register_dict[reg_type][request.register_addr]
+        address = request.register_addr
+
+        if type(self._register_dict[reg_type][address]['val']) is list:
+            data = self._register_dict[reg_type][address]['val']
         else:
-            data = [self._register_dict[reg_type][request.register_addr]]
+            data = [self._register_dict[reg_type][address]['val']]
 
         return data[:request.quantity]
 
@@ -136,7 +137,16 @@ def _process_read_access(self, request: Request, reg_type: str) -> None:
         :param      reg_type:  The register type
         :type       reg_type:  str
         """
-        if request.register_addr in self._register_dict[reg_type]:
+        address = request.register_addr
+
+        if address in self._register_dict[reg_type]:
+
+            if self._register_dict[reg_type][address].get('on_get_cb', 0):
+                vals = self._create_response(request=request,
+                                             reg_type=reg_type)
+                _cb = self._register_dict[reg_type][address]['on_get_cb']
+                _cb(reg_type=reg_type, address=address, val=vals)
+
             vals = self._create_response(request=request, reg_type=reg_type)
             request.send_response(vals)
         else:
@@ -192,23 +202,42 @@ def _process_write_access(self, request: Request, reg_type: str) -> None:
                 self._set_changed_register(reg_type=reg_type,
                                            address=address,
                                            value=val)
+                if self._register_dict[reg_type][address].get('on_set_cb', 0):
+                    _cb = self._register_dict[reg_type][address]['on_set_cb']
+                    _cb(reg_type=reg_type, address=address, val=val)
         else:
             request.send_exception(Const.ILLEGAL_DATA_ADDRESS)
 
     def add_coil(self,
                  address: int,
-                 value: Union[bool, List[bool]] = False) -> None:
+                 value: Union[bool, List[bool]] = False,
+                 on_set_cb: Callable[[str, int, Union[List[bool], List[int]]],
+                                     None] = None,
+                 on_get_cb: Callable[[str, int, Union[List[bool], List[int]]],
+                                     None] = None) -> None:
         """
         Add a coil to the modbus register dictionary.
 
-        :param      address:  The address (ID) of the register
-        :type       address:  int
-        :param      value:    The default value
-        :type       value:    Union[bool, List[bool]], optional
+        :param      address:    The address (ID) of the register
+        :type       address:    int
+        :param      value:      The default value
+        :type       value:      Union[bool, List[bool]], optional
+        :param      on_set_cb:  Callback on setting the coil
+        :type       on_set_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
+        :param      on_get_cb:  Callback on getting the coil
+        :type       on_get_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
         """
         self._set_reg_in_dict(reg_type='COILS',
                               address=address,
-                              value=value)
+                              value=value,
+                              on_set_cb=on_set_cb,
+                              on_get_cb=on_get_cb)
 
     def remove_coil(self, address: int) -> Union[None, bool, List[bool]]:
         """
@@ -260,18 +289,28 @@ def coils(self) -> dict_keys:
         """
         return self._get_regs_of_dict(reg_type='COILS')
 
-    def add_hreg(self, address: int, value: Union[int, List[int]] = 0) -> None:
+    def add_hreg(self,
+                 address: int,
+                 value: Union[int, List[int]] = 0,
+                 on_set_cb: Callable[[str, int, List[int]], None] = None,
+                 on_get_cb: Callable[[str, int, List[int]], None] = None) -> None:
         """
         Add a holding register to the modbus register dictionary.
 
-        :param      address:  The address (ID) of the register
-        :type       address:  int
-        :param      value:    The default value
-        :type       value:    Union[int, List[int]], optional
+        :param      address:    The address (ID) of the register
+        :type       address:    int
+        :param      value:      The default value
+        :type       value:      Union[int, List[int]], optional
+        :param      on_set_cb:  Callback on setting the holding register
+        :type       on_set_cb:  Callable[[str, int, List[int]], None]
+        :param      on_get_cb:  Callback on getting the holding register
+        :type       on_get_cb:  Callable[[str, int, List[int]], None]
         """
         self._set_reg_in_dict(reg_type='HREGS',
                               address=address,
-                              value=value)
+                              value=value,
+                              on_set_cb=on_set_cb,
+                              on_get_cb=on_get_cb)
 
     def remove_hreg(self, address: int) -> Union[None, int, List[int]]:
         """
@@ -323,22 +362,30 @@ def hregs(self) -> dict_keys:
 
     def add_ist(self,
                 address: int,
-                value: Union[bool, List[bool]] = False) -> None:
+                value: Union[bool, List[bool]] = False,
+                on_get_cb: Callable[[str, int, Union[List[bool], List[int]]],
+                                    None] = None) -> None:
         """
         Add a discrete input register to the modbus register dictionary.
 
-        :param      address:  The address (ID) of the register
-        :type       address:  int
-        :param      value:    The default value
-        :type       value:    bool or list of bool, optional
+        :param      address:    The address (ID) of the register
+        :type       address:    int
+        :param      value:      The default value
+        :type       value:      bool or list of bool, optional
+        :param      on_get_cb:  Callback on getting the discrete input register
+        :type       on_get_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
         """
         self._set_reg_in_dict(reg_type='ISTS',
                               address=address,
-                              value=value)
+                              value=value,
+                              on_get_cb=on_get_cb)
 
     def remove_ist(self, address: int) -> Union[None, bool, List[bool]]:
         """
-        Remove a holding register from the modbus register dictionary.
+        Remove a discrete input register from the modbus register dictionary.
 
         :param      address:  The address (ID) of the register
         :type       address:  int
@@ -384,18 +431,28 @@ def ists(self) -> dict_keys:
         """
         return self._get_regs_of_dict(reg_type='ISTS')
 
-    def add_ireg(self, address: int, value: Union[int, List[int]] = 0) -> None:
+    def add_ireg(self,
+                 address: int,
+                 value: Union[int, List[int]] = 0,
+                 on_get_cb: Callable[[str, int, Union[List[bool], List[int]]],
+                                     None] = None) -> None:
         """
         Add an input register to the modbus register dictionary.
 
-        :param      address:  The address (ID) of the register
-        :type       address:  int
-        :param      value:    The default value
-        :type       value:    Union[int, List[int]], optional
+        :param      address:    The address (ID) of the register
+        :type       address:    int
+        :param      value:      The default value
+        :type       value:      Union[int, List[int]], optional
+        :param      on_get_cb:  Callback on getting the input register
+        :type       on_get_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
         """
         self._set_reg_in_dict(reg_type='IREGS',
                               address=address,
-                              value=value)
+                              value=value,
+                              on_get_cb=on_get_cb)
 
     def remove_ireg(self, address: int) -> Union[None, int, List[int]]:
         """
@@ -448,16 +505,32 @@ def iregs(self) -> dict_keys:
     def _set_reg_in_dict(self,
                          reg_type: str,
                          address: int,
-                         value: Union[bool, int, List[bool], List[int]]) -> None:
+                         value: Union[bool, int, List[bool], List[int]],
+                         on_set_cb: Callable[[str, int, Union[List[bool],
+                                                              List[int]]],
+                                             None] = None,
+                         on_get_cb: Callable[[str, int, Union[List[bool],
+                                                              List[int]]],
+                                             None] = None) -> None:
         """
         Set the register value in the dictionary of registers.
 
-        :param      reg_type:  The register type
-        :type       reg_type:  str
-        :param      address:   The address (ID) of the register
-        :type       address:   int
-        :param      value:     The default value
-        :type       value:     Union[bool, int, List[bool], List[int]]
+        :param      reg_type:   The register type
+        :type       reg_type:   str
+        :param      address:    The address (ID) of the register
+        :type       address:    int
+        :param      value:      The default value
+        :type       value:      Union[bool, int, List[bool], List[int]]
+        :param      on_set_cb:  Callback on setting the register
+        :type       on_get_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
+        :param      on_get_cb:  Callback on getting the register
+        :type       on_get_cb:  Callable[
+                                    [str, int, Union[List[bool], List[int]]],
+                                    None
+                                ]
 
         :raise      KeyError:  No register at specified address found
         :returns:   Register value
@@ -467,7 +540,27 @@ def _set_reg_in_dict(self,
             raise KeyError('{} is not a valid register type of {}'.
                            format(reg_type, self._available_register_types))
 
-        self._register_dict[reg_type][address] = value
+        data = {'val': value}
+
+        # if the register exists already in the register dict a "set_*"
+        # function might have called this functions
+        if address in self._register_dict[reg_type]:
+            # try to get the (already) registered callback function from the
+            # register dict of this address with the this time call function
+            # parameter callback value as fallback
+            on_set_cb = self._register_dict[reg_type][address].get('on_set_cb',
+                                                                   on_set_cb)
+
+            on_get_cb = self._register_dict[reg_type][address].get('on_get_cb',
+                                                                   on_get_cb)
+
+        if callable(on_set_cb):
+            data['on_set_cb'] = on_set_cb
+
+        if callable(on_get_cb):
+            data['on_get_cb'] = on_get_cb
+
+        self._register_dict[reg_type][address] = data
 
     def _remove_reg_from_dict(self,
                               reg_type: str,
@@ -510,7 +603,7 @@ def _get_reg_in_dict(self,
                            format(reg_type, self._available_register_types))
 
         if address in self._register_dict[reg_type]:
-            return self._register_dict[reg_type][address]
+            return self._register_dict[reg_type][address]['val']
         else:
             raise KeyError('No {} available for the register address {}'.
                            format(reg_type, address))
@@ -656,18 +749,27 @@ def setup_registers(self,
                         else:
                             value = val['val']
 
+                        on_set_cb = val.get('on_set_cb', None)
+                        on_get_cb = val.get('on_get_cb', None)
+
                         if reg_type == 'COILS':
                             self.add_coil(address=address,
-                                          value=value)
+                                          value=value,
+                                          on_set_cb=on_set_cb,
+                                          on_get_cb=on_get_cb)
                         elif reg_type == 'HREGS':
                             self.add_hreg(address=address,
-                                          value=value)
+                                          value=value,
+                                          on_set_cb=on_set_cb,
+                                          on_get_cb=on_get_cb)
                         elif reg_type == 'ISTS':
                             self.add_ist(address=address,
-                                         value=value)
+                                         value=value,
+                                         on_get_cb=on_get_cb)   # only getter
                         elif reg_type == 'IREGS':
                             self.add_ireg(address=address,
-                                          value=value)
+                                          value=value,
+                                          on_get_cb=on_get_cb)  # only getter
                         else:
                             # invalid register type
                             pass
diff --git a/umodbus/typing.py b/umodbus/typing.py
index 7b14274..ba64efa 100644
--- a/umodbus/typing.py
+++ b/umodbus/typing.py
@@ -88,8 +88,9 @@ class Collection:
     pass
 
 
-class Callable:
-    pass
+# class Callable:
+#     pass
+Callable = _subscriptable
 
 
 class AbstractSet:


Test patch:
diff --git a/tests/test_tcp_example.py b/tests/test_tcp_example.py
index 73ccefc..b5d12c8 100644
--- a/tests/test_tcp_example.py
+++ b/tests/test_tcp_example.py
@@ -542,6 +542,10 @@ def test_read_input_registers_single(self) -> None:
         expectation = \
             (self._register_definitions['IREGS']['EXAMPLE_IREG']['val'], )
 
+        # due to value increment by registered callback in
+        # tcp_client_example.py, see #31 and #51
+        expectation = (expectation[0] + 1, )
+
         register_value = self._host.read_input_registers(
             slave_addr=self._client_addr,
             starting_addr=ireg_address,