Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dryer modbus examples #6

Merged
merged 1 commit into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions dryer/read_dry_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright 2024 Enapter
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import sys

from enum import IntEnum
from typing import Any, Final, Self

try:
from pyModbusTCP import client

except ImportError:
print(
'No pyModbusTCP module installed.\n.'
'1. Create virtual environment\n'
'2. Run \'pip install pyModbusTCP==0.2.1\''
)

raise


# Supported Python version
MIN_PYTHON_VERSION: Final[tuple[int, int]] = (3, 10)

# Register address
DRYER_ERRORS_INPUT: Final[int] = 6000

# Error message usually indicating that DCN is disabled
SLAVE_DEVICE_FAILURE: Final[str] = 'slave device failure'


class DryerError(IntEnum):
"""
Enum values for bitmask of modbus dryer_errors register (6000).
"""
UNKNOWN = -1

TT00_INVALID_VALUE = 0
TT01_INVALID_VALUE = 1
TT02_INVALID_VALUE = 2
TT03_INVALID_VALUE = 3
TT00_VALUE_GROWTH_NOT_ENOUGH = 4
TT01_VALUE_GROWTH_NOT_ENOUGH = 5
TT02_VALUE_GROWTH_NOT_ENOUGH = 6
TT03_VALUE_GROWTH_NOT_ENOUGH = 7
PS00_TRIGGERED = 8
PS01_TRIGGERED = 9
F100_INVALID_RPM = 10
F101_INVALID_RPM = 11
F102_INVALID_RPM = 12
PT00_INVALID_VALUE = 13
PT01_INVALID_VALUE = 14

@classmethod
def _missing_(cls, value: Any) -> Self:
return cls.UNKNOWN


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Reading DRY errors with Modbus'
)

parser.add_argument(
'--modbus-ip', '-i', help='Modbus IP address', required=True
)

parser.add_argument(
'--modbus-port', '-p', help='Modbus port', type=int, default=502
)

return parser.parse_args()


def main() -> None:
if sys.version_info < MIN_PYTHON_VERSION:
raise RuntimeError(
f'Python version >='
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
f' required'
)

args: argparse.Namespace = parse_args()

modbus_client: client.ModbusClient = client.ModbusClient(
host=args.modbus_ip, port=args.modbus_port
)

try:
# Read dryer errors input register, address is 6000. Register type is
# uint16, so number of registers to read is 16 / 16 = 1.
raw_errors_data: list[int] = modbus_client.read_input_registers(
reg_addr=DRYER_ERRORS_INPUT, reg_nb=1
)

print(f'Got raw dryer errors data: {raw_errors_data}')

if errors := raw_errors_data[0]:
# Value is not 0, converting int value to bitmask.
bitmask: str = '{:016b}'.format(errors)[::-1]

print(f'Got dryer errors bitmask: {bitmask}')

decoded_errors: list[str] = [
DryerError(bit_number).name for bit_number in [
index for index, bit in enumerate(bitmask) if int(bit)
]
]

print(
f'Got decoded errors: {", ".join(decoded_errors)}\nErrors'
f' description is available at https://handbook.enapter.com'
)

else:
# Value is 0.
print('There are no errors')

except Exception as e:
# If something went wrong, we can access Modbus error/exception info.
# For example, in case of connection problems, reading register will
# return None and script will fail with error while data converting,
# but real problem description will be stored in client.
print(f'Exception occurred: {e}')
print(f'Modbus error: {modbus_client.last_error_as_txt}')
print(f'Modbus exception: {modbus_client.last_except_as_txt}')

if SLAVE_DEVICE_FAILURE in modbus_client.last_except_as_txt:
print('Please check that DCN is enabled')

raise


if __name__ == '__main__':
main()
184 changes: 184 additions & 0 deletions dryer/read_dry_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Copyright 2024 Enapter
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import sys

from enum import IntEnum
from typing import Any, Final, Self

try:
from pyModbusTCP import client, utils

except ImportError:
print(
'No pyModbusTCP module installed.\n.'
'1. Create virtual environment\n'
'2. Run \'pip install pyModbusTCP==0.2.1\''
)

raise


# Supported Python version
MIN_PYTHON_VERSION: Final[tuple[int, int]] = (3, 10)

# Registers addresses
DRYER_PT00_INPUT: Final[int] = 6010
DRYER_PT01_INPUT: Final[int] = 6012
DRYER_STATE_INPUT: Final[int] = 6021

# Error message usually indicating that DCN is disabled
SLAVE_DEVICE_FAILURE: Final[str] = 'slave device failure'


class DryerState(IntEnum):
"""
Enum values for dryer state input register (6021).
"""
UNKNOWN = -1

NONE = 0
WAITING_FOR_POWER = 257
STOPPED_BY_USER = 259
STARTING = 260
STANDBY = 262
WAITING_FOR_PRESSURE = 263
IDLE = 265
DRYING_0 = 513
COOLING_0 = 514
SWITCHING_0 = 515
PRESSURIZING_0 = 516
FINALIZING_0 = 517
DRYING_1 = 769
COOLING_1 = 770
SWITCHING_1 = 771
PRESSURIZING_1 = 772
FINALIZING_1 = 773
ERROR = 1281
BYPASS = 1537
BYPASS_1 = 1793
BYPASS_2 = 2049
MAINTENANCE = 2305
EXPERT = 2561
FSR_WAIT_BEGIN = 2817
FSR_WAIT_CONFIRM = 2818
FSR_WAIT_END = 2819
FSR_DECLINED = 2820
IDCN_WAIT_START = 3073
IDCN_WAIT_CONFIRM = 3074
IDCN_BEGIN = 3075
IDCN_COMMIT = 3076
IDCN_COMMIT_ACK = 3077
IDCN_WAIT_SYNCED = 3078
IDCN_SYNCED = 3079
IDCN_DECLINED = 3080
IDCN_CANCEL = 3081
OTA_FW = 3328

@classmethod
def _missing_(cls, value: Any) -> Self:
return cls.UNKNOWN


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Reading DRY params with Modbus'
)

parser.add_argument(
'--modbus-ip', '-i', help='Modbus IP address', required=True
)

parser.add_argument(
'--modbus-port', '-p', help='Modbus port', type=int, default=502
)

return parser.parse_args()


def _read_input_registers(
modbus_client: client.ModbusClient, address: int, count: int
) -> list[int]:
"""
Read input registers.
"""
return modbus_client.read_input_registers(reg_addr=address, reg_nb=count)


def main() -> None:
if sys.version_info < MIN_PYTHON_VERSION:
raise RuntimeError(
f'Python version >='
f' {".".join(str(version) for version in MIN_PYTHON_VERSION)} is'
f' required'
)

args: argparse.Namespace = parse_args()

modbus_client: client.ModbusClient = client.ModbusClient(
host=args.modbus_ip, port=args.modbus_port
)

try:
# Read dryer state input register, address is 6021. Register type is
# uint16, so number of registers to read is 16 / 16 = 1.
raw_dryer_state: list[int] = modbus_client.read_input_registers(
reg_addr=DRYER_STATE_INPUT, reg_nb=1
)

print(f'Got raw dryer state data: {raw_dryer_state}')

print(
f'Got decoded human-readable dryer state:'
f' {DryerState(raw_dryer_state[0]).name}'
)

# Read 6010 and 6012 input registers. Each register type is float32, so
# number of registers to read is 32 / 16 = 2.
for register, description in (
(DRYER_PT00_INPUT, 'PT00 pressure'),
(DRYER_PT01_INPUT, 'PT01 pressure')
):
raw_pressure_data: list[int] = (
_read_input_registers(
modbus_client=modbus_client, address=register, count=2
)
)

# Convert raw response to single float value with pyModbusTCP
# utils.
converted_pressure_value: float = utils.decode_ieee(
val_int=utils.word_list_to_long(
val_list=raw_pressure_data
)[0]
)

print(f'Got {description} in bar: {converted_pressure_value}')

except Exception as e:
# If something went wrong, we can access Modbus error/exception info.
# For example, in case of connection problems, reading register will
# return None and script will fail with error while data converting,
# but real problem description will be stored in client.
print(f'Exception occurred: {e}')
print(f'Modbus error: {modbus_client.last_error_as_txt}')
print(f'Modbus exception: {modbus_client.last_except_as_txt}')

if SLAVE_DEVICE_FAILURE in modbus_client.last_except_as_txt:
print('Please check that DCN is enabled')

raise


if __name__ == '__main__':
main()
Loading
Loading