Skip to content

Commit 32966e6

Browse files
authored
dryer modbus examples (#6)
1 parent 6d533d2 commit 32966e6

11 files changed

+476
-1
lines changed

dryer/read_dry_errors.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 IntEnum
17+
from typing import Any, Final, Self
18+
19+
try:
20+
from pyModbusTCP import client
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+
DRYER_ERRORS_INPUT: Final[int] = 6000
37+
38+
# Error message usually indicating that DCN is disabled
39+
SLAVE_DEVICE_FAILURE: Final[str] = 'slave device failure'
40+
41+
42+
class DryerError(IntEnum):
43+
"""
44+
Enum values for bitmask of modbus dryer_errors register (6000).
45+
"""
46+
UNKNOWN = -1
47+
48+
TT00_INVALID_VALUE = 0
49+
TT01_INVALID_VALUE = 1
50+
TT02_INVALID_VALUE = 2
51+
TT03_INVALID_VALUE = 3
52+
TT00_VALUE_GROWTH_NOT_ENOUGH = 4
53+
TT01_VALUE_GROWTH_NOT_ENOUGH = 5
54+
TT02_VALUE_GROWTH_NOT_ENOUGH = 6
55+
TT03_VALUE_GROWTH_NOT_ENOUGH = 7
56+
PS00_TRIGGERED = 8
57+
PS01_TRIGGERED = 9
58+
F100_INVALID_RPM = 10
59+
F101_INVALID_RPM = 11
60+
F102_INVALID_RPM = 12
61+
PT00_INVALID_VALUE = 13
62+
PT01_INVALID_VALUE = 14
63+
64+
@classmethod
65+
def _missing_(cls, value: Any) -> Self:
66+
return cls.UNKNOWN
67+
68+
69+
def parse_args() -> argparse.Namespace:
70+
parser = argparse.ArgumentParser(
71+
description='Reading DRY errors with Modbus'
72+
)
73+
74+
parser.add_argument(
75+
'--modbus-ip', '-i', help='Modbus IP address', required=True
76+
)
77+
78+
parser.add_argument(
79+
'--modbus-port', '-p', help='Modbus port', type=int, default=502
80+
)
81+
82+
return parser.parse_args()
83+
84+
85+
def main() -> None:
86+
if sys.version_info < MIN_PYTHON_VERSION:
87+
raise RuntimeError(
88+
f'Python version >='
89+
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
90+
f' required'
91+
)
92+
93+
args: argparse.Namespace = parse_args()
94+
95+
modbus_client: client.ModbusClient = client.ModbusClient(
96+
host=args.modbus_ip, port=args.modbus_port
97+
)
98+
99+
try:
100+
# Read dryer errors input register, address is 6000. Register type is
101+
# uint16, so number of registers to read is 16 / 16 = 1.
102+
raw_errors_data: list[int] = modbus_client.read_input_registers(
103+
reg_addr=DRYER_ERRORS_INPUT, reg_nb=1
104+
)
105+
106+
print(f'Got raw dryer errors data: {raw_errors_data}')
107+
108+
if errors := raw_errors_data[0]:
109+
# Value is not 0, converting int value to bitmask.
110+
bitmask: str = '{:016b}'.format(errors)[::-1]
111+
112+
print(f'Got dryer errors bitmask: {bitmask}')
113+
114+
decoded_errors: list[str] = [
115+
DryerError(bit_number).name for bit_number in [
116+
index for index, bit in enumerate(bitmask) if int(bit)
117+
]
118+
]
119+
120+
print(
121+
f'Got decoded errors: {", ".join(decoded_errors)}\nErrors'
122+
f' description is available at https://handbook.enapter.com'
123+
)
124+
125+
else:
126+
# Value is 0.
127+
print('There are no errors')
128+
129+
except Exception as e:
130+
# If something went wrong, we can access Modbus error/exception info.
131+
# For example, in case of connection problems, reading register will
132+
# return None and script will fail with error while data converting,
133+
# but real problem description will be stored in client.
134+
print(f'Exception occurred: {e}')
135+
print(f'Modbus error: {modbus_client.last_error_as_txt}')
136+
print(f'Modbus exception: {modbus_client.last_except_as_txt}')
137+
138+
if SLAVE_DEVICE_FAILURE in modbus_client.last_except_as_txt:
139+
print('Please check that DCN is enabled')
140+
141+
raise
142+
143+
144+
if __name__ == '__main__':
145+
main()

dryer/read_dry_params.py

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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 IntEnum
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+
# Registers addresses
36+
DRYER_PT00_INPUT: Final[int] = 6010
37+
DRYER_PT01_INPUT: Final[int] = 6012
38+
DRYER_STATE_INPUT: Final[int] = 6021
39+
40+
# Error message usually indicating that DCN is disabled
41+
SLAVE_DEVICE_FAILURE: Final[str] = 'slave device failure'
42+
43+
44+
class DryerState(IntEnum):
45+
"""
46+
Enum values for dryer state input register (6021).
47+
"""
48+
UNKNOWN = -1
49+
50+
NONE = 0
51+
WAITING_FOR_POWER = 257
52+
STOPPED_BY_USER = 259
53+
STARTING = 260
54+
STANDBY = 262
55+
WAITING_FOR_PRESSURE = 263
56+
IDLE = 265
57+
DRYING_0 = 513
58+
COOLING_0 = 514
59+
SWITCHING_0 = 515
60+
PRESSURIZING_0 = 516
61+
FINALIZING_0 = 517
62+
DRYING_1 = 769
63+
COOLING_1 = 770
64+
SWITCHING_1 = 771
65+
PRESSURIZING_1 = 772
66+
FINALIZING_1 = 773
67+
ERROR = 1281
68+
BYPASS = 1537
69+
BYPASS_1 = 1793
70+
BYPASS_2 = 2049
71+
MAINTENANCE = 2305
72+
EXPERT = 2561
73+
FSR_WAIT_BEGIN = 2817
74+
FSR_WAIT_CONFIRM = 2818
75+
FSR_WAIT_END = 2819
76+
FSR_DECLINED = 2820
77+
IDCN_WAIT_START = 3073
78+
IDCN_WAIT_CONFIRM = 3074
79+
IDCN_BEGIN = 3075
80+
IDCN_COMMIT = 3076
81+
IDCN_COMMIT_ACK = 3077
82+
IDCN_WAIT_SYNCED = 3078
83+
IDCN_SYNCED = 3079
84+
IDCN_DECLINED = 3080
85+
IDCN_CANCEL = 3081
86+
OTA_FW = 3328
87+
88+
@classmethod
89+
def _missing_(cls, value: Any) -> Self:
90+
return cls.UNKNOWN
91+
92+
93+
def parse_args() -> argparse.Namespace:
94+
parser = argparse.ArgumentParser(
95+
description='Reading DRY params with Modbus'
96+
)
97+
98+
parser.add_argument(
99+
'--modbus-ip', '-i', help='Modbus IP address', required=True
100+
)
101+
102+
parser.add_argument(
103+
'--modbus-port', '-p', help='Modbus port', type=int, default=502
104+
)
105+
106+
return parser.parse_args()
107+
108+
109+
def _read_input_registers(
110+
modbus_client: client.ModbusClient, address: int, count: int
111+
) -> list[int]:
112+
"""
113+
Read input registers.
114+
"""
115+
return modbus_client.read_input_registers(reg_addr=address, reg_nb=count)
116+
117+
118+
def main() -> None:
119+
if sys.version_info < MIN_PYTHON_VERSION:
120+
raise RuntimeError(
121+
f'Python version >='
122+
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
123+
f' required'
124+
)
125+
126+
args: argparse.Namespace = parse_args()
127+
128+
modbus_client: client.ModbusClient = client.ModbusClient(
129+
host=args.modbus_ip, port=args.modbus_port
130+
)
131+
132+
try:
133+
# Read dryer state input register, address is 6021. Register type is
134+
# uint16, so number of registers to read is 16 / 16 = 1.
135+
raw_dryer_state: list[int] = modbus_client.read_input_registers(
136+
reg_addr=DRYER_STATE_INPUT, reg_nb=1
137+
)
138+
139+
print(f'Got raw dryer state data: {raw_dryer_state}')
140+
141+
print(
142+
f'Got decoded human-readable dryer state:'
143+
f' {DryerState(raw_dryer_state[0]).name}'
144+
)
145+
146+
# Read 6010 and 6012 input registers. Each register type is float32, so
147+
# number of registers to read is 32 / 16 = 2.
148+
for register, description in (
149+
(DRYER_PT00_INPUT, 'PT00 pressure'),
150+
(DRYER_PT01_INPUT, 'PT01 pressure')
151+
):
152+
raw_pressure_data: list[int] = (
153+
_read_input_registers(
154+
modbus_client=modbus_client, address=register, count=2
155+
)
156+
)
157+
158+
# Convert raw response to single float value with pyModbusTCP
159+
# utils.
160+
converted_pressure_value: float = utils.decode_ieee(
161+
val_int=utils.word_list_to_long(
162+
val_list=raw_pressure_data
163+
)[0]
164+
)
165+
166+
print(f'Got {description} in bar: {converted_pressure_value}')
167+
168+
except Exception as e:
169+
# If something went wrong, we can access Modbus error/exception info.
170+
# For example, in case of connection problems, reading register will
171+
# return None and script will fail with error while data converting,
172+
# but real problem description will be stored in client.
173+
print(f'Exception occurred: {e}')
174+
print(f'Modbus error: {modbus_client.last_error_as_txt}')
175+
print(f'Modbus exception: {modbus_client.last_except_as_txt}')
176+
177+
if SLAVE_DEVICE_FAILURE in modbus_client.last_except_as_txt:
178+
print('Please check that DCN is enabled')
179+
180+
raise
181+
182+
183+
if __name__ == '__main__':
184+
main()

0 commit comments

Comments
 (0)