From bfe5ff5010a35e5845e0439b64ebe468409534d8 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Wed, 27 Jul 2022 11:31:07 +0200 Subject: [PATCH] Initial commit --- .gitignore | 4 + LICENSE | 19 ++++ README.md | 23 ++++ boot/config.txt | 5 + controlpi_plugins/unipi.py | 194 ++++++++++++++++++++++++++++++++++ doc/index.md | 28 +++++ etc/modules-load.d/unipi.conf | 1 + etc/udev/rules.d/unipi.rules | 2 + setup.py | 24 +++++ 9 files changed, 300 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 boot/config.txt create mode 100644 controlpi_plugins/unipi.py create mode 100644 doc/index.md create mode 100644 etc/modules-load.d/unipi.conf create mode 100644 etc/udev/rules.d/unipi.rules create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..facd30a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +dist/ +controlpi_unipi.egg-info/ +venv/ diff --git a/LICENSE b/LICENSE new file mode 100644 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 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 index 0000000..7cc1b06 --- /dev/null +++ b/boot/config.txt @@ -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 index 0000000..6afc1de --- /dev/null +++ b/controlpi_plugins/unipi.py @@ -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 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/etc/modules-load.d/unipi.conf b/etc/modules-load.d/unipi.conf new file mode 100644 index 0000000..50acd26 --- /dev/null +++ b/etc/modules-load.d/unipi.conf @@ -0,0 +1 @@ +unipi diff --git a/etc/udev/rules.d/unipi.rules b/etc/udev/rules.d/unipi.rules new file mode 100644 index 0000000..ef6b43d --- /dev/null +++ b/etc/udev/rules.d/unipi.rules @@ -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 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", + ], +) -- 2.34.1