Skip to content

Commit b97de0d

Browse files
Merge pull request #51 from brainelectronics/feature/add-callbacks-to-register-get-and-set-functions
Feature/add callbacks to register get and set functions
2 parents 59e1c6c + 51813d4 commit b97de0d

File tree

6 files changed

+337
-61
lines changed

6 files changed

+337
-61
lines changed

changelog.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
<!-- ## [Unreleased] -->
1616

1717
## Released
18+
## [2.3.0] - 2023-01-03
19+
### Added
20+
- 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
21+
- Example callback usage shown in [TCP client example](examples/tcp_client_example.py)
22+
- Documentation for callback functions in USAGE
23+
24+
### Changed
25+
- Typing hint `Callable` is now subscriptable
26+
1827
## [2.2.0] - 2023-01-03
1928
### Added
2029
- [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
224233
- PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus)
225234

226235
<!-- Links -->
227-
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.2.0...develop
236+
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.0...develop
228237

238+
[2.3.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.0
229239
[2.2.0]: https://github.com/brainelectronics/micropython-modbus/tree/2.2.0
230240
[2.1.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.3
231241
[2.1.2]: https://github.com/brainelectronics/micropython-modbus/tree/2.1.2

docs/USAGE.md

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ The JSON file/dictionary shall follow the following pattern/structure
6161
# the onwards mentioned keys are optional
6262
"description": "Optional description of the coil",
6363
"range": "[0, 1]", # may provide a range of the value, only for documentation purpose
64-
"unit": "BOOL" # may provide a unit of the value, only for documentation purpose
64+
"unit": "BOOL", # may provide a unit of the value, only for documentation purpose
65+
"on_set_cb": my_function, # callback function executed on the client after a new value has been set
66+
"on_get_cb": some_function # callback function executed on the client after a value has been requested
6567
}
6668
},
6769
"HREGS": { # this key shall contain all holding registers
@@ -71,7 +73,9 @@ The JSON file/dictionary shall follow the following pattern/structure
7173
"val": 19, # used to set a register
7274
"description": "Optional description of the holding register",
7375
"range": "[0, 65535]",
74-
"unit": "Hz"
76+
"unit": "Hz",
77+
"on_set_cb": my_function, # callback function executed on the client after a new value has been set
78+
"on_get_cb": some_function # callback function executed on the client after a value has been requested
7579
},
7680
},
7781
"ISTS": { # this key shall contain all static input registers
@@ -81,7 +85,8 @@ The JSON file/dictionary shall follow the following pattern/structure
8185
"val": 0, # used to set a register, not possible for ISTS
8286
"description": "Optional description of the static input register",
8387
"range": "[0, 1]",
84-
"unit": "activated"
88+
"unit": "activated",
89+
"on_get_cb": some_function # callback function executed on the client after a value has been requested
8590
}
8691
},
8792
"IREGS": { # this key shall contain all input registers
@@ -91,7 +96,8 @@ The JSON file/dictionary shall follow the following pattern/structure
9196
"val": 60001, # used to set a register, not possible for IREGS
9297
"description": "Optional description of the static input register",
9398
"range": "[0, 65535]",
94-
"unit": "millivolt"
99+
"unit": "millivolt",
100+
"on_get_cb": some_function # callback function executed on the client after a value has been requested
95101
}
96102
}
97103
}
@@ -222,6 +228,92 @@ of the register. In case of the PWM output register example of the
222228
[optional range key](#optional-range) the recommended value for this key could
223229
be `percent`.
224230

231+
###### Optional callbacks
232+
233+
The optional keys `on_set_cb` and `on_get_cb` can be used to register a
234+
callback function on client side which is executed **after** a new value has
235+
been set or **before** the response of a requested register value has been
236+
sent.
237+
238+
```{note}
239+
Getter callbacks can be registered for all registers with the `on_get_cb`
240+
parameter whereas the `on_set_cb` parameter is only available for coils and
241+
holding registers as only those can be set by a external host.
242+
```
243+
244+
The callback function shall have the following three parameters:
245+
246+
| Parameter | Type | Description |
247+
| ---------- | ------ | -------------------|
248+
| `reg_type` | string | Type of register. `COILS`, `HREGS`, `ISTS`, `IREGS` |
249+
| `address` | int | Type of register. `COILS`, `HREGS`, `ISTS`, `IREGS` |
250+
| `val` | Union[bool, int, Tuple[bool], Tuple[int], List[bool], List[int]] | Current value of register |
251+
252+
This example functions registered for e.g. coil 123 will output the following
253+
content after the coil has been requested and afterwards set to a different
254+
value
255+
256+
```python
257+
def my_coil_set_cb(reg_type, address, val):
258+
print('Custom callback, called on setting {} at {} to: {}'.
259+
format(reg_type, address, val))
260+
261+
262+
def my_coil_get_cb(reg_type, address, val):
263+
print('Custom callback, called on getting {} at {}, currently: {}'.
264+
format(reg_type, address, val))
265+
266+
267+
# assuming the client specific setup (port/ID settings, network connections,
268+
# UART setup) has already been done
269+
# Check the provided examples for further details
270+
271+
# define some registers, for simplicity only a single coil is used
272+
register_definitions = {
273+
"COILS": {
274+
"EXAMPLE_COIL": {
275+
"register": 123,
276+
"len": 1,
277+
"val": 0,
278+
"on_get_cb": my_coil_get_cb,
279+
"on_set_cb": my_coil_set_cb
280+
}
281+
}
282+
}
283+
284+
print('Setting up registers ...')
285+
# use the defined values of each register type provided by register_definitions
286+
client.setup_registers(registers=register_definitions)
287+
# alternatively use dummy default values (True for bool regs, 999 otherwise)
288+
# client.setup_registers(registers=register_definitions, use_default_vals=True)
289+
print('Register setup done')
290+
291+
while True:
292+
try:
293+
result = client.process()
294+
except KeyboardInterrupt:
295+
print('KeyboardInterrupt, stopping TCP client...')
296+
break
297+
except Exception as e:
298+
print('Exception during execution: {}'.format(e))
299+
```
300+
301+
```
302+
Setting up registers ...
303+
Register setup done
304+
Custom callback, called on getting COILS at 123, currently: False
305+
Custom callback, called on setting COILS at 123 to: True
306+
```
307+
308+
In case only specific registers shall be enhanced with callbacks the specific
309+
functions can be used individually instead of setting up all registers with the
310+
[`setup_registers`](umodbus.modbus.Modbus.setup_registers) function.
311+
312+
- [`add_coil`](umodbus.modbus.Modbus.add_coil)
313+
- [`add_hreg`](umodbus.modbus.Modbus.add_hreg)
314+
- [`add_ist`](umodbus.modbus.Modbus.add_ist)
315+
- [`add_ireg`](umodbus.modbus.Modbus.add_ireg)
316+
225317
### Register usage
226318

227319
This section describes the usage of the following implemented functions

examples/tcp_client_example.py

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,60 @@
7272
if not is_bound:
7373
client.bind(local_ip=local_ip, local_port=tcp_port)
7474

75+
76+
def my_coil_set_cb(reg_type, address, val):
77+
print('Custom callback, called on setting {} at {} to: {}'.
78+
format(reg_type, address, val))
79+
80+
81+
def my_coil_get_cb(reg_type, address, val):
82+
print('Custom callback, called on getting {} at {}, currently: {}'.
83+
format(reg_type, address, val))
84+
85+
86+
def my_holding_register_set_cb(reg_type, address, val):
87+
print('Custom callback, called on setting {} at {} to: {}'.
88+
format(reg_type, address, val))
89+
90+
91+
def my_holding_register_get_cb(reg_type, address, val):
92+
print('Custom callback, called on getting {} at {}, currently: {}'.
93+
format(reg_type, address, val))
94+
95+
96+
def my_discrete_inputs_register_get_cb(reg_type, address, val):
97+
print('Custom callback, called on getting {} at {}, currently: {}'.
98+
format(reg_type, address, val))
99+
100+
101+
def my_inputs_register_get_cb(reg_type, address, val):
102+
# usage of global isn't great, but okay for an example
103+
global client
104+
105+
print('Custom callback, called on getting {} at {}, currently: {}'.
106+
format(reg_type, address, val))
107+
108+
# any operation should be as short as possible to avoid response timeouts
109+
new_val = val[0] + 1
110+
111+
# It would be also possible to read the latest ADC value at this time
112+
# adc = machine.ADC(12) # check MicroPython port specific syntax
113+
# new_val = adc.read()
114+
115+
client.set_ireg(address=address, value=new_val)
116+
print('Incremented current value by +1 before sending response')
117+
118+
119+
def reset_data_registers_cb(reg_type, address, val):
120+
# usage of global isn't great, but okay for an example
121+
global client
122+
global register_definitions
123+
124+
print('Resetting register data to default values ...')
125+
client.setup_registers(registers=register_definitions)
126+
print('Default values restored')
127+
128+
75129
# commond slave register setup, to be used with the Master example above
76130
register_definitions = {
77131
"COILS": {
@@ -116,6 +170,27 @@
116170
with open('registers/example.json', 'r') as file:
117171
register_definitions = json.load(file)
118172

173+
# add callbacks for different Modbus functions
174+
# each register can have a different callback
175+
# coils and holding register support callbacks for set and get
176+
register_definitions['COILS']['EXAMPLE_COIL']['on_set_cb'] = my_coil_set_cb
177+
register_definitions['COILS']['EXAMPLE_COIL']['on_get_cb'] = my_coil_get_cb
178+
register_definitions['HREGS']['EXAMPLE_HREG']['on_set_cb'] = \
179+
my_holding_register_set_cb
180+
register_definitions['HREGS']['EXAMPLE_HREG']['on_get_cb'] = \
181+
my_holding_register_get_cb
182+
183+
# discrete inputs and input registers support only get callbacks as they can't
184+
# be set externally
185+
register_definitions['ISTS']['EXAMPLE_ISTS']['on_get_cb'] = \
186+
my_discrete_inputs_register_get_cb
187+
register_definitions['IREGS']['EXAMPLE_IREG']['on_get_cb'] = \
188+
my_inputs_register_get_cb
189+
190+
# reset all registers back to their default value with a callback
191+
register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['on_set_cb'] = \
192+
reset_data_registers_cb
193+
119194
print('Setting up registers ...')
120195
# use the defined values of each register type provided by register_definitions
121196
client.setup_registers(registers=register_definitions)
@@ -125,17 +200,9 @@
125200

126201
print('Serving as TCP client on {}:{}'.format(local_ip, tcp_port))
127202

128-
reset_data_register = \
129-
register_definitions['COILS']['RESET_REGISTER_DATA_COIL']['register']
130-
131203
while True:
132204
try:
133205
result = client.process()
134-
if reset_data_register in client.coils:
135-
if client.get_coil(address=reset_data_register):
136-
print('Resetting register data to default values ...')
137-
client.setup_registers(registers=register_definitions)
138-
print('Default values restored')
139206
except KeyboardInterrupt:
140207
print('KeyboardInterrupt, stopping TCP client...')
141208
break

tests/test_tcp_example.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,10 @@ def test_read_input_registers_single(self) -> None:
542542
expectation = \
543543
(self._register_definitions['IREGS']['EXAMPLE_IREG']['val'], )
544544

545+
# due to value increment by registered callback in
546+
# tcp_client_example.py, see #31 and #51
547+
expectation = (expectation[0] + 1, )
548+
545549
register_value = self._host.read_input_registers(
546550
slave_addr=self._client_addr,
547551
starting_addr=ireg_address,

0 commit comments

Comments
 (0)