Modbus old draft added
authorBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 20 Jan 2021 15:20:40 +0000 (16:20 +0100)
committerBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 20 Jan 2021 15:20:40 +0000 (16:20 +0100)
graphit_modbus/__init__.py [new file with mode: 0644]
graphit_modbus/hitachi.py [new file with mode: 0644]
graphit_modbus/transport.py [new file with mode: 0644]

diff --git a/graphit_modbus/__init__.py b/graphit_modbus/__init__.py
new file mode 100644 (file)
index 0000000..46a4c07
--- /dev/null
@@ -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 (file)
index 0000000..2fb7372
--- /dev/null
@@ -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 (file)
index 0000000..8959145
--- /dev/null
@@ -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