Initial commit
authorBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 27 Jul 2022 09:31:07 +0000 (11:31 +0200)
committerBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 27 Jul 2022 09:31:07 +0000 (11:31 +0200)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
boot/config.txt [new file with mode: 0755]
controlpi_plugins/unipi.py [new file with mode: 0644]
doc/index.md [new file with mode: 0644]
etc/modules-load.d/unipi.conf [new file with mode: 0644]
etc/udev/rules.d/unipi.rules [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..facd30a
--- /dev/null
@@ -0,0 +1,4 @@
+__pycache__/
+dist/
+controlpi_unipi.egg-info/
+venv/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..5d57cb2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2022 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..f79dd76
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# ControlPi Plugin for UniPis
+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-unipi/](https://docs.graph-it.com/graphit/controlpi-unipi/).
+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-unipi/controlpi_plugins/](https://docs.graph-it.com/graphit/controlpi-unipi/controlpi_plugins/).
+
+
+Necessary preparations:
+* `make` and `sudo make install` unipi-kernel.
+* In unipi-kernel/device_tree:
+** `dtc -O dtb -@ -o neuron-spi.dtbo neuron-spi-new-overlay.dts`
+** `cp neuron-spi.dtbo /boot/overlays`
+* Copy configuration files from `boot/` and `etc/` in the repository to
+  filesystem.
diff --git a/boot/config.txt b/boot/config.txt
new file mode 100755 (executable)
index 0000000..7cc1b06
--- /dev/null
@@ -0,0 +1,5 @@
+dtparam=i2c=on
+dtoverlay=i2c-rtc,mcp7941x
+dtoverlay=neuron-spi
+
+initramfs initramfs-linux.img followkernel
diff --git a/controlpi_plugins/unipi.py b/controlpi_plugins/unipi.py
new file mode 100644 (file)
index 0000000..6afc1de
--- /dev/null
@@ -0,0 +1,194 @@
+import aiofiles
+import asyncio
+import glob
+
+from typing import Dict, List
+
+from controlpi import BasePlugin, Message, MessageTemplate
+
+
+class UniPi(BasePlugin):
+    CONF_SCHEMA = {}
+
+    def process_conf(self) -> None:
+        """Search all input and output pins and initialise."""
+        self._paths: Dict[str, str] = {}
+        self._states: Dict[str, bool] = {}
+        self._counters: Dict[str, int] = {}
+        for path in glob.glob('/sys/bus/platform/devices/unipi_plc/io_group?/di_?_??'):
+            client = path[46:53]
+            self._paths[client] = path
+            with open(f"{path}/{client[0:2]}_value", 'r') as sysfs_file:
+                file_state = sysfs_file.read()
+                state = False
+                if file_state[0] == '1':
+                    state = True
+                self._states[client] = state
+            with open(f"{path}/counter", 'r') as sysfs_file:
+                file_counter = sysfs_file.read()
+                self._counters[client] = int(file_counter)
+            self.bus.register(client, 'UniPiInput',
+                              [MessageTemplate({'event':
+                                                {'const': 'changed'},
+                                                'state':
+                                                {'type': 'boolean'}}),
+                               MessageTemplate({'state':
+                                                {'type': 'boolean'}}),
+                               MessageTemplate({'event':
+                                                {'const': 'changed'},
+                                                'counter':
+                                                {'type': 'integer'}}),
+                               MessageTemplate({'counter':
+                                                {'type': 'integer'}})],
+                              [([MessageTemplate({'target':
+                                                  {'const': client},
+                                                  'command':
+                                                  {'const': 'get state'}})],
+                                self._get_state),
+                               ([MessageTemplate({'target':
+                                                  {'const': client},
+                                                  'command':
+                                                  {'const': 'get counter'}})],
+                                self._get_counter),
+                               ([MessageTemplate({'target':
+                                                  {'const': client},
+                                                  'command':
+                                                  {'const': 'reset counter'}})],
+                                self._reset_counter)])
+        for path in glob.glob('/sys/bus/platform/devices/unipi_plc/io_group?/do_?_??'):
+            client = path[46:53]
+            self._paths[client] = path
+            with open(f"{path}/{client[0:2]}_value", 'r') as sysfs_file:
+                file_state = sysfs_file.read()
+                state = False
+                if file_state[0] == '1':
+                    state = True
+                self._states[client] = state
+            self.bus.register(client, 'UniPiOutput',
+                              [MessageTemplate({'event':
+                                                {'const': 'changed'},
+                                                'state':
+                                                {'type': 'boolean'}}),
+                               MessageTemplate({'state':
+                                                {'type': 'boolean'}})],
+                              [([MessageTemplate({'target':
+                                                  {'const': client},
+                                                  'command':
+                                                  {'const': 'get state'}})],
+                                self._get_state),
+                               ([MessageTemplate({'target':
+                                                  {'const': client},
+                                                  'command':
+                                                  {'const': 'set state'},
+                                                  'new state':
+                                                  {'type': 'boolean'}})],
+                                self._set_state)])
+
+    async def _get_state(self, message: Message) -> None:
+        assert isinstance(message['target'], str)
+        client = message['target']
+        path = self._paths[client]
+        # Get state from sysfs file:
+        old_state = self._states[client]
+        new_state = False
+        async with aiofiles.open(f"{path}/{client[0:2]}_value", 'r') as sysfs_file:
+            file_state = await sysfs_file.read()
+            if file_state[0] == '1':
+                new_state = True
+        self._states[client] = new_state
+        if old_state == new_state:
+            await self.bus.send(Message(client, {'state': new_state}))
+        else:
+            await self.bus.send(Message(client, {'event': 'changed',
+                                                 'state': new_state}))
+
+    async def _get_counter(self, message: Message) -> None:
+        assert isinstance(message['target'], str)
+        client = message['target']
+        path = self._paths[client]
+        # Get counter from sysfs file:
+        old_counter = self._counters[client]
+        new_counter = 0
+        async with aiofiles.open(f"{path}/counter", 'r') as sysfs_file:
+            file_counter = await sysfs_file.read()
+            new_counter = int(file_counter)
+        self._counters[client] = new_counter
+        if old_counter == new_counter:
+            await self.bus.send(Message(client, {'counter': new_counter}))
+        else:
+            await self.bus.send(Message(client, {'event': 'changed',
+                                                 'counter': new_counter}))
+
+    async def _reset_counter(self, message: Message) -> None:
+        assert isinstance(message['target'], str)
+        client = message['target']
+        path = self._paths[client]
+        # Write zero to sysfs file:
+        async with aiofiles.open(f"{path}/counter", 'w') as sysfs_file:
+            await sysfs_file.write('0')
+        # Get counter from sysfs file:
+        old_counter = self._counters[client]
+        new_counter = 0
+        async with aiofiles.open(f"{path}/counter", 'r') as sysfs_file:
+            file_counter = await sysfs_file.read()
+            new_counter = int(file_counter)
+        self._counters[client] = new_counter
+        if old_counter == new_counter:
+            await self.bus.send(Message(client, {'counter': new_counter}))
+        else:
+            await self.bus.send(Message(client, {'event': 'changed',
+                                                 'counter': new_counter}))
+
+    async def _set_state(self, message: Message) -> None:
+        assert isinstance(message['target'], str)
+        client = message['target']
+        path = self._paths[client]
+        # Write new state to sysfs file:
+        async with aiofiles.open(f"{path}/{client[0:2]}_value", 'w') as sysfs_file:
+            if message['new state']:
+                await sysfs_file.write('1')
+            else:
+                await sysfs_file.write('0')
+        # Get state from sysfs file:
+        old_state = self._states[client]
+        new_state = False
+        async with aiofiles.open(f"{path}/{client[0:2]}_value", 'r') as sysfs_file:
+            file_state = await sysfs_file.read()
+            if file_state[0] == '1':
+                new_state = True
+        self._states[client] = new_state
+        if old_state == new_state:
+            await self.bus.send(Message(client, {'state': new_state}))
+        else:
+            await self.bus.send(Message(client, {'event': 'changed',
+                                                 'state': new_state}))
+
+    async def run(self) -> None:
+        """Poll states."""
+        while True:
+            await asyncio.sleep(0.05)
+            for client in self._states:
+                path = self._paths[client]
+                # Get state from sysfs file:
+                old_state = self._states[client]
+                new_state = False
+                async with aiofiles.open(f"{path}/{client[0:2]}_value", 'r') as sysfs_file:
+                    file_state = await sysfs_file.read()
+                    if file_state[0] == '1':
+                        new_state = True
+                self._states[client] = new_state
+                if old_state != new_state:
+                    await self.bus.send(Message(client, {'event': 'changed',
+                                                         'state': new_state}))
+            for client in self._counters:
+                path = self._paths[client]
+                # Get counter from sysfs file:
+                old_counter = self._counters[client]
+                new_counter = 0
+                async with aiofiles.open(f"{path}/counter", 'r') as sysfs_file:
+                    file_counter = await sysfs_file.read()
+                    new_counter = int(file_counter)
+                self._counters[client] = new_counter
+                if old_counter != new_counter:
+                    await self.bus.send(Message(client, {'event': 'changed',
+                                                         'counter': new_counter}))
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/etc/modules-load.d/unipi.conf b/etc/modules-load.d/unipi.conf
new file mode 100644 (file)
index 0000000..50acd26
--- /dev/null
@@ -0,0 +1 @@
+unipi
diff --git a/etc/udev/rules.d/unipi.rules b/etc/udev/rules.d/unipi.rules
new file mode 100644 (file)
index 0000000..ef6b43d
--- /dev/null
@@ -0,0 +1,2 @@
+DEVPATH=="/devices/platform/unipi_plc/io_group[0-9]/di_[0-9]_[0-9][0-9]", RUN+="/usr/bin/chgrp wheel /sys%p/di_value /sys/%p/counter"
+DEVPATH=="/devices/platform/unipi_plc/io_group[0-9]/do_[0-9]_[0-9][0-9]", RUN+="/usr/bin/chgrp wheel /sys%p/do_value"
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..b3b7d39
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,24 @@
+import setuptools
+
+with open("README.md", "r") as readme_file:
+    long_description = readme_file.read()
+
+setuptools.setup(
+    name="controlpi-unipi",
+    version="0.1.0",
+    author="Graph-IT GmbH",
+    author_email="info@graph-it.com",
+    description="ControlPi Plugin for UniPis",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="http://docs.graph-it.com/graphit/controlpi-unipi",
+    packages=["controlpi_plugins"],
+    install_requires=[
+        "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git@master",
+    ],
+    classifiers=[
+        "Programming Language :: Python",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+    ],
+)