├── .gitignore ├── LICENSE ├── README.md ├── docs ├── installation.md ├── logo-256.png └── pimotion-demo-capture.jpg ├── pimotion ├── __init__.py ├── cloudapp │ ├── __init__.py │ └── exceptions.py ├── main.py └── pimotion.py ├── requirements.txt ├── settings.cfg.example └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /settings.cfg 2 | 3 | /pimotion/pimotion.pyc 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jelle Henkens for Citrusbyte, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pimotion 2 | ======== 3 | 4 | ![PIMotion Logo](docs/logo-256.png?raw=true) 5 | 6 | Pimotion is a motion detector application that runs on the Raspberry PI. It captures snapshots of movement and uploads the montage image to an [M2X](https://m2x.att.com) device. 7 | 8 | ### Prerequisites 9 | 10 | * A free [AT&T M2X](https://m2x.att.com) developer account 11 | * A [CloudApp](https://www.getcloudapp.com/) account. Note that the free CloudApp plan is limited to 10 uploads per day, you can [upgrade your CloudApp account](https://www.getcloudapp.com/plans) to allow for unlimited uploads. 12 | 13 | ### Installation 14 | 15 | Detailed installation instructions can be found at [docs/installation.md](docs/installation.md) 16 | 17 | ### Running 18 | 19 | Once you've completed steps outlined in the [installation](docs/installation.md) instructions, the application can be started by running: 20 | 21 | $ python pimotion/main.py 22 | 23 | ### Example Capture 24 | ![pimotion-demo-capture](https://raw.githubusercontent.com/citrusbyte/pimotion/master/docs/pimotion-demo-capture.jpg) 25 | 26 | ### License 27 | 28 | This project is delivered under the MIT license. See [LICENSE](LICENSE) for the terms. 29 | 30 | ### Attributions 31 | 32 | Written by [Sarah Henkens](https://github.com/sarahhenkens), sponsored by [Citrusbyte](https://citrusbyte.com/) 33 | 34 | ## About Citrusbyte 35 | 36 | ![Citrusbyte](http://i.imgur.com/W6eISI3.png) 37 | 38 | Pimotion is lovingly maintained and funded by Citrusbyte. 39 | At Citrusbyte, we specialize in solving difficult computer science problems for startups and the enterprise. 40 | 41 | At Citrusbyte we believe in and support open source software. 42 | * Check out more of our open source software at Citrusbyte Labs. 43 | * Learn more about [our work](https://citrusbyte.com/portfolio). 44 | * [Hire us](https://citrusbyte.com/contact) to work on your project. 45 | * [Want to join the team?](http://careers.citrusbyte.com) 46 | 47 | *Citrusbyte and the Citrusbyte logo are trademarks or registered trademarks of Citrusbyte, LLC.* 48 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | Installation Guide 2 | ================== 3 | 4 | This guide will explain step by step how to set up your Raspberry PI to run the pimotion application. 5 | 6 | ## Raspberry PI Setup 7 | 8 | ### Hardware 9 | 10 | The camera module is required for this application to run. For the installation of the hardware I refer to the [official guide](http://www.raspberrypi.org/help/camera-module-setup). 11 | 12 | ### Operating System 13 | 14 | I recommend the [operating system name] OS, this is the OS that the pimotion application has been developed on. 15 | 16 | ### Enabling the camera module 17 | 18 | There are 2 ways of enabling the camera module: 19 | 20 | 1. During the OS installation 21 | During the installation of the operating system, you will be promted for additional actions to take. In this menu, one of the options is to enable/disable the camera module. 22 | 23 | 2. After installation 24 | If your Raspberry PI already has a running operating system you can call up the same setup menu by running the following command: 25 | 26 | ``` 27 | $ sudo raspi-config 28 | ``` 29 | 30 | Select `Enable camera` and hit `Enter`, then go to `Finish` and you'll be prompted to reboot. 31 | 32 | ### Testing the camera 33 | 34 | Make sure your Raspberry PI is connected to a monitor/tv with an hdmi cable and login to the shell. Make sure to login on the PI itself, not through SSH. 35 | 36 | In the shell, run the following command to trigger the preview window of the camera: 37 | 38 | ``` 39 | $ raspistill -d 40 | ``` 41 | 42 | You should see a preview window for a couple of seconds which closes by itself. If you see this, your camera module is working correctly. 43 | 44 | In case the preview does not show or you receive an error, refer to the [troubleshooting guide](http://www.raspberrypi.org/documentation/troubleshooting/hardware/camera.md) on the Raspberry PI website. 45 | 46 | ## Preparing the system 47 | 48 | ### Required packages 49 | 50 | Run the following command to install the required system packages: 51 | 52 | ``` 53 | $ sudo apt-get update 54 | $ sudo apt-get install -y python-dev python-pip imagemagick libffi-dev libssl-dev git 55 | ``` 56 | 57 | ## Pimotion application installation 58 | 59 | ### Python 60 | 61 | Confirm that python is installed on the system. 62 | 63 | ``` 64 | $ python --version 65 | Python 2.7.3 66 | ``` 67 | 68 | ### Getting the source code 69 | 70 | Run the following command to pull the latest version of Pimotion: 71 | 72 | ``` 73 | $ mkdir ~/projects 74 | $ cd ~/projects 75 | $ git clone https://github.com/citrusbyte/pimotion.git 76 | ``` 77 | 78 | ### Dependency installation 79 | 80 | In the pimotion folder run the following commands to install all the dependent python packages: 81 | 82 | ``` 83 | $ sudo pip install http://effbot.org/downloads/Imaging-1.1.7.tar.gz 84 | $ sudo pip install -r requirements.txt 85 | ``` 86 | 87 | ### Additional setup 88 | 89 | Create the tmp folder that will hold the captured images: 90 | 91 | ``` 92 | $ mkdir ~/projects/pimotion/captures 93 | ``` 94 | 95 | ### Configuration 96 | 97 | Make a copy of the provided `settings.cfg.example` configuration file: 98 | 99 | ``` 100 | $ cd ~/projects/pimotion 101 | $ cp settings.cfg.example settings.cfg 102 | ``` 103 | 104 | Open up the `settings.cfg` file in your favorite editor and update the configuration values. 105 | 106 | __cloudapp__: 107 | * username: Your CloudApp username 108 | * password: Your CloudApp password 109 | 110 | __m2x__: 111 | * api_key: Your master API key or the device specific API key 112 | * device_id: The Device ID 113 | * stream: The name of the stream in your M2X device 114 | 115 | ### Running pimotion 116 | 117 | Make sure you are in the correct working directory: 118 | 119 | ``` 120 | $ cd ~/projects/pimotion 121 | ``` 122 | 123 | Start the application with the following command: 124 | 125 | ``` 126 | $ python pimotion/main.py 127 | ``` 128 | 129 | Example output of a correct running pimotion application: 130 | 131 | $ python pimotion/main.py 132 | Waiting 2 seconds for the camera to warm up 133 | Started recording 134 | Started working on capturing 135 | Captured detected-01.jpg 136 | Captured detected-02.jpg 137 | Captured detected-03.jpg 138 | Captured detected-04.jpg 139 | Captured detected-05.jpg 140 | Captured detected-06.jpg 141 | Captured detected-07.jpg 142 | Captured detected-08.jpg 143 | Captured detected-09.jpg 144 | Captured detected-10.jpg 145 | Captured detected-11.jpg 146 | Captured detected-12.jpg 147 | Captured detected-13.jpg 148 | Captured detected-14.jpg 149 | Captured detected-15.jpg 150 | Generating the montage 151 | Finished capturing 152 | Public URL: http://cl.ly/image/*******/download/montage.jpg 153 | Posted URL to M2X channel motion-photos 154 | 155 | -------------------------------------------------------------------------------- /docs/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/pimotion/ee683ad49d29d0d2cc41d0f34b722d34dddd72b4/docs/logo-256.png -------------------------------------------------------------------------------- /docs/pimotion-demo-capture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/pimotion/ee683ad49d29d0d2cc41d0f34b722d34dddd72b4/docs/pimotion-demo-capture.jpg -------------------------------------------------------------------------------- /pimotion/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/pimotion/ee683ad49d29d0d2cc41d0f34b722d34dddd72b4/pimotion/__init__.py -------------------------------------------------------------------------------- /pimotion/cloudapp/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests.auth import HTTPDigestAuth 3 | import json 4 | 5 | from cloudapp.exceptions import CloudAppHttpError 6 | 7 | class CloudAppAPI: 8 | 9 | def __init__(self, username, password): 10 | self.username = username 11 | self.password = password 12 | 13 | def upload(self, path): 14 | data = self.__get('http://my.cl.ly/items/new') 15 | # TODO: Do something with data['uploads_remaining'] 16 | url = data['url'] 17 | params = data['params'] 18 | 19 | headers = {'accept': 'application/json'} 20 | response = requests.post(url, files={'file': open(path, 'rb')}, data=params, allow_redirects=False) 21 | uri = response.headers['location'] 22 | data = self.__get(uri) 23 | return data['download_url'] 24 | 25 | def __get(self, uri): 26 | headers = {'accept': 'application/json'} 27 | r = requests.get(uri, auth=HTTPDigestAuth(self.username, self.password), 28 | headers=headers) 29 | if r.status_code != 200: 30 | raise CloudAppHttpError(response=r) 31 | return json.loads(r.text) 32 | -------------------------------------------------------------------------------- /pimotion/cloudapp/exceptions.py: -------------------------------------------------------------------------------- 1 | class CloudAppError(Exception): 2 | def __init__(self, message=None): 3 | super(CloudAppError, self).__init__(message) 4 | 5 | 6 | class CloudAppHttpError(Exception): 7 | def __init__(self, response=None): 8 | message = str(response.status_code) + ': ' + response.text 9 | super(CloudAppHttpError, self).__init__(message) 10 | -------------------------------------------------------------------------------- /pimotion/main.py: -------------------------------------------------------------------------------- 1 | from pimotion import PiMotion 2 | from cloudapp import CloudAppAPI 3 | from cloudapp.exceptions import CloudAppHttpError 4 | from m2x.client import M2XClient 5 | from requests.exceptions import HTTPError, RequestException 6 | import ConfigParser 7 | 8 | 9 | Config = ConfigParser.ConfigParser() 10 | Config.read('settings.cfg') 11 | 12 | 13 | def callback(path): 14 | client = M2XClient(Config.get('m2x', 'api_key')) 15 | try: 16 | api = CloudAppAPI(Config.get('cloudapp', 'username'), Config.get('cloudapp', 'password')) 17 | url = api.upload(path) 18 | print 'Public URL: ' + url 19 | 20 | stream = client.device(Config.get('m2x', 'device_id')).stream(Config.get('m2x', 'stream')) 21 | result = stream.add_value(url) 22 | print "Posted URL to M2X stream %s" % Config.get('m2x', 'stream') 23 | except HTTPError, e: 24 | print 'ERROR: ' 25 | print ' STATUS: %s' % client.last_response.status 26 | print ' HEADERS: %s' % client.last_response.headers 27 | print ' BODY: %s' % client.last_response.json 28 | except (RequestException, CloudAppHttpError), e: 29 | print 'CloudApp ERROR:' + e.message 30 | 31 | 32 | motion = PiMotion(verbose=True, post_capture_callback=callback) 33 | motion.start() 34 | -------------------------------------------------------------------------------- /pimotion/pimotion.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import time 4 | import os 5 | import subprocess 6 | import datetime 7 | 8 | import picamera 9 | import picamera.array 10 | import numpy as np 11 | 12 | 13 | class CaptureHandler: 14 | def __init__(self, camera, post_capture_callback=None): 15 | self.camera = camera 16 | self.callback = post_capture_callback 17 | self.detected = False 18 | self.working = False 19 | self.i = 0 20 | 21 | def motion_detected(self): 22 | if not self.working: 23 | self.detected = True 24 | 25 | def tick(self): 26 | if self.detected: 27 | print "Started working on capturing" 28 | self.working = True 29 | self.detected = False 30 | self.i += 1 31 | 32 | path = "captures/%s/" % datetime.datetime.now().isoformat() 33 | 34 | os.makedirs(path) 35 | 36 | self.camera.start_preview() 37 | 38 | for x in range(1, 16): 39 | filename = "detected-%02d.jpg" % x 40 | self.camera.capture(path + filename, use_video_port=True) 41 | print "Captured " + filename 42 | 43 | self.camera.stop_preview() 44 | 45 | print "Generating the montage" 46 | montage_file = path + 'montage.jpg' 47 | subprocess.call("montage -border 0 -background none -geometry 240x180 " + path + "* " + montage_file, shell=True) 48 | 49 | print "Finished capturing" 50 | 51 | if self.callback: 52 | self.callback(montage_file) 53 | 54 | self.working = False 55 | 56 | 57 | class MyMotionDetector(picamera.array.PiMotionAnalysis): 58 | def __init__(self, camera, handler): 59 | super(MyMotionDetector, self).__init__(camera) 60 | self.handler = handler 61 | self.first = True 62 | 63 | def analyse(self, a): 64 | a = np.sqrt( 65 | np.square(a['x'].astype(np.float)) + 66 | np.square(a['y'].astype(np.float)) 67 | ).clip(0, 255).astype(np.uint8) 68 | if (a > 60).sum() > 50: 69 | # Ignore the first detection 70 | if self.first: 71 | self.first = False 72 | return 73 | self.handler.motion_detected() 74 | 75 | 76 | class PiMotion: 77 | def __init__(self, verbose=False, post_capture_callback=None): 78 | self.verbose = verbose 79 | self.post_capture_callback = post_capture_callback 80 | 81 | def __print(self, str): 82 | if self.verbose: 83 | print str 84 | 85 | def start(self): 86 | with picamera.PiCamera() as camera: 87 | camera.resolution = (1280, 960) 88 | camera.framerate = 10 89 | 90 | handler = CaptureHandler(camera, self.post_capture_callback) 91 | 92 | self.__print('Waiting 2 seconds for the camera to warm up') 93 | time.sleep(2) 94 | 95 | try: 96 | self.__print('Started recording') 97 | camera.start_recording( 98 | '/dev/null', format='h264', 99 | motion_output=MyMotionDetector(camera, handler) 100 | ) 101 | 102 | while True: 103 | handler.tick() 104 | time.sleep(1) 105 | finally: 106 | camera.stop_recording() 107 | self.__print('Stopped recording') 108 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.6.2 2 | PIL==1.1.7 3 | requests==2.10.0 4 | picamera==1.10 5 | m2x==5.2.0 6 | pyopenssl==16.0.0 7 | ndg-httpsclient==0.4.0 8 | pyasn1==0.1.9 9 | -------------------------------------------------------------------------------- /settings.cfg.example: -------------------------------------------------------------------------------- 1 | [cloudapp] 2 | username=__your_cloudapp_email__ 3 | password=__your_cloudapp_password__ 4 | 5 | [m2x] 6 | api_key= 7 | device_id= 8 | stream= 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/pimotion/ee683ad49d29d0d2cc41d0f34b722d34dddd72b4/setup.py --------------------------------------------------------------------------------