From 2e3f805fc31da10ae292c57eab1df0bcd38201a2 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Thu, 29 Jul 2021 13:12:56 +0200 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ LICENSE | 19 ++++++++ README.md | 15 ++++++ controlpi_plugins/camera.py | 93 +++++++++++++++++++++++++++++++++++++ doc/index.md | 27 +++++++++++ setup.py | 26 +++++++++++ 6 files changed, 184 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 controlpi_plugins/camera.py create mode 100644 doc/index.md create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b0f532 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +dist/ +controlpi_camera.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..9cfe9e7 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# ControlPi Plugin for Pi Camera Module +This distribution package contains a plugin for the +[ControlPi system](https://docs.graph-it.com/graphit/controlpi), that +uses a connected Pi camera module to create snapshots at configurable +intervals and announce them on the message bus. + +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-camera/](https://docs.graph-it.com/graphit/controlpi-camera/). +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-camera/controlpi_plugins/](https://docs.graph-it.com/graphit/controlpi-camera/controlpi_plugins/). diff --git a/controlpi_plugins/camera.py b/controlpi_plugins/camera.py new file mode 100644 index 0000000..68339ae --- /dev/null +++ b/controlpi_plugins/camera.py @@ -0,0 +1,93 @@ +import aiofiles +import asyncio +import collections +import io +import os +import picamera + +from controlpi import BasePlugin, Message, MessageTemplate + + +class Camera(BasePlugin): + CONF_SCHEMA = {'properties': + {'pause': {'type': 'number'}, + 'keep': {'type': 'integer'}, + 'path': {'type': 'string'}, + 'resolution': {'type': 'string'}, + 'iso': {'type': 'integer'}}, + 'required': ['pause', 'keep', 'path']} + + async def _receive(self, message: Message) -> None: + if message['command'] = 'get image': + if len(self._images) > 0: + await self.bus.send(Message(self.name, + {'image': self._images[0]})) + elif message['command'] = 'start capture': + self._capture = True + elif message['command'] = 'stop capture': + self._capture = False + + def process_conf(self) -> None: + """Register plugin as bus client.""" + self._images = collections.deque() + self._capture = False + sends = [MessageTemplate({'event': {'const': 'new image'}, + 'image': {'type': 'string'}}), + MessageTemplate({'image': {'type': 'string'}})] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'get image'}}), + MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'start capture'}}), + MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'stop capture'}})] + self.bus.register(self.name, 'Camera', sends, receives, self._receive) + + async def run(self) -> None: + """Run camera.""" + camera = picamera.PiCamera() + stream = io.BytesIO() + camera.resolution = '1024x768' + if 'resolution' in self.conf: + camera.resolution = self.conf['resolution'] + camera.iso = 100 + if 'iso' in self.conf: + camera.iso = self.conf['iso'] + try: + while True: + if self._capture: + camera.exposure_mode = 'auto' + camera.awb_mode = 'auto' + camera.start_preview() + # Wait for auto adjustment and fix settings: + await asyncio.sleep(2) + camera.shutter_speed = camera.exposure_speed + camera.exposure_mode = 'off' + gains = camera.awb_gains + camera.awb_mode = 'off' + camera.awb_gains = gains + while self._capture: + while len(self._images) >= self.conf['keep']: + filename = self._images.popleft() + filepath = os.path.join(self.conf['path'], + filename) + await aiofiles.os.remove(filepath) + camera.capture(stream, 'jpeg') + filename = (datetime.datetime.utcnow() + .strftime('%Y%m%d%H%M%S%f') + '.jpg') + filepath = os.path.join(self.conf['path'], filename) + async with aiofiles.open(filepath, 'wb') as f: + await f.write(stream.getvalue()) + self._images.append(filename) + await self.bus.send(Message(self.name, + {'event': 'new image', + 'image': filename})) + camera.stop_preview() + await asyncio.sleep(2) + except asyncio.CancelledError: + camera.stop_preview() + while len(self._images) > 0: + filename = self._images.popleft() + filepath = os.path.join(self.conf['path'], + filename) + await aiofiles.os.remove(filepath) + raise diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..27d3fe7 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,27 @@ +# ControlPi-Plugin für Pi-Camera-Modul +Dieses Paket enthält ein Plugin für das ControlPi-System, mit dem +Einzelbilder eines angeschlossenen Pi-Camera-Modul regelmäßig in einem +gegegebenen Pfad abgelegt werden können. + +## 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-camera.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-camera.git +``` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8dfac7e --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +import setuptools + +with open("README.md", "r") as readme_file: + long_description = readme_file.read() + +setuptools.setup( + name="controlpi-camera", + version="0.1.0", + author="Graph-IT GmbH", + author_email="info@graph-it.com", + description="ControlPi Plugin for Pi Camera", + long_description=long_description, + long_description_content_type="text/markdown", + url="http://docs.graph-it.com/graphit/controlpi-camera", + packages=["controlpi_plugins"], + install_requires=[ + "aiofiles", + "picamera", + "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