[ "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 } }
+++ /dev/null
-[Unit]
-Description=Control Pi 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/
-
-[Install]
-WantedBy=multi-user.target
--- /dev/null
+__all__ = ['EventEmitterInterface', 'EventEmitterMixin']
+
+from .interface import EventEmitterInterface
+from .mixin import EventEmitterMixin
--- /dev/null
+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.
+ '''
--- /dev/null
+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)
--- /dev/null
+# -*- 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)
--- /dev/null
+# -*- 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',
+ }
+ }
+ },
+}
--- /dev/null
+# -*- 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
--- /dev/null
+__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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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()
--- /dev/null
+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
--- /dev/null
+import pigpio
+
+_pigpio_pi = None
+
+def get_pigpio_pi():
+ global _pigpio_pi
+ if _pigpio_pi is None:
+ _pigpio_pi = pigpio.pi()
+ return _pigpio_pi
+++ /dev/null
-from graphit_pin import PCF8574Output, PCF8574Input, GPIOInputPin
-from graphit_modbus.transport import SerialPort, SerialClient
-from graphit_modbus.hitachi import SJP1Fu
-
-
-async def process_configuration(conf, queues):
-
- def callback_factory(pin_name):
- settable = pins[pin_name].settable
-
- def callback(value):
- for queue in queues:
- queue.put_nowait({'event': 'pinstate', 'pin': pin_name,
- 'settable': settable, 'value': value,
- 'changed': True})
- return callback
- pins = {}
- if 'i/o cards' in conf:
- for card_conf in conf['i/o cards']:
- card = None
- if card_conf['type'] == 'output':
- card = PCF8574Output(card_conf['address'])
- elif card_conf['type'] == 'input':
- card = PCF8574Input(card_conf['address'],
- GPIOInputPin(card_conf['interrupt pin'],
- up=True))
- if card is not None:
- for i in range(8):
- pin = card.getPin(i)
- pin_names = card_conf['pins'][i]
- for pin_name in pin_names:
- pins[pin_name] = pin
- pin.on('change', callback_factory(pin_name))
- fu = None
- if 'modbus' in conf:
- modbus_conf = conf['modbus']
- port = SerialPort(modbus_conf['serial device'])
- client = SerialClient(port, modbus_conf['slave id'])
- fu = SJP1Fu(client)
- return (pins, fu)
+++ /dev/null
-import sys
-import json
-import asyncio
-
-from .config import process_configuration
-from .websocket import setup_websocket
-
-
-async def setup():
- pins = {}
- fu = None
- queues = []
- with open(sys.argv[1]) as json_data:
- conf = json.load(json_data)
- (pins, fu) = await process_configuration(conf, queues)
- await setup_websocket(pins, fu, queues, sys.argv[2])
-
-
-if __name__ == '__main__':
- asyncio.get_event_loop().run_until_complete(setup())
- try:
- asyncio.get_event_loop().run_forever()
- except KeyboardInterrupt:
- pass
+++ /dev/null
-import os
-import functools
-import socket
-import json
-import asyncio
-import websockets
-from http import HTTPStatus
-
-
-async def process_command(command, pins, fu, queue):
- if command['command'] == 'setpin':
- if command['pin'] in pins and pins[command['pin']].settable:
- pins[command['pin']].value = command['value']
- elif command['command'] == 'getpin':
- if command['pin'] in pins:
- pin = pins[command['pin']]
- await queue.put({'event': 'pinstate', 'pin': command['pin'],
- 'settable': pin.settable, 'value': pin.value,
- 'changed': False})
- elif command['command'] == 'getallpins':
- for pin_name in pins:
- pin = pins[pin_name]
- await queue.put({'event': 'pinstate', 'pin': pin_name,
- 'settable': pin.settable, 'value': pin.value,
- 'changed': False})
- elif command['command'] == 'setfrequency':
- await fu.set_frequency(command['value'])
- elif command['command'] == 'getfrequency':
- frequency = await fu.get_frequency()
- await queue.put({'event': 'frequency', 'frequency': frequency})
- elif command['command'] == 'startinverter':
- await fu.start_inverter()
- elif command['command'] == 'stopinverter':
- await fu.stop_inverter()
- elif command['command'] == 'getinverter':
- active = await fu.inverter_active
- await queue.put({'event': 'inverterstate', 'active': active})
-
-
-async def command_handler(websocket, path, pins, fu, queue):
- async for message in websocket:
- command = json.loads(message)
- await process_command(command, pins, fu, queue)
-
-
-async def event_handler(websocket, path, queue):
- while True:
- event = await queue.get()
- message = json.dumps(event)
- await websocket.send(message)
- queue.task_done()
-
-
-async def handler(pins, fu, queues, websocket, path):
- queue = asyncio.Queue()
- queues.append(queue)
- command_task = asyncio.create_task(command_handler(websocket, path,
- pins, fu, queue))
- event_task = asyncio.create_task(event_handler(websocket, path,
- queue))
- done, pending = await asyncio.wait(
- [command_task, event_task],
- return_when=asyncio.FIRST_COMPLETED,
- )
- for task in pending:
- task.cancel()
- queues.remove(queue)
-
-
-async def process_request(server_root, path, request_headers):
- if 'Upgrade' in request_headers:
- return
- if path == '/':
- path = '/index.html'
- response_headers = [
- ('Server', 'graphit_controlpi websocket server'),
- ('Connection', 'close'),
- ]
- server_root = os.path.realpath(os.path.join(os.getcwd(), server_root))
- full_path = os.path.realpath(os.path.join(server_root, path[1:]))
- if os.path.commonpath((server_root, full_path)) != server_root or \
- not os.path.exists(full_path) or not os.path.isfile(full_path):
- return HTTPStatus.NOT_FOUND, [], b'404 NOT FOUND'
- mime_type = 'application/octet-stream'
- extension = full_path.split(".")[-1]
- if extension == 'html':
- mime_type = 'text/html'
- elif extension == 'js':
- mime_type = 'text/javascript'
- elif extension == 'css':
- mime_type = 'text/css'
- response_headers.append(('Content-Type', mime_type))
- body = open(full_path, 'rb').read()
- response_headers.append(('Content-Length', str(len(body))))
- return HTTPStatus.OK, response_headers, body
-
-
-async def get_ip():
- ip = None
- while not ip:
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect(('10.255.255.255', 1))
- ip = s.getsockname()[0]
- except Exception:
- ip = None
- await asyncio.sleep(0.1)
- finally:
- s.close()
- return ip
-
-
-async def setup_websocket(pins, fu, queues, server_root):
- parameterised_handler = functools.partial(handler, pins, fu, queues)
- parameterised_process_request = functools.partial(process_request, server_root)
- hostname = await get_ip()
- await websockets.serve(parameterised_handler, hostname, 80,
- process_request=parameterised_process_request)
- print(f"Serving on ws://{hostname}:80")
--- /dev/null
+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]))
--- /dev/null
+[Unit]
+Description=Schaltschrank Service
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+WorkingDirectory=/home/pi
+Environment=PYTHONUNBUFFERED=1
+ExecStart=/home/pi/schaltschrank/bin/python -m schaltschrank.main conf.json web/
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+from gpin import PCF8574Output, PCF8574Input, GPIOInputPin
+from gmodbus.transport import SerialPort, SerialClient
+from gmodbus.hitachi import SJP1Fu
+
+
+async def process_configuration(conf, queues):
+
+ def callback_factory(pin_name):
+ settable = pins[pin_name].settable
+
+ def callback(value):
+ for queue in queues:
+ queue.put_nowait({'event': 'pinstate', 'pin': pin_name,
+ 'settable': settable, 'value': value,
+ 'changed': True})
+ return callback
+ pins = {}
+ if 'i/o cards' in conf:
+ for card_conf in conf['i/o cards']:
+ card = None
+ if card_conf['type'] == 'output':
+ card = PCF8574Output(card_conf['address'])
+ elif card_conf['type'] == 'input':
+ card = PCF8574Input(card_conf['address'],
+ GPIOInputPin(card_conf['interrupt pin'],
+ up=True))
+ if card is not None:
+ for i in range(8):
+ pin = card.getPin(i)
+ pin_names = card_conf['pins'][i]
+ for pin_name in pin_names:
+ pins[pin_name] = pin
+ pin.on('change', callback_factory(pin_name))
+ fu = None
+ if 'modbus' in conf:
+ modbus_conf = conf['modbus']
+ port = SerialPort(modbus_conf['serial device'])
+ client = SerialClient(port, modbus_conf['slave id'])
+ fu = SJP1Fu(client)
+ return (pins, fu)
--- /dev/null
+import sys
+import json
+import asyncio
+
+from .config import process_configuration
+from .websocket import setup_websocket
+
+
+async def setup():
+ pins = {}
+ fu = None
+ queues = []
+ with open(sys.argv[1]) as json_data:
+ conf = json.load(json_data)
+ (pins, fu) = await process_configuration(conf, queues)
+ await setup_websocket(pins, fu, queues, sys.argv[2])
+
+
+if __name__ == '__main__':
+ asyncio.get_event_loop().run_until_complete(setup())
+ try:
+ asyncio.get_event_loop().run_forever()
+ except KeyboardInterrupt:
+ pass
--- /dev/null
+import os
+import functools
+import socket
+import json
+import asyncio
+import websockets
+from http import HTTPStatus
+
+
+async def process_command(command, pins, fu, queue):
+ if command['command'] == 'setpin':
+ if command['pin'] in pins and pins[command['pin']].settable:
+ pins[command['pin']].value = command['value']
+ elif command['command'] == 'getpin':
+ if command['pin'] in pins:
+ pin = pins[command['pin']]
+ await queue.put({'event': 'pinstate', 'pin': command['pin'],
+ 'settable': pin.settable, 'value': pin.value,
+ 'changed': False})
+ elif command['command'] == 'getallpins':
+ for pin_name in pins:
+ pin = pins[pin_name]
+ await queue.put({'event': 'pinstate', 'pin': pin_name,
+ 'settable': pin.settable, 'value': pin.value,
+ 'changed': False})
+ elif command['command'] == 'setfrequency':
+ await fu.set_frequency(command['value'])
+ elif command['command'] == 'getfrequency':
+ frequency = await fu.get_frequency()
+ await queue.put({'event': 'frequency', 'frequency': frequency})
+ elif command['command'] == 'startinverter':
+ await fu.start_inverter()
+ elif command['command'] == 'stopinverter':
+ await fu.stop_inverter()
+ elif command['command'] == 'getinverter':
+ active = await fu.inverter_active
+ await queue.put({'event': 'inverterstate', 'active': active})
+
+
+async def command_handler(websocket, path, pins, fu, queue):
+ async for message in websocket:
+ command = json.loads(message)
+ await process_command(command, pins, fu, queue)
+
+
+async def event_handler(websocket, path, queue):
+ while True:
+ event = await queue.get()
+ message = json.dumps(event)
+ await websocket.send(message)
+ queue.task_done()
+
+
+async def handler(pins, fu, queues, websocket, path):
+ queue = asyncio.Queue()
+ queues.append(queue)
+ command_task = asyncio.create_task(command_handler(websocket, path,
+ pins, fu, queue))
+ event_task = asyncio.create_task(event_handler(websocket, path,
+ queue))
+ done, pending = await asyncio.wait(
+ [command_task, event_task],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ for task in pending:
+ task.cancel()
+ queues.remove(queue)
+
+
+async def process_request(server_root, path, request_headers):
+ if 'Upgrade' in request_headers:
+ return
+ if path == '/':
+ path = '/index.html'
+ response_headers = [
+ ('Server', 'graphit_controlpi websocket server'),
+ ('Connection', 'close'),
+ ]
+ server_root = os.path.realpath(os.path.join(os.getcwd(), server_root))
+ full_path = os.path.realpath(os.path.join(server_root, path[1:]))
+ if os.path.commonpath((server_root, full_path)) != server_root or \
+ not os.path.exists(full_path) or not os.path.isfile(full_path):
+ return HTTPStatus.NOT_FOUND, [], b'404 NOT FOUND'
+ mime_type = 'application/octet-stream'
+ extension = full_path.split(".")[-1]
+ if extension == 'html':
+ mime_type = 'text/html'
+ elif extension == 'js':
+ mime_type = 'text/javascript'
+ elif extension == 'css':
+ mime_type = 'text/css'
+ response_headers.append(('Content-Type', mime_type))
+ body = open(full_path, 'rb').read()
+ response_headers.append(('Content-Length', str(len(body))))
+ return HTTPStatus.OK, response_headers, body
+
+
+async def get_ip():
+ ip = None
+ while not ip:
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ s.connect(('10.255.255.255', 1))
+ ip = s.getsockname()[0]
+ except Exception:
+ ip = None
+ await asyncio.sleep(0.1)
+ finally:
+ s.close()
+ return ip
+
+
+async def setup_websocket(pins, fu, queues, server_root):
+ parameterised_handler = functools.partial(handler, pins, fu, queues)
+ parameterised_process_request = functools.partial(process_request, server_root)
+ hostname = await get_ip()
+ await websockets.serve(parameterised_handler, hostname, 80,
+ process_request=parameterised_process_request)
+ print(f"Serving on ws://{hostname}:80")
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",
],
install_requires=[
"websockets",
- "graphit-pin @ git+git://git.graph-it.com/graphit/pin-py.git",
+ "pigpio",
+ "umodbus",
],
classifiers=[
"Programming Language :: Python",