From dd7e470290e4a68da97f46850eb8e9438ce1cbd2 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Wed, 24 Mar 2021 01:18:22 +0100 Subject: [PATCH] Initial commit: I/O cards. --- .gitignore | 4 + LICENSE | 19 ++++ README.md | 14 +++ conf.json | 53 +++++++++++ controlpi_plugins/pinio.py | 187 +++++++++++++++++++++++++++++++++++++ doc/index.md | 28 ++++++ setup.py | 25 +++++ 7 files changed, 330 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conf.json create mode 100644 controlpi_plugins/pinio.py create mode 100644 doc/index.md create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..756c751 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +dist/ +controlpi_pinio.egg-info/ +venv/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ebb8ac1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Graph-IT GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..612edab --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# ControlPi Plugin for GPIO Pins and I2C I/O Cards +This distribution package contains a plugin for the +[ControlPi system](https://docs.graph-it.com/graphit/controlpi), that +<…> + +Documentation (in German) can be found at [doc/index.md](doc/index.md) in +the source repository and at +[https://docs.graph-it.com/graphit/controlpi-pinio/](https://docs.graph-it.com/graphit/controlpi-pinio/). +Code documentation (in English) including doctests is contained in the +source files. +An API documentation generated by pdoc3 can be found at +[doc/controlpi_plugins/index.html](doc/controlpi_plugins/index.html) in the source +repository and at +[https://docs.graph-it.com/graphit/controlpi-pinio/controlpi_plugins/](https://docs.graph-it.com/graphit/controlpi-pinio/controlpi_plugins/). diff --git a/conf.json b/conf.json new file mode 100644 index 0000000..55d7cf8 --- /dev/null +++ b/conf.json @@ -0,0 +1,53 @@ +{ + "Example Server": { + "plugin": "WSServer", + "port": 8080, + "web": { + "/": { "module": "controlpi_plugins.wsserver", + "location": "Debug" } + } + }, + "Output Card 1": { + "plugin": "OutputCard", + "address": 56, + "pins": { "T1-1": 0, "T1-2": 1, "T1-3": 2, "T1-4": 3, + "T1-5": 4, "T1-6": 5, "T1-7": 6, "T1-8": 7 } + }, + "Output Card 2": { + "plugin": "OutputCard", + "address": 57, + "pins": { "T1-9": 0, "T1-10": 1, "T1-11": 2, "T1-12": 3, + "T1-13": 4, "T1-14": 5, "T1-15": 6, "T1-16": 7 } + }, + "Input Card 1": { + "plugin": "InputCard", + "address": 32, + "interrupt pin": 4, + "pins": { "T1-18": 0, "T1-19": 1, "T1-20": 2, "T1-21": 3, + "T1-22": 4, "T1-23": 5, "T1-24": 6, "T1-25": 7 } + }, + "Input Card 2": { + "plugin": "InputCard", + "address": 33, + "interrupt pin": 17, + "pins": { "T1-26": 0, "T1-27": 1, "T1-28": 2, "T1-29": 3, + "T1-30": 4, "T1-31": 5, "T1-32": 6, "T1-33": 7 } + }, + "Input Card 3": { + "plugin": "InputCard", + "address": 34, + "interrupt pin": 27, + "pins": { "T2-1": 0, "T2-2": 1, "T2-3": 2, "T2-4": 3, + "T2-5": 4, "T2-6": 5, "T2-7": 6, "T2-8": 7 } + }, + "Input Card 3": { + "plugin": "InputCard", + "address": 35, + "interrupt pin": 22, + "pins": { "T2-9": 0, "T2-10": 1, "T2-11": 2, "T2-12": 3, + "T2-13": 4, "T2-14": 5, "T2-15": 6, "T2-16": 7 } + }, + "Example State": { + "plugin": "State" + } +} diff --git a/controlpi_plugins/pinio.py b/controlpi_plugins/pinio.py new file mode 100644 index 0000000..e9c7f7c --- /dev/null +++ b/controlpi_plugins/pinio.py @@ -0,0 +1,187 @@ +"""Provide plugin for GPIO pins and I2C I/O cards. + +… + +TODO: documentation, doctests +""" +import asyncio +import pigpio # type: ignore + +from typing import Dict, List + +from controlpi import BasePlugin, Message, MessageTemplate + + +_pigpio_pi = None + + +def _get_pigpio_pi(): + global _pigpio_pi + if _pigpio_pi is None: + _pigpio_pi = pigpio.pi() + # Close all handles on first access: + for h in range(32): + try: + _pigpio_pi.i2c_close(h) + except pigpio.error: + pass + return _pigpio_pi + + +class OutputCard(BasePlugin): + """… plugin. + + Do this and that. + """ + + CONF_SCHEMA = {'properties': + {'address': {'type': 'integer'}, + 'pins': + {'type': 'object', + 'patternProperties': + {'^.+$': + {'type': 'integer', + 'minimum': 0, + 'maximum': 7}}}}, + 'required': ['address', 'pins']} + + async def _receive(self, message: Message) -> None: + assert isinstance(message['target'], str) + client = message['target'] + client_pin = self._clients2pins[client] + client_pin_state = self._pins2states[client_pin] + if message['command'] == 'get state': + await self.bus.send(Message(client, + {'state': client_pin_state})) + elif message['command'] == 'set state': + # Compute new status byte for all pins of card: + byte = 0 + for pin in range(0, 8): + if pin == client_pin: + byte |= int(not message['new state']) << pin + else: + byte |= int(not self._pins2states[pin]) << pin + # Write and immediately read back status byte: + pi = _get_pigpio_pi() + pi.i2c_write_byte(self._handle, byte) + byte = pi.i2c_read_byte(self._handle) + # Send changed events for all changed clients: + for pin in range(0, 8): + new_state = not bool(byte & (1 << pin)) + if new_state != self._pins2states[pin]: + self._pins2states[pin] = new_state + for changed_client in self._pins2clients[pin]: + await self.bus.send(Message(changed_client, + {'event': 'changed', + 'state': new_state})) + # Send message without change event if target client not changed: + new_pin_state = self._pins2states[client_pin] + if new_pin_state == client_pin_state: + await self.bus.send(Message(client, + {'state': client_pin_state})) + + def process_conf(self) -> None: + """Open I2C device, read initial state and register bus clients.""" + pi = _get_pigpio_pi() + self._handle = pi.i2c_open(1, self.conf['address']) + byte = pi.i2c_read_byte(self._handle) + self._pins2states: Dict[int, bool] = {} + self._pins2clients: Dict[int, List[str]] = {} + for pin in range(0, 8): + self._pins2states[pin] = not bool(byte & (1 << pin)) + self._pins2clients[pin] = [] + sends = [MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + self._clients2pins: Dict[str, int] = {} + for client in self.conf['pins']: + pin = self.conf['pins'][client] + self._clients2pins[client] = pin + self._pins2clients[pin].append(client) + receives = [MessageTemplate({'target': {'const': client}, + 'command': {'const': 'get state'}}), + MessageTemplate({'target': {'const': client}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}})] + self.bus.register(client, 'OutputCard', + sends, receives, self._receive) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class InputCard(BasePlugin): + """… plugin. + + Do this and that. + """ + + CONF_SCHEMA = {'properties': + {'address': {'type': 'integer'}, + 'interrupt pin': {'type': 'integer'}, + 'pins': + {'type': 'object', + 'patternProperties': + {'^.+$': + {'type': 'integer', + 'minimum': 0, + 'maximum': 7}}}}, + 'required': ['address', 'pins']} + + async def _receive(self, message: Message) -> None: + assert isinstance(message['target'], str) + client = message['target'] + client_pin = self._clients2pins[client] + client_pin_state = self._pins2states[client_pin] + if message['command'] == 'get state': + await self.bus.send(Message(client, + {'state': client_pin_state})) + + def _read(self) -> None: + # Read status byte: + pi = _get_pigpio_pi() + byte = pi.i2c_read_byte(self._handle) + # Send changed events for all changed clients: + for pin in range(0, 8): + new_state = not bool(byte & (1 << pin)) + if new_state != self._pins2states[pin]: + self._pins2states[pin] = new_state + for changed_client in self._pins2clients[pin]: + self.bus.send_nowait(Message(changed_client, + {'event': 'changed', + 'state': new_state})) + + def process_conf(self) -> None: + """Open I2C device, read initial state and register bus clients.""" + pi = _get_pigpio_pi() + self._handle = pi.i2c_open(1, self.conf['address']) + byte = pi.i2c_read_byte(self._handle) + self._pins2states: Dict[int, bool] = {} + self._pins2clients: Dict[int, List[str]] = {} + for pin in range(0, 8): + self._pins2states[pin] = not bool(byte & (1 << pin)) + self._pins2clients[pin] = [] + sends = [MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + self._clients2pins: Dict[str, int] = {} + for client in self.conf['pins']: + pin = self.conf['pins'][client] + self._clients2pins[client] = pin + self._pins2clients[pin].append(client) + receives = [MessageTemplate({'target': {'const': client}, + 'command': {'const': 'get state'}})] + self.bus.register(client, 'InputCard', + sends, receives, self._receive) + pi.set_mode(self.conf['interrupt pin'], pigpio.INPUT) + pi.set_glitch_filter(self.conf['interrupt pin'], 5000) + pi.set_pull_up_down(self.conf['interrupt pin'], pigpio.PUD_UP) + loop = asyncio.get_running_loop() + pi.callback(self.conf['interrupt pin'], pigpio.EITHER_EDGE, + lambda pin, level, tick: + loop.call_soon_threadsafe(self._read)) + + async def run(self) -> None: + """Run no code proactively.""" + pass diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..0913b50 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,28 @@ +# ControlPi-Plugin für <…> +Dieses Paket enthält ein Plugin für das ControlPi-System, mit dem <…> + +## Benutzung +… + +## Installation +Eine ausführliche Dokumentation ist in der Dokumentation der +[ControlPi-Infrastruktur](https://docs.graph-it.com/graphit/controlpi) zu +finden. + +Der Code dieses Plugins kann mit git geclonet werden: +```sh +$ git clone git://git.graph-it.com/graphit/controlpi-.git +``` +(Falls Zugang zu diesem Server per SSH besteht und Änderungen gepusht +werden sollen, sollte stattdessen die SSH-URL benutzt werden.) + +Dann kann es editierbar in ein virtuelles Environment installiert werden: +```sh +(venv)$ pip install --editable +``` + +Auf dem Raspberry Pi (oder wenn keine Code-Änderungen gewünscht sind) kann +es auch direkt, ohne einen git-Clone installiert werden: +```sh +(venv)$ pip install git+git://git.graph-it.com/graphit/controlpi-.git +``` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..12922e3 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +import setuptools + +with open("README.md", "r") as readme_file: + long_description = readme_file.read() + +setuptools.setup( + name="controlpi-pinio", + version="0.1.0", + author="Graph-IT GmbH", + author_email="info@graph-it.com", + description="ControlPi Plugin for GPIO Pins and I2C I/O Cards", + long_description=long_description, + long_description_content_type="text/markdown", + url="http://docs.graph-it.com/graphit/controlpi-pinio", + packages=["controlpi_plugins"], + install_requires=[ + "pigpio", + "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git", + ], + classifiers=[ + "Programming Language :: Python", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], +) -- 2.34.1