From 5e8e49da49f73114eb12643a8267a6972e76b9b0 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Wed, 20 Jan 2021 16:20:40 +0100 Subject: [PATCH] Modbus old draft added --- graphit_modbus/__init__.py | 138 ++++++++++++++++++++++++++ graphit_modbus/hitachi.py | 124 +++++++++++++++++++++++ graphit_modbus/transport.py | 190 ++++++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+) create mode 100644 graphit_modbus/__init__.py create mode 100644 graphit_modbus/hitachi.py create mode 100644 graphit_modbus/transport.py diff --git a/graphit_modbus/__init__.py b/graphit_modbus/__init__.py new file mode 100644 index 0000000..46a4c07 --- /dev/null +++ b/graphit_modbus/__init__.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + +__all__ = ('DatatypesProtocol') + + +import random +import struct + +from typing import Mapping + +from .transport import ClientInterface + + +class DatatypesProtocol(): + + def __init__(self, client: ClientInterface, coils, registers) -> None: + self.__client = client + self.__coils = coils + self.__registers = registers + + async def read_coil(self, coil: int) -> bool: + assert coil in self.__coils, 'unknown coil' + + res = await self.__client.read_coils(coil - 1, 1) + return res[0] + + async def write_coil(self, coil: int, value: bool) -> None: + assert coil in self.__coils, 'unknown coil' + + await self.__client.write_single_coil(coil - 1, value) + + async def read_register(self, register: int) -> float: + assert register in self.__registers, 'unknown register' + + rw = self.__registers[register]['rw'] + assert rw is 'r' or rw is 'rw', 'register is not readable' + + rType = self.__registers[register]['type'] + rConf = self.__registers[register]['conf'] + if rType == 'uint16': + return await self.__read_uint16_register(register, **rConf) + elif rType == 'uint32': + return await self.__read_uint32_register(register, **rConf) + elif rType == 'int32': + return await self.__read_int32_register(register, **rConf) + elif rType == 'enum': + return await self.__read_enum_register(register, **rConf) + else: + assert False, 'unknown register type' + + + async def write_register(self, register: int, value: int) -> None: + assert register in self.__registers, 'unknown register' + + rw = self.__registers[register]['rw'] + assert rw is 'w' or rw is 'rw', 'register is not writable' + + rType = self.__registers[register]['type'] + rConf = self.__registers[register]['conf'] + if rType == 'uint16': + await self.__write_uint16_register(register, value, **rConf) + elif rType == 'uint32': + await self.__write_uint32_register(register, value, **rConf) + elif rType == 'int32': + await self.__write_int32_register(register, value, **rConf) + elif rType == 'enum': + await self.__write_enum_register(register, value, ** rConf) + else: + assert False, 'unknown register type' + + async def __read_uint16_register(self, register: int, min: int, max: int, unit: str, scale: int)-> float: + res = await self.__client.read_holding_registers(register-1, 1) + res = res[0] / (1/scale) + + assert res >= min and res <= max + return res + + async def __write_uint16_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None: + assert value >= min and value <= max + + await self.__client.write_multiple_registers(register-1, [int(value * (1/scale))]) + + async def __read_uint32_register(self, register: int, min: int, max: int, unit: str, scale: int) -> float: + res = await self.__client.read_holding_registers(register-1, 2) + res = struct.pack('>HH', res[0], res[1]) + res = struct.unpack('>L', res) + res = res[0] / (1/scale) + + assert res >= min and res <= max + return res + + async def __write_uint32_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None: + assert value >= min and value <= max + + req = int(value * (1/scale)) + req = struct.pack('>L', req) + req = struct.unpack('>HH', req) + + await self.__client.write_multiple_registers(register-1, [x for x in req]) + + async def __read_int32_register(self, register: int, min: int, max: int, unit: str, scale: int) -> float: + res = await self.__client.read_holding_registers(register-1, 2) + + res = struct.pack('>HH', res[0], res[1]) + res = struct.unpack('>l', res) + res = res[0] / (1/scale) + + assert res >= min and res <= max + return res + + async def __write_int32_register(self, register: int, value: float, min: int, max: int, unit: str, scale: int) -> None: + assert value >= min and value <= max + + req = int(value * (1/scale)) + req = struct.pack('>l', req) + req = struct.unpack('>HH', req) + + await self.__client.write_multiple_registers(register-1, [x for x in req]) + + async def __read_enum_register(self, register: int, values: Mapping[int, str]) -> int: + res = await self.__client.read_holding_registers(register - 1, 1) + res = res[0] + + assert res in values + return res + + async def __write_enum_register(self, register: int, value: int, values: Mapping[int, str]) -> None: + assert value in values + + await self.__client.write_multiple_registers(register-1, [value]) + + async def loopback_test(self, testvalue: int = None) -> None: + if testvalue is not None: + assert testvalue >= 0 and testvalue < 2 ^ 16 + else: + testvalue = random.randrange(2 ^ 16) + + await self.__client.loopback_test(testvalue) diff --git a/graphit_modbus/hitachi.py b/graphit_modbus/hitachi.py new file mode 100644 index 0000000..2fb7372 --- /dev/null +++ b/graphit_modbus/hitachi.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +__all__ = ('SJP1Fu') + + +from . import DatatypesProtocol, ClientInterface + + +class SJP1Fu(): + + def __init__(self, client: ClientInterface) -> None: + self.__protocol = DatatypesProtocol(client, SJP1FU_COILS, SJP1FU_REGISTERS) + + async def set_frequency(self, frequency: int) -> None: + # Page 532 / 14-41 + await self.__protocol.write_register(10502, frequency) + + async def get_frequency(self) -> int: + # Page 532 / 14-41 + return int(await self.__protocol.read_register(10502)) + + async def start_inverter(self) -> None: + # Page 518 / 14-27 + await self.__protocol.write_coil(1, True) + + async def stop_inverter(self) -> None: + # Page 518 / 14-27 + await self.__protocol.write_coil(1, False) + + @property + async def inverter_active(self) -> bool: + return await self.__protocol.read_coil(1) + + +SJP1FU_COILS = { + 1: { + 'name': 'Operation Command', + 'rw': 'rw', + 'values': [ 'Stop', 'Run' ] + } +} + + +SJP1FU_REGISTERS = { + 10001: { + 'code': 'dA-01', + 'name': 'Output frequency monitor', + 'rw': 'r', + 'type': 'uint16', + 'conf': { + 'min': 0, + 'max': 590, + 'unit': 'Hz', + 'scale': 0.01, + }, + }, + + 10003: { + 'code': 'dA-03', + 'name': 'Operation direction monitor', + 'rw': 'r', + 'type': 'enum', + 'conf': { + 'values': { + 0: 'Stop', + 1: 'Zero-speed out', + 2: 'Forward run', + 3: 'Reverse run', + } + } + }, + + 10502: { + 'name': 'Set Frequency', + 'rw': 'rw', + 'type': 'int32', + 'conf': { + 'min': -590, + 'max': 590, + 'unit': 'Hz', + 'scale': 0.01, + }, + }, + + 11010: { + 'code': 'FA-10', + 'name': 'Acceleration time (monitor + setting)', + 'rw': 'rw', + 'type': 'uint32', + 'conf': { + 'min': 0, + 'max': 3600, + 'unit': 's', + 'scale': 0.01, + }, + }, + 11012: { + 'code': 'FA-12', + 'name': 'Deceleration time (monitor + setting)', + 'rw': 'rw', + 'type': 'uint32', + 'conf': { + 'min': 0, + 'max': 3600, + 'unit': 's', + 'scale': 0.01, + }, + }, + + 12501: { + 'code': 'AF101', + 'name': 'First DC braking selection', + 'rw': 'rw', + 'type': 'enum', + 'conf': { + 'values': { +# 0: 'Stop', +# 1: 'Zero-speed out', +# 2: 'Forward run', +# 3: 'Reverse run', + } + } + }, +} diff --git a/graphit_modbus/transport.py b/graphit_modbus/transport.py new file mode 100644 index 0000000..8959145 --- /dev/null +++ b/graphit_modbus/transport.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +__all__ = ('ClientInterface', 'SerialPort', 'SerialClient') + + +import os +import abc +import fcntl +import struct +import asyncio + +from typing import List + +from umodbus.client.serial import rtu +from umodbus.functions import expected_response_pdu_size_from_request_pdu +from umodbus.client.serial.redundancy_check import add_crc + + +class ClientInterface(): + + @abc.abstractmethod + async def read_coils(self, starting_address: int, quantity: int) -> List[bool]: + ... + + @abc.abstractmethod + async def read_discrete_inputs(self, starting_address: int, quantity: int) -> List[bool]: + ... + + @abc.abstractmethod + async def read_holding_registers(self, starting_address: int, quantity: int) -> List[int]: + ... + + @abc.abstractmethod + async def read_input_registers(self, starting_address: int, quantity: int) -> List[int]: + ... + + @abc.abstractmethod + async def write_single_coil(self, address: int, value: bool) -> None: + ... + + @abc.abstractmethod + async def write_single_register(self, address: int, value: int) -> None: + ... + + @abc.abstractmethod + async def write_multiple_coils(self, starting_address: int, values: List[bool]) -> None: + ... + + @abc.abstractmethod + async def write_multiple_registers(self, starting_address: int, values: List[int]) -> None: + ... + + @abc.abstractmethod + async def loopback_test(self, value: int) -> int: + ... + + +class SerialPort(): + + def __init__(self, filename: str) -> None: + self.__file = os.open(filename, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + + self.__read_lock = asyncio.Lock() + self.__write_lock = asyncio.Lock() + + fcntl.fcntl(self.__file, fcntl.F_SETFL, os.O_NONBLOCK) + + async def __read(self, size: int = 1) -> bytes: + assert size > 0 + + loop = asyncio.get_event_loop() + future = loop.create_future() + + loop.add_reader(self.__file, lambda: future.set_result(os.read(self.__file, size))) + future.add_done_callback(lambda _f: loop.remove_reader(self.__file)) + + return await future + + async def read(self, size: int = 1) -> bytes: + assert size > 0 + + async with self.__read_lock: + buffer = b"" + while size > 0: + data = await self.__read(size) + if data is b"": + return buffer + + buffer += data + size -= len(data) + + return buffer + + async def __write(self, data: bytes) -> int: + assert len(data) > 0 + + loop = asyncio.get_event_loop() + future = loop.create_future() + + loop.add_writer(self.__file, lambda: future.set_result(os.write(self.__file, data))) + future.add_done_callback(lambda _f: loop.remove_writer(self.__file)) + + return await future + + async def write(self, data: bytes) -> int: + assert len(data) > 0 + + async with self.__write_lock: + size = 0 + while len(data) > 0: + written = await self.__write(data) + if written is 0: + return size + + data = data[written:] + size += written + + return size + + +class SerialClient(ClientInterface): + + def __init__(self, port: SerialPort, slave_id: int) -> None: + self.__port = port + self.__slave_id = slave_id + + async def read_coils(self, starting_address: int, quantity: int) -> List[bool]: + response = await self.__send_message(rtu.read_coils(self.__slave_id, starting_address, quantity)) + response = [bool(x) for x in response] + + return response + + async def read_discrete_inputs(self, starting_address: int, quantity: int) -> List[bool]: + response = await self.__send_message(rtu.read_discrete_inputs(self.__slave_id, starting_address, quantity)) + response = [bool(x) for x in response] + + return response + + async def read_holding_registers(self, starting_address: int, quantity: int) -> List[int]: + return await self.__send_message(rtu.read_holding_registers(self.__slave_id, starting_address, quantity)) + + async def read_input_registers(self, starting_address: int, quantity: int) -> List[int]: + return await self.__send_message(rtu.read_input_registers(self.__slave_id, starting_address, quantity)) + + async def write_single_coil(self, address: int, value: bool) -> None: + await self.__send_message(rtu.write_single_coil(self.__slave_id, address, int(value))) + + async def write_single_register(self, address: int, value: int) -> None: + await self.__send_message(rtu.write_single_register(self.__slave_id, address, value)) + + async def write_multiple_coils(self, starting_address: int, values: List[bool]) -> None: + await self.__send_message(rtu.write_multiple_coils(self.__slave_id, starting_address, [int(x) for x in values])) + + async def write_multiple_registers(self, starting_address: int, values: List[int]) -> None: + await self.__send_message(rtu.write_multiple_registers(self.__slave_id, starting_address, values)) + + async def loopback_test(self, value: int) -> int: + assert 0 <= value < 2 ^ 16 + + req = struct.pack('>BBHH', 1, 8, 0, value) + req = add_crc(req) + + await self.__port.write(req) + + res = await self.__port.read(len(req)) + assert req == res + + return value + + async def __send_message(self, message: bytes) -> List[int]: + await self.__port.write(message) + + # Check exception ADU (which is shorter than all other responses) first. + exception_adu_size = 5 + response_error_adu = await self.__port.read(exception_adu_size) + rtu.raise_for_exception_adu(response_error_adu) + + expected_response_size = \ + expected_response_pdu_size_from_request_pdu(message[1:-2]) + 3 + response_remainder = await self.__port.read(expected_response_size - exception_adu_size) + + if len(response_remainder) < expected_response_size - exception_adu_size: + raise ValueError + + result = rtu.parse_response_adu(response_error_adu + response_remainder, message) + + if not isinstance(result, list): + return [result] + + return result -- 2.34.1