From c46607a2df871b5809af7762fe3409133b1b1c9c Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Fri, 22 Jan 2021 10:26:47 +0100 Subject: [PATCH] Put dependencies in this repository and rename --- conf.json | 2 +- gevent/__init__.py | 4 + gevent/interface.py | 25 +++ gevent/mixin.py | 64 ++++++ gmodbus/__init__.py | 138 ++++++++++++ gmodbus/hitachi.py | 124 +++++++++++ gmodbus/transport.py | 190 +++++++++++++++++ gpin/__init__.py | 13 ++ gpin/composition.py | 196 ++++++++++++++++++ gpin/gpio.py | 65 ++++++ gpin/interface.py | 23 ++ gpin/pcf8574.py | 136 ++++++++++++ gpin/pigpio.py | 9 + modbus_test.py | 35 ++++ controlpi.service => schaltschrank.service | 4 +- .../__init__.py | 0 .../config.py | 6 +- {graphit_controlpi => schaltschrank}/main.py | 0 .../websocket.py | 0 setup.py | 5 +- 20 files changed, 1031 insertions(+), 8 deletions(-) create mode 100644 gevent/__init__.py create mode 100644 gevent/interface.py create mode 100644 gevent/mixin.py create mode 100644 gmodbus/__init__.py create mode 100644 gmodbus/hitachi.py create mode 100644 gmodbus/transport.py create mode 100644 gpin/__init__.py create mode 100644 gpin/composition.py create mode 100644 gpin/gpio.py create mode 100644 gpin/interface.py create mode 100644 gpin/pcf8574.py create mode 100644 gpin/pigpio.py create mode 100644 modbus_test.py rename controlpi.service => schaltschrank.service (65%) rename {graphit_controlpi => schaltschrank}/__init__.py (100%) rename {graphit_controlpi => schaltschrank}/config.py (89%) rename {graphit_controlpi => schaltschrank}/main.py (100%) rename {graphit_controlpi => schaltschrank}/websocket.py (100%) diff --git a/conf.json b/conf.json index f3d1757..ad32949 100644 --- a/conf.json +++ b/conf.json @@ -52,5 +52,5 @@ [ "E4-5", "T2-13" ], [ "E4-6", "T2-14" ], [ "E4-7", "T2-15" ], [ "E4-8", "T2-16" ] ] } ], "modbus": - { "serial device": "/dev/serial1", + { "serial device": "/dev/serial0", "slave id": 1 } } diff --git a/gevent/__init__.py b/gevent/__init__.py new file mode 100644 index 0000000..d2148e1 --- /dev/null +++ b/gevent/__init__.py @@ -0,0 +1,4 @@ +__all__ = ['EventEmitterInterface', 'EventEmitterMixin'] + +from .interface import EventEmitterInterface +from .mixin import EventEmitterMixin diff --git a/gevent/interface.py b/gevent/interface.py new file mode 100644 index 0000000..939f1e4 --- /dev/null +++ b/gevent/interface.py @@ -0,0 +1,25 @@ +import abc +from typing import Hashable, Callable + + +class EventEmitterInterface(abc.ABC): + @abc.abstractmethod + def on(self, event: Hashable, callback: Callable) -> Hashable: + ''' + Registers the given callback for the given event. + Returns handle to unregister the given callback. + ''' + + @abc.abstractmethod + def off(self, handle: Hashable) -> bool: + ''' + Unregisters a previously registered callback by the given handle. + Returns True on success. + ''' + + @abc.abstractmethod + def _emit(self, event: Hashable, *args, **kwargs) -> None: + ''' + Emits the given event by calling all callbacks registered for this + event. + ''' diff --git a/gevent/mixin.py b/gevent/mixin.py new file mode 100644 index 0000000..dfc618b --- /dev/null +++ b/gevent/mixin.py @@ -0,0 +1,64 @@ +import uuid +from typing import Hashable, Callable, MutableMapping, Mapping + +from .interface import EventEmitterInterface + +EvMap = MutableMapping[Hashable, Hashable] +CbMap = MutableMapping[Hashable, MutableMapping[Hashable, Callable]] + + +class EventEmitterMixin(EventEmitterInterface): + def on(self, event: Hashable, callback: Callable) -> Hashable: + events: EvMap + callbacks: CbMap + try: + events = self._eventEmitterMixinEvents + callbacks = self._eventEmitterMixinCallbacks + except AttributeError: + self._eventEmitterMixinEvents: EvMap = {} + self._eventEmitterMixinCallbacks: CbMap = {} + events = self._eventEmitterMixinEvents + callbacks = self._eventEmitterMixinCallbacks + + if event not in callbacks: + callbacks[event] = {} + + handle = uuid.uuid4() + while handle in events: + handle = uuid.uuid4() + + events[handle] = event + callbacks[event][handle] = callback + + return handle + + def off(self, handle: Hashable) -> bool: + try: + events = self._eventEmitterMixinEvents + callbacks = self._eventEmitterMixinCallbacks + except AttributeError: + return False + + if handle not in events: + return False + event = events[handle] + + del events[handle] + del callbacks[event][handle] + + if not callbacks[event]: + del callbacks[event] + + return True + + def _emit(self, event: Hashable, *args, **kwargs) -> None: + try: + callbacks = self._eventEmitterMixinCallbacks + except AttributeError: + return + + if event not in callbacks: + return + + for callback in callbacks[event].values(): + callback(*args, **kwargs) diff --git a/gmodbus/__init__.py b/gmodbus/__init__.py new file mode 100644 index 0000000..46a4c07 --- /dev/null +++ b/gmodbus/__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/gmodbus/hitachi.py b/gmodbus/hitachi.py new file mode 100644 index 0000000..2fb7372 --- /dev/null +++ b/gmodbus/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/gmodbus/transport.py b/gmodbus/transport.py new file mode 100644 index 0000000..8959145 --- /dev/null +++ b/gmodbus/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 diff --git a/gpin/__init__.py b/gpin/__init__.py new file mode 100644 index 0000000..34d0b74 --- /dev/null +++ b/gpin/__init__.py @@ -0,0 +1,13 @@ +__all__ = ['PinInterface', + 'InvertingPin', 'SwitchPin', 'GuardedPin', 'TimerPin', + 'AggregatePinInterface', 'AbstractAggregatePin', + 'OrAggregatePin', 'AndAggregatePin', + 'GPIOInputPin', 'GPIOOutputPin', + 'PCF8574Input', 'PCF8574Output'] + +from .interface import PinInterface +from .composition import InvertingPin, SwitchPin, GuardedPin, TimerPin,\ + AggregatePinInterface, AbstractAggregatePin,\ + OrAggregatePin, AndAggregatePin +from .gpio import GPIOInputPin, GPIOOutputPin +from .pcf8574 import PCF8574Input, PCF8574Output diff --git a/gpin/composition.py b/gpin/composition.py new file mode 100644 index 0000000..30d12f3 --- /dev/null +++ b/gpin/composition.py @@ -0,0 +1,196 @@ +import abc +import asyncio +from typing import Sequence +import gevent + +from .interface import PinInterface + + +class InvertingPin(PinInterface, gevent.EventEmitterMixin): + ''' Wraps and inverts a pin ''' + + def __init__(self, pin: PinInterface) -> None: + self.__pin = pin + + def _onChange(value: bool): + self._emit('change', not value) + self.__pin.on('change', _onChange) + + @property + def value(self) -> bool: + return not self.__pin.value + + @value.setter + def value(self, value: bool) -> None: + self.__pin.value = not value + + @property + def settable(self) -> bool: + return self.__pin.settable + + +class SwitchPin(PinInterface, gevent.EventEmitterMixin): + ''' Turns a Push-Button into a Switch ''' + + def __init__(self, pin: PinInterface, value: bool = False) -> None: + self.__pin = pin + self.__value = value + + def _onChange(value: bool): + if value: + self.value = not self.value + self.__pin.on('change', _onChange) + + @property + def value(self) -> bool: + return self.__value + + @value.setter + def value(self, value: bool) -> None: + if value != self.__value: + self.__value = value + self._emit('change', self.__value) + + @property + def settable(self) -> bool: + return True + + +class GuardedPin(PinInterface, gevent.EventEmitterMixin): + ''' Wraps a pin and a guard ''' + + def __init__(self, wrapped: PinInterface, guard: PinInterface) -> None: + self.__wrapped = wrapped + self.__guard = guard + + def _onChange(value: bool): + if self.__guard.value: + return + self._emit('change', value) + self.__wrapped.on('change', _onChange) + + @property + def value(self) -> bool: + return self.__wrapped.value + + @value.setter + def value(self, value: bool) -> None: + if self.__guard.value: + return + self.__wrapped.value = value + + @property + def settable(self) -> bool: + return self.__wrapped.settable + + +class TimerPin(PinInterface, gevent.EventEmitterMixin): + ''' The TimerPin unsets itself after a given delay ''' + + def __init__(self, delay: float) -> None: + self.__delay = delay + + self.__value = False + self.__handle = None + + @property + def value(self) -> bool: + return self.__value + + @value.setter + def value(self, value: bool) -> None: + if self.__value != value: + self.__value = value + self.__switch() + + self._emit('change', self.__value) + + @property + def settable(self) -> bool: + return True + + def __switch(self): + def _trigger(): + self.value = False + + if self.__value: + loop = asyncio.get_running_loop() + self.__handle = loop.call_later(self.__delay, _trigger) + elif self.__handle: + self.__handle.cancel() + self.__handle = None + + +class AggregatePinInterface(PinInterface): + ''' A pin that aggregates other pins ''' + + @property + @abc.abstractmethod + def children(self) -> Sequence[PinInterface]: + ''' The pins ''' + + +class AbstractAggregatePin(AggregatePinInterface, + gevent.EventEmitterMixin): + ''' An abstract pin aggregate ''' + + def __init__(self, children: Sequence[PinInterface]) -> None: + assert children, 'AggregatePin needs at least one child!' + + self.__children = tuple(children) + self.__value = self._calculate(self.__children) + + def _onChange(_value: bool): + value = self.__value + self.__value = self._calculate(self.__children) + if value != self.__value: + self._emit('change', self.__value) + for pin in self.__children: + pin.on('change', _onChange) + + @property + def value(self) -> bool: + return self.__value + + @value.setter + def value(self, value: bool) -> None: + raise NotImplementedError() + + @property + def settable(self) -> bool: + return False + + @property + def children(self) -> Sequence[PinInterface]: + return self.__children + + @abc.abstractmethod + def _calculate(self, children: Sequence[PinInterface]) -> bool: + ''' Calculate the aggregated value ''' + raise NotImplementedError() + + +class OrAggregatePin(AbstractAggregatePin): + ''' A pin that aggregates with the 'or' function. ''' + + def __init__(self, children: Sequence[PinInterface]) -> None: + AbstractAggregatePin.__init__(self, children) + + def _calculate(self, children: Sequence[PinInterface]) -> bool: + value = False + for child in children: + value = value or child.value + return value + + +class AndAggregatePin(AbstractAggregatePin): + ''' A pin that aggregates with the 'and' function. ''' + + def __init__(self, children: Sequence[PinInterface]) -> None: + AbstractAggregatePin.__init__(self, children) + + def _calculate(self, children: Sequence[PinInterface]) -> bool: + value = True + for child in children: + value = value and child.value + return value diff --git a/gpin/gpio.py b/gpin/gpio.py new file mode 100644 index 0000000..5945b01 --- /dev/null +++ b/gpin/gpio.py @@ -0,0 +1,65 @@ +import asyncio +import pigpio +import gevent + +from .pigpio import get_pigpio_pi +from .interface import PinInterface + + +class GPIOInputPin(PinInterface, gevent.EventEmitterMixin): + def __init__(self, pin: int, glitch: int = 5000, up: bool = False) -> None: + self._pin = pin + pi = get_pigpio_pi() + pi.set_mode(self._pin, pigpio.INPUT) + pi.set_glitch_filter(self._pin, glitch) + pi.set_pull_up_down(self._pin, + pigpio.PUD_UP if up else pigpio.PUD_DOWN) + + def _onLoopChange(value: bool): + if self._value != value: + self._value = value + self._emit('change', self._value) + + loop = asyncio.get_running_loop() + + def _onGpioChange(pin: int, level: int, _tick: int): + if self._pin == pin and level < 2: + loop.call_soon_threadsafe(_onLoopChange, bool(level)) + + pi.callback(self._pin, pigpio.EITHER_EDGE, _onGpioChange) + self._value = bool(pi.read(self._pin)) + + @property + def value(self) -> bool: + return self._value + + @value.setter + def value(self, value: bool) -> None: + raise NotImplementedError() + + @property + def settable(self) -> bool: + return False + + +class GPIOOutputPin(PinInterface, gevent.EventEmitterMixin): + def __init__(self, pin: int) -> None: + self._pin = pin + pi = get_pigpio_pi() + pi.set_mode(self._pin, pigpio.OUTPUT) + self._value = bool(pi.read(self._pin)) + + @property + def value(self) -> bool: + return self._value + + @value.setter + def value(self, value: bool) -> None: + if self._value != value: + self._value = value + get_pigpio_pi().write(self._pin, int(value)) + self._emit('change', self._value) + + @property + def settable(self) -> bool: + return True diff --git a/gpin/interface.py b/gpin/interface.py new file mode 100644 index 0000000..c1c8130 --- /dev/null +++ b/gpin/interface.py @@ -0,0 +1,23 @@ +import abc +import gevent + + +class PinInterface(gevent.EventEmitterInterface): + ''' Emits change(bool) ''' + + @property + @abc.abstractmethod + def value(self) -> bool: + ''' Get current pin value ''' + raise NotImplementedError() + + @value.setter + def value(self, value: bool) -> None: + ''' Set the pin value ''' + raise NotImplementedError() + + @property + @abc.abstractmethod + def settable(self) -> bool: + ''' Is the pin settable? ''' + raise NotImplementedError() diff --git a/gpin/pcf8574.py b/gpin/pcf8574.py new file mode 100644 index 0000000..9e737e9 --- /dev/null +++ b/gpin/pcf8574.py @@ -0,0 +1,136 @@ +import gevent +from typing import Callable + +from .pigpio import get_pigpio_pi +from .interface import PinInterface + +PCF_ADDRESSES = tuple(range(32, 40)) + tuple(range(56, 64)) + + +def emitDiff(emit: Callable, oldValues: int, newValues: int): + assert isinstance(oldValues, int), 'oldValues must be an integer' + assert oldValues >= 0 and oldValues <= 255,\ + 'oldValues must be >= 0 and <= 255' + assert isinstance(newValues, int), 'newValues must be an integer' + assert newValues >= 0 and newValues <= 255,\ + 'newValues must be >= 0 and <= 255' + for i in range(0, 8): + mask = 1 << i + if mask & oldValues != mask & newValues: + emit('change', i, not bool(mask & newValues)) + + +class PCF8574Input(gevent.EventEmitterMixin): + def __init__(self, address: int, interrupt: PinInterface) -> None: + assert address in PCF_ADDRESSES, 'Invalid PCF8574(A) I²C address' + self._address = address + self._interrupt = interrupt + pi = get_pigpio_pi() + self._handle = pi.i2c_open(1, self._address) + self._values = pi.i2c_read_byte(self._handle) + self._pins = tuple(PCF8574InputPin(self, i) for i in range(0, 8)) + + def _onInterrupt(_value: bool): + oldValues = self._values + self._values = pi.i2c_read_byte(self._handle) + emitDiff(self._emit, oldValues, self._values) + self._int_handle = self._interrupt.on('change', _onInterrupt) + + def close(self) -> None: + self._interrupt.off(self._int_handle) + try: + get_pigpio_pi().i2c_close(self._handle) + except AttributeError: + pass + + def getPin(self, pin: int) -> PinInterface: + assert isinstance(pin, int), 'pin must be an integer' + assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7' + return self._pins[pin] + + def getValue(self, pin: int) -> bool: + assert isinstance(pin, int), 'pin must be an integer' + assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7' + return not bool(self._values & (1 << pin)) + + +class PCF8574InputPin(PinInterface, gevent.EventEmitterMixin): + def __init__(self, pcfInput: PCF8574Input, pcfPin: int) -> None: + self._input = pcfInput + self._pin = pcfPin + + def _onChange(pin: int, value: int): + if self._pin == pin: + self._emit('change', value) + self._input.on('change', _onChange) + + @property + def value(self) -> bool: + return self._input.getValue(self._pin) + + @value.setter + def value(self, value: bool) -> None: + raise NotImplementedError() + + @property + def settable(self) -> bool: + return False + + +class PCF8574Output(gevent.EventEmitterMixin): + def __init__(self, address: int) -> None: + assert address in PCF_ADDRESSES, 'Invalid PCF8574(A) I²C address' + self._address = address + pi = get_pigpio_pi() + self._handle = pi.i2c_open(1, self._address) + self._values = pi.i2c_read_byte(self._handle) + self._pins = tuple(PCF8574OutputPin(self, i) for i in range(0, 8)) + + def close(self) -> None: + try: + get_pigpio_pi().i2c_close(self._handle) + except AttributeError: + pass + + def getPin(self, pin: int) -> PinInterface: + assert isinstance(pin, int), 'pin must be an integer' + assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7' + return self._pins[pin] + + def getValue(self, pin: int) -> bool: + assert isinstance(pin, int), 'pin must be an integer' + assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7' + return not bool(self._values & (1 << pin)) + + def setValue(self, pin: int, value: bool) -> None: + assert isinstance(pin, int), 'pin must be an integer' + assert pin >= 0 and pin <= 7, 'pin must be >= 0 and <= 7' + assert isinstance(value, bool), 'value must be a bool' + value = not value + oldValues = self._values + self._values = (oldValues & (0xFF ^ (1 << pin))) | (int(value) << pin) + get_pigpio_pi().i2c_write_byte(self._handle, self._values) + emitDiff(self._emit, oldValues, self._values) + + +class PCF8574OutputPin(PinInterface, gevent.EventEmitterMixin): + def __init__(self, pcfOutput: PCF8574Output, pcfPin: int) -> None: + self._output = pcfOutput + self._pin = pcfPin + + def _onChange(pin: int, value: bool): + if self._pin == pin: + self._emit('change', value) + self._output.on('change', _onChange) + + @property + def value(self) -> bool: + return self._output.getValue(self._pin) + + @value.setter + def value(self, value: bool) -> None: + self._output.setValue(self._pin, value) + + @property + def settable(self) -> bool: + return True diff --git a/gpin/pigpio.py b/gpin/pigpio.py new file mode 100644 index 0000000..0e851c1 --- /dev/null +++ b/gpin/pigpio.py @@ -0,0 +1,9 @@ +import pigpio + +_pigpio_pi = None + +def get_pigpio_pi(): + global _pigpio_pi + if _pigpio_pi is None: + _pigpio_pi = pigpio.pi() + return _pigpio_pi diff --git a/modbus_test.py b/modbus_test.py new file mode 100644 index 0000000..1cf1ba2 --- /dev/null +++ b/modbus_test.py @@ -0,0 +1,35 @@ +import sys +import json +import asyncio +import websockets + + +async def test_commands(websocket): + commands = [{'command': 'getfrequency'}, + {'command': 'setfrequency', 'value': 100}, + {'command': 'getfrequency'}, + {'command': 'getinverter'}, + {'command': 'startinverter'}, + {'command': 'getinverter'}, + {'command': 'stopinverter'}, + {'command': 'getinverter'}] + for command in commands: + message = json.dumps(command) + await websocket.send(message) + print(f"Sent Command: {message}") + await asyncio.sleep(1) + + +async def receive_events(websocket): + async for message in websocket: + print(f"Recvd. Event: {message}") + + +async def main(hostname): + async with websockets.connect(f"ws://{hostname}") as websocket: + command_task = asyncio.create_task(test_commands(websocket)) + event_task = asyncio.create_task(receive_events(websocket)) + await command_task + +if __name__ == '__main__': + asyncio.run(main(sys.argv[1])) diff --git a/controlpi.service b/schaltschrank.service similarity index 65% rename from controlpi.service rename to schaltschrank.service index 535996c..04fb234 100644 --- a/controlpi.service +++ b/schaltschrank.service @@ -1,12 +1,12 @@ [Unit] -Description=Control Pi Service +Description=Schaltschrank Service Wants=network-online.target After=network-online.target [Service] WorkingDirectory=/home/pi Environment=PYTHONUNBUFFERED=1 -ExecStart=/home/pi/controlpi/bin/python -m graphit_controlpi.main conf.json web/ +ExecStart=/home/pi/schaltschrank/bin/python -m schaltschrank.main conf.json web/ [Install] WantedBy=multi-user.target diff --git a/graphit_controlpi/__init__.py b/schaltschrank/__init__.py similarity index 100% rename from graphit_controlpi/__init__.py rename to schaltschrank/__init__.py diff --git a/graphit_controlpi/config.py b/schaltschrank/config.py similarity index 89% rename from graphit_controlpi/config.py rename to schaltschrank/config.py index 3b44e77..d358d06 100644 --- a/graphit_controlpi/config.py +++ b/schaltschrank/config.py @@ -1,6 +1,6 @@ -from graphit_pin import PCF8574Output, PCF8574Input, GPIOInputPin -from graphit_modbus.transport import SerialPort, SerialClient -from graphit_modbus.hitachi import SJP1Fu +from gpin import PCF8574Output, PCF8574Input, GPIOInputPin +from gmodbus.transport import SerialPort, SerialClient +from gmodbus.hitachi import SJP1Fu async def process_configuration(conf, queues): diff --git a/graphit_controlpi/main.py b/schaltschrank/main.py similarity index 100% rename from graphit_controlpi/main.py rename to schaltschrank/main.py diff --git a/graphit_controlpi/websocket.py b/schaltschrank/websocket.py similarity index 100% rename from graphit_controlpi/websocket.py rename to schaltschrank/websocket.py diff --git a/setup.py b/setup.py index d0cbf30..3ba7f44 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r") as readme_file: setuptools.setup( name="graphit-controlpi", - version="0.2.3", + version="0.2.4", author="Graph-IT GmbH", author_email="info@graph-it.com", description="Main Module for Machine Control Pi", @@ -18,7 +18,8 @@ setuptools.setup( ], install_requires=[ "websockets", - "graphit-pin @ git+git://git.graph-it.com/graphit/pin-py.git", + "pigpio", + "umodbus", ], classifiers=[ "Programming Language :: Python", -- 2.34.1