Initial commit: I/O cards.
authorBenjamin Braatz <bb@bbraatz.eu>
Wed, 24 Mar 2021 00:18:22 +0000 (01:18 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Wed, 24 Mar 2021 00:18:22 +0000 (01:18 +0100)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
conf.json [new file with mode: 0644]
controlpi_plugins/pinio.py [new file with mode: 0644]
doc/index.md [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..756c751
--- /dev/null
@@ -0,0 +1,4 @@
+__pycache__/
+dist/
+controlpi_pinio.egg-info/
+venv/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
index 0000000..e9c7f7c
--- /dev/null
@@ -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 (file)
index 0000000..0913b50
--- /dev/null
@@ -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-<plugin>.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 <Pfad zum Code-Repository>
+```
+
+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-<plugin>.git
+```
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
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",
+    ],
+)