Skip to content

Commit 13c47bd

Browse files
authored
el modbus examples (#1)
1 parent 0f9288e commit 13c47bd

10 files changed

+2955
-2
lines changed

README.md

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,106 @@
1-
# modbus-scripts
2-
Examples of interaction with Enapter devices over Modbus communication protocol
1+
## Introduction
2+
3+
This repository contains examples of interaction with Enapter devices over
4+
Modbus communication protocol. Writing holding registers allows to execute
5+
commands (e.g. reboot), set specific parameters or update configuration.
6+
7+
Reading registers (both holding and inputs) allows to get current hardware status
8+
(e.g. switches states, H2 production parameters, timings, etc.), current configuration,
9+
detect configuration problems.
10+
11+
All information regarding registers, events (errors/warnings), etc. is available at [Enapter Handbook](https://handbook.enapter.com).
12+
13+
### Requirements
14+
15+
Python is used as programming language, version >= 3.10 is required.
16+
Please refer to the actual [downloads](https://www.python.org/downloads/) and [documentation](https://www.python.org/doc/).
17+
18+
Git is required to clone the repository.
19+
Please refer to the actual [downloads](https://www.git-scm.com/downloads) and [documentation](https://www.git-scm.com/doc).
20+
21+
[pyModbusTCP](https://pypi.org/project/pyModbusTCP/0.2.1/) package is required.
22+
Please refer to the actual documentation regarding [usage of virtual environments](https://docs.python.org/3/library/venv.html) and [packages installation](https://packaging.python.org/en/latest/tutorials/installing-packages/).
23+
24+
### Running scripts
25+
26+
Please refer to the actual [documentation](https://docs.python.org/3/using/cmdline.html) regarding general information about running Python scripts.
27+
28+
Each script requires two parameters - Modbus IP address and Modbus port.
29+
IP address is required parameter, default port is _502_.
30+
31+
**Running script with default port:**
32+
```
33+
python3 <path_to_script>/<script_name>.py --modbus-ip <address>
34+
```
35+
36+
**Running script with custom port:**
37+
```
38+
python3 <path_to_script>/<script_name>.py --modbus-ip <address> --modbus-port <port>
39+
```
40+
41+
### Scripts description
42+
43+
**_read_el_control_board_serial.py_**
44+
45+
Read and decode control board serial number input (6) to a human-readable string value (e.g. '9E25E695-A66A-61DD-6570-50DB4E73652D').
46+
47+
**_read_el_device_model.py_**
48+
49+
Read and decode device model input register (0) to a human-readable string value (e.g. 'EL21', 'EL40', etc.).
50+
51+
**_read_el_errors.py_**
52+
53+
Read and decode errors input register (832) to a list of human-readable strings with error name and hex
54+
value (e.g. 'WR_20 (0x3194)'). Since new firmwares may add new events, UNKNOWN errors may be identified by hex value.
55+
56+
**_read_el_params.py_**
57+
58+
Read and decode current hardware parameters:
59+
- system state (input, 18)
60+
- uptime (input, 22)
61+
- total H2 production (input, 1006)
62+
- production rate (holding, 1002)
63+
- high electrolyte level switch (input, 7000)
64+
- very high electrolyte level switch (input, 7001)
65+
- low electrolyte level switch (input, 7002)
66+
- medium electrolyte level switch (input, 7003)
67+
- electrolyte tank high pressure switch (input, 7004)
68+
- electronic compartment high temperature Switch (input, 7007)
69+
- chassis water presence switch (input, 7009)).
70+
71+
**_run_el_maintenance.py_**
72+
73+
Interactive script to perform maintenance on EL2.1/4.x by following the instructions in console.
74+
75+
**ATTENTION!** Maintenance requires manual actions with electrolyser such as electrolyte draining,
76+
flushing (for 4.x) and refilling.
77+
78+
If script is terminated for some reason (e.g. due to network failure), in most cases it can be re-run and
79+
maintenance will continue.
80+
81+
Only refilling is performed in case of first maintenance (from factory state).
82+
83+
**_write_el_production_rate.py_**
84+
85+
- Read current value of the production rate percent holding register (1002)
86+
- Write random value in 90-99 range
87+
- Read register again to check that it contains new value
88+
89+
- write_el_reboot.py
90+
91+
- Write 1 to reboot holding register (4)
92+
- Wait until electrolyser is rebooted
93+
- Read state input register (1200)
94+
95+
**_write_el_syslog_skip_priority.py_**
96+
97+
- Read current value of the log skip priority holding register (4042)
98+
- Check that there is no other configuration in progress (read configuration in progress input register (4000))
99+
- Begin configuration (write 1 to the configuration begin holding register (4000))
100+
- Ensure that configuration source is Modbus (read configuration over modbus input register (4001))
101+
- Write random value in 0-6 range (excluding current value) to the log skip priority holding register (4042)
102+
- Check that configuration is OK (read configuration last result input register (4002))
103+
- Read log skip priority holding register (4042) again to check that it contains new value
104+
105+
NOTICE. Log skip priority holding register (4042) has int32 type, so it may contain any value in the appropriate
106+
range. Values less than 0 are considered as DISABLE_LOGGING (0), values greater than 6 are considered as ALL_MESSAGES (6).

read_el_control_board_serial.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright 2024 Enapter
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an
8+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
9+
# or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import argparse
14+
import sys
15+
import uuid
16+
17+
from typing import Final
18+
19+
try:
20+
from pyModbusTCP import client, utils
21+
22+
except ImportError:
23+
print(
24+
'No pyModbusTCP module installed.\n.'
25+
'1. Create virtual environment\n'
26+
'2. Run \'pip install pyModbusTCP==0.2.1\''
27+
)
28+
29+
raise
30+
31+
32+
# Supported Python version
33+
MIN_PYTHON_VERSION: Final[tuple[int, int]] = (3, 10)
34+
35+
# Register address
36+
CONTROL_BOARD_SERIAL_INPUT: Final[int] = 6
37+
38+
39+
def parse_args() -> argparse.Namespace:
40+
parser = argparse.ArgumentParser(
41+
description='Reading EL control board serial with Modbus'
42+
)
43+
44+
parser.add_argument(
45+
'--modbus-ip', '-i', help='Modbus IP address', required=True
46+
)
47+
48+
parser.add_argument(
49+
'--modbus-port', '-p', help='Modbus port', type=int, default=502
50+
)
51+
52+
return parser.parse_args()
53+
54+
55+
def main() -> None:
56+
if sys.version_info < MIN_PYTHON_VERSION:
57+
raise RuntimeError(
58+
f'Python version >='
59+
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
60+
f' required'
61+
)
62+
63+
args: argparse.Namespace = parse_args()
64+
65+
modbus_client: client.ModbusClient = client.ModbusClient(
66+
host=args.modbus_ip, port=args.modbus_port
67+
)
68+
69+
try:
70+
# Read control board serial input register, address is 6. Register type
71+
# is uint128, so number of registers to read is 128 / 16 = 8.
72+
raw_board_serial: list[int] = modbus_client.read_input_registers(
73+
reg_addr=CONTROL_BOARD_SERIAL_INPUT, reg_nb=8
74+
)
75+
76+
print(f'Got raw control board serial data: {raw_board_serial}')
77+
78+
# Convert raw response to single int value. pyModbusTCP utils has no
79+
# built-in method for uint128, combining with 'manual' conversion.
80+
long_long_list: list[int] = utils.word_list_to_long(
81+
val_list=raw_board_serial, long_long=True
82+
)
83+
84+
converted_board_serial: int = (
85+
long_long_list[0] << 64 | long_long_list[1]
86+
)
87+
88+
print(
89+
f'Got converted int value: {converted_board_serial}'
90+
)
91+
92+
# Decode converted int to human-readable mainboard id which in fact is
93+
# string representation of UUID.
94+
decoded_board_serial: str = str(
95+
uuid.UUID(int=converted_board_serial)
96+
).upper()
97+
98+
print(
99+
f'Got decoded human-readable serial number: {decoded_board_serial}'
100+
)
101+
102+
except Exception as e:
103+
# If something went wrong, we can access Modbus error/exception info.
104+
# For example, in case of connection problems, reading register will
105+
# return None and script will fail with error while data converting,
106+
# but real problem description will be stored in client.
107+
print(f'Exception occurred: {e}')
108+
print(f'Modbus error: {modbus_client.last_error_as_txt}')
109+
print(f'Modbus exception: {modbus_client.last_except_as_txt}')
110+
111+
raise
112+
113+
114+
if __name__ == '__main__':
115+
main()

read_el_device_model.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright 2024 Enapter
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an
8+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
9+
# or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import argparse
14+
import sys
15+
16+
from enum import StrEnum
17+
from typing import Any, Final, Self
18+
19+
try:
20+
from pyModbusTCP import client, utils
21+
22+
except ImportError:
23+
print(
24+
'No pyModbusTCP module installed.\n.'
25+
'1. Create virtual environment\n'
26+
'2. Run \'pip install pyModbusTCP==0.2.1\''
27+
)
28+
29+
raise
30+
31+
32+
# Supported Python version
33+
MIN_PYTHON_VERSION: Final[tuple[int, int]] = (3, 10)
34+
35+
# Register address
36+
DEVICE_MODEL_INPUT: Final[int] = 0
37+
38+
39+
class DeviceModel(StrEnum):
40+
"""
41+
Values for ProjectId input register (0).
42+
"""
43+
44+
UNKNOWN = 'UNKNOWN'
45+
46+
# Specific value for EL21.
47+
EL21 = 'EL21'
48+
49+
# Specific values for EL40.
50+
EL40 = 'EL40'
51+
ES40 = 'ES40'
52+
53+
# Specific value for EL41.
54+
ES41 = 'ES41'
55+
56+
@classmethod
57+
def _missing_(cls, value: Any) -> Self:
58+
return cls.UNKNOWN
59+
60+
61+
def parse_args() -> argparse.Namespace:
62+
parser = argparse.ArgumentParser(
63+
description='Reading EL device model with Modbus'
64+
)
65+
66+
parser.add_argument(
67+
'--modbus-ip', '-i', help='Modbus IP address', required=True
68+
)
69+
70+
parser.add_argument(
71+
'--modbus-port', '-p', help='Modbus port', type=int, default=502
72+
)
73+
74+
return parser.parse_args()
75+
76+
77+
def main() -> None:
78+
if sys.version_info < MIN_PYTHON_VERSION:
79+
raise RuntimeError(
80+
f'Python version >='
81+
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
82+
f' required'
83+
)
84+
85+
args: argparse.Namespace = parse_args()
86+
87+
modbus_client: client.ModbusClient = client.ModbusClient(
88+
host=args.modbus_ip, port=args.modbus_port
89+
)
90+
91+
try:
92+
# Read ProjectId input register, address is 0. Register type is uint32,
93+
# so number of registers to read is 32 / 16 = 2.
94+
raw_device_model: list[int] = modbus_client.read_input_registers(
95+
reg_addr=DEVICE_MODEL_INPUT, reg_nb=2
96+
)
97+
98+
print(f'Got raw device model data: {raw_device_model}')
99+
100+
# Convert raw response to single int value with pyModbusTCP utils.
101+
converted_device_model: int = utils.word_list_to_long(
102+
val_list=raw_device_model
103+
)[0]
104+
105+
print(f'Got converted int value: {converted_device_model}')
106+
107+
# Decode converted int to human-readable device model.
108+
decoded_device_model: DeviceModel = DeviceModel(
109+
bytes.fromhex(f'{converted_device_model:x}').decode()
110+
)
111+
112+
print(
113+
f'Got decoded human-readable device model: {decoded_device_model}'
114+
)
115+
116+
except Exception as e:
117+
# If something went wrong, we can access Modbus error/exception info.
118+
# For example, in case of connection problems, reading register will
119+
# return None and script will fail with error while data converting,
120+
# but real problem description will be stored in client.
121+
print(f'Exception occurred: {e}')
122+
print(f'Modbus error: {modbus_client.last_error_as_txt}')
123+
print(f'Modbus exception: {modbus_client.last_except_as_txt}')
124+
125+
raise
126+
127+
128+
if __name__ == '__main__':
129+
main()

0 commit comments

Comments
 (0)