├── requirements.txt ├── .gitignore ├── LICENCE.md ├── decorators.py ├── README.md └── flask-api.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | #The MIT License (MIT)# 2 | 3 | **Copyright (c) 2014 - Rasika Amaratissa** 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /decorators.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from flask import make_response, request, current_app 3 | from functools import update_wrapper 4 | 5 | 6 | def crossdomain(origin=None, methods=None, headers=None, 7 | max_age=21600, attach_to_all=True, 8 | automatic_options=True): 9 | if methods is not None: 10 | methods = ', '.join(sorted(x.upper() for x in methods)) 11 | if headers is not None and not isinstance(headers, basestring): 12 | headers = ', '.join(x.upper() for x in headers) 13 | if not isinstance(origin, basestring): 14 | origin = ', '.join(origin) 15 | if isinstance(max_age, timedelta): 16 | max_age = max_age.total_seconds() 17 | 18 | def get_methods(): 19 | if methods is not None: 20 | return methods 21 | 22 | options_resp = current_app.make_default_options_response() 23 | return options_resp.headers['allow'] 24 | 25 | def decorator(f): 26 | def wrapped_function(*args, **kwargs): 27 | if automatic_options and request.method == 'OPTIONS': 28 | resp = current_app.make_default_options_response() 29 | else: 30 | resp = make_response(f(*args, **kwargs)) 31 | if not attach_to_all and request.method != 'OPTIONS': 32 | return resp 33 | 34 | h = resp.headers 35 | 36 | h['Access-Control-Allow-Origin'] = origin 37 | h['Access-Control-Allow-Methods'] = get_methods() 38 | h['Access-Control-Max-Age'] = str(max_age) 39 | if headers is not None: 40 | h['Access-Control-Allow-Headers'] = headers 41 | return resp 42 | 43 | f.provide_automatic_options = False 44 | return update_wrapper(wrapped_function, f) 45 | return decorator -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi GPIO API # 2 | 3 | This is a simple RESTful API built with Python on Flask to control an 8 channel relay module with the GPIO pins of a Raspberry Pi. Please note that this application can only be run on a Raspberry Pi and the GPIO pin numbering is in BCM format. 4 | 5 | ## Getting Started ## 6 | 7 | ### Step 1 - Installing Pip ### 8 | ```shell 9 | pi@raspberrypi ~ $ sudo apt-get install python-pip 10 | ``` 11 | ### Step 2 - Installing Requirements ### 12 | ```shell 13 | pi@raspberrypi ~ $ cd raspberrypi-gpio-api 14 | pi@raspberrypi ~/raspberrypi-gpio-api $ sudo pip install -r requirements.txt 15 | 16 | ``` 17 | Above command should successfully install the required dependencies. 18 | ### Step 3 - Run the development web server ### 19 | ```shell 20 | pi@raspberrypi ~/raspberrypi-gpio-api $ sudo python flask-api.py 21 | 22 | ``` 23 | That should start the development server up. 24 | ### Step 4 - Test the API ### 25 | To test if the API is running properly, open up a browser and call the ping url of the API - **{{Your RPi IP Address}}/api/v1/ping/** 26 | 27 | If the API is running properly, you should see something like this on your browser. 28 | ``` 29 | { 30 | 'api_name': 'RPi GPIO API', 31 | 'response': 'pong', 32 | 'status': 'SUCCESS', 33 | 'version': '1.0' 34 | } 35 | ``` 36 | You're all set! :D 37 | 38 | ## API endpoints ## 39 | 40 | **/api/v1/ping/** 41 | 42 | * Supported Methods - GET 43 | * GET - Returns the status of the API. 44 | * POST - Not supported. 45 | 46 | Example GET response: 47 | ``` 48 | { 49 | 'api_name': 'RPi GPIO API', 50 | 'response': 'pong', 51 | 'status': 'SUCCESS', 52 | 'version': '1.0' 53 | } 54 | ``` 55 | 56 | 57 | **/api/v1/gpio/\/** 58 | 59 | * Supported Methods - GET, POST 60 | * GET - Returns the status of specified GPIO BCM pin number. 61 | * POST - Sets the value for the specified GPIO pin. 62 | * Parameters supported 63 | * value (Valid values are 0 or 1) 64 | 65 | Example GET response: 66 | ``` 67 | { 68 | 'pin_number': 17, 69 | 'pin_name': 'Bedroom Lights', 70 | 'value': 1, 71 | 'status': 'SUCCESS', 72 | 'error': null 73 | } 74 | ``` 75 | 76 | Example POST response: 77 | ``` 78 | { 79 | 'pin_number': 17, 80 | 'pin_name': 'Bedroom Lights', 81 | 'new_value': 1, 82 | 'status': 'SUCCESS', 83 | 'error': null 84 | } 85 | ``` 86 | 87 | 88 | **/api/v1/gpio/status/** 89 | 90 | * Supported Methods - GET 91 | * GET - Returns the status all the GPIO pins. 92 | * POST - Not supported. 93 | 94 | Example GET response: 95 | ``` 96 | { 97 | 'data': [ 98 | { 99 | 'pin_number': 17, 100 | 'pin_name': 'Bedroom Lights', 101 | 'value': 1, 102 | 'status': 'SUCCESS', 103 | 'error': null 104 | }, 105 | { 106 | 'pin_number': 4, 107 | 'pin_name': 'Bedroom TV', 108 | 'value': 1, 109 | 'status': 'SUCCESS', 110 | 'error': null 111 | }, 112 | { 113 | 'pin_number': 22, 114 | 'pin_name': 'Living Room Lights', 115 | 'value': 0, 116 | 'status': 'SUCCESS', 117 | 'error': null 118 | } 119 | ] 120 | } 121 | ``` 122 | 123 | 124 | 125 | **/api/v1/gpio/all-high/** 126 | 127 | * Supported Methods - POST 128 | * GET - Not supported. 129 | * POST - Sets the GPIO value to 1 on all the pins. 130 | 131 | Example POST response: 132 | ``` 133 | { 134 | 'data': [ 135 | { 136 | 'pin_number': 17, 137 | 'pin_name': 'Bedroom Lights', 138 | 'new_value': 1, 139 | 'status': 'SUCCESS', 140 | 'error': null 141 | }, 142 | { 143 | 'pin_number': 4, 144 | 'pin_name': 'Bedroom TV', 145 | 'new_value': 1, 146 | 'status': 'SUCCESS', 147 | 'error': null 148 | }, 149 | { 150 | 'pin_number': 22, 151 | 'pin_name': 'Living Room Lights', 152 | 'new_value': 1, 153 | 'status': 'SUCCESS', 154 | 'error': null 155 | } 156 | ] 157 | } 158 | ``` 159 | 160 | 161 | **/api/v1/gpio/all-low/** 162 | 163 | * Supported Methods - POST 164 | * GET - Not supported. 165 | * POST - Sets the GPIO value to 0 on all the pins. 166 | 167 | Example POST response: 168 | ``` 169 | { 170 | 'data': [ 171 | { 172 | 'pin_number': 17, 173 | 'pin_name': 'Bedroom Lights', 174 | 'new_value': 0, 175 | 'status': 'SUCCESS', 176 | 'error': null 177 | }, 178 | { 179 | 'pin_number': 4, 180 | 'pin_name': 'Bedroom TV', 181 | 'new_value': 0, 182 | 'status': 'SUCCESS', 183 | 'error': null 184 | }, 185 | { 186 | 'pin_number': 22, 187 | 'pin_name': 'Living Room Lights', 188 | 'new_value': 0, 189 | 'status': 'SUCCESS', 190 | 'error': null 191 | } 192 | ] 193 | } 194 | ``` 195 | 196 | ## Future Tasks ## 197 | * Add API authentication. 198 | -------------------------------------------------------------------------------- /flask-api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask, jsonify, request 4 | from flask.ext.login import LoginManager, UserMixin, login_required 5 | from itsdangerous import URLSafeTimedSerializer 6 | 7 | import RPi.GPIO as GPIO 8 | from decorators import crossdomain 9 | 10 | app = Flask(__name__) 11 | app.debug = True 12 | 13 | app.secret_key = 'my_#$%^&_security_&*(4_key' 14 | 15 | #Login_serializer used to encryt and decrypt the cookie token for the remember 16 | #me option of flask-login 17 | 18 | login_serializer = URLSafeTimedSerializer(app.secret_key) 19 | login_manager = LoginManager() 20 | login_manager.init_app(app) 21 | 22 | class User(UserMixin): 23 | # proxy for a database of users 24 | user_database = {'admin': ('admin', 'pass', 'WyJhZG1pbiIsInBhc3MiXQ.B5Xusw.6ZGpDxCdcZL7HrkHTP97aZ9dfPA')} 25 | 26 | def __init__(self, username, password, token): 27 | self.id = username 28 | self.password = password 29 | self.token = token 30 | 31 | @classmethod 32 | def get(cls, id): 33 | return cls.user_database.get(id) 34 | 35 | def hash_pass(password): 36 | """ 37 | Return the md5 hash of the password+salt 38 | """ 39 | salted_password = password + app.secret_key 40 | return md5.new(salted_password).hexdigest() 41 | 42 | def gen_auth_token(user, password): 43 | """ 44 | Encode a secure token for cookie 45 | """ 46 | data = [str(user), password] 47 | return login_serializer.dumps(data) 48 | 49 | @login_manager.request_loader 50 | def load_user(request): 51 | auth_header = request.headers.get('X-Auth') 52 | token_header = request.headers.get('X-Auth-Token') 53 | data = [] 54 | 55 | if auth_header is not None: 56 | (username, password) = auth_header.split(':') 57 | user_entry = User.get(username) 58 | if user_entry is not None: 59 | user = User(user_entry[0], user_entry[1], user_entry[2]) 60 | if user.password == password: 61 | if token_header == user.token: 62 | data = login_serializer.loads(user.token) 63 | token_user = User.get(data[0]) 64 | #Check Password and return user or None 65 | if token_user == user_entry and data[1] == password: 66 | return user 67 | return None 68 | 69 | @login_manager.unauthorized_handler 70 | def unauthorized(): 71 | data = {'status': 'ERROR', 72 | 'error': 'Auth requerided'} 73 | return jsonify(data) 74 | 75 | VALID_BCM_PIN_NUMBERS = [17, 18, 27, 22, 23, 24, 25, 4] 76 | VALID_HIGH_VALUES = [1, '1', 'HIGH'] 77 | VALID_LOW_VALUES = [0, '0', 'LOW'] 78 | PIN_NAMES = {'17': 'IN1', 79 | '18': 'IN2', 80 | '27': 'IN3', 81 | '22': 'IN4', 82 | '23': 'IN5', 83 | '24': 'IN6', 84 | '25': 'IN7', 85 | '4': 'IN8'} 86 | 87 | GPIO.setmode(GPIO.BCM) 88 | 89 | for pin in VALID_BCM_PIN_NUMBERS: 90 | GPIO.setup(pin, GPIO.OUT) 91 | 92 | 93 | def pin_status(pin_number): 94 | if pin_number in VALID_BCM_PIN_NUMBERS: 95 | value = GPIO.input(pin_number) 96 | data = {'pin_number': pin_number, 97 | 'pin_name': PIN_NAMES[str(pin_number)], 98 | 'value': value, 99 | 'status': 'SUCCESS', 100 | 'error': None} 101 | else: 102 | data = {'status': 'ERROR', 103 | 'error': 'Invalid pin number.'} 104 | 105 | return data 106 | 107 | 108 | def pin_update(pin_number, value): 109 | if pin_number in VALID_BCM_PIN_NUMBERS: 110 | GPIO.output(pin_number, value) 111 | new_value = GPIO.input(pin_number) 112 | data = {'status': 'SUCCESS', 113 | 'error': None, 114 | 'pin_number': pin_number, 115 | 'pin_name': PIN_NAMES[str(pin_number)], 116 | 'new_value': new_value} 117 | else: 118 | data = {'status': 'ERROR', 119 | 'error': 'Invalid pin number or value.'} 120 | 121 | return data 122 | 123 | 124 | @app.route("/api/v1/ping/", methods=['GET']) 125 | @crossdomain(origin='*') 126 | def api_status(): 127 | if request.method == 'GET': 128 | data = {'api_name': 'RPi GPIO API', 129 | 'version': '1.0', 130 | 'status': 'SUCCESS', 131 | 'response': 'pong'} 132 | return jsonify(data) 133 | 134 | 135 | @app.route("/api/v1/gpio//", methods=['POST', 'GET']) 136 | @crossdomain(origin='*') 137 | @login_required 138 | def gpio_pin(pin_number): 139 | pin_number = int(pin_number) 140 | if request.method == 'GET': 141 | data = pin_status(pin_number) 142 | 143 | elif request.method == 'POST': 144 | value = request.values['value'] 145 | if value in VALID_HIGH_VALUES: 146 | data = pin_update(pin_number, 1) 147 | elif value in VALID_LOW_VALUES: 148 | data = pin_update(pin_number, 0) 149 | else: 150 | data = {'status': 'ERROR', 151 | 'error': 'Invalid value.'} 152 | return jsonify(data) 153 | 154 | 155 | @app.route("/api/v1/gpio/status/", methods=['GET']) 156 | @crossdomain(origin='*') 157 | def gpio_status(): 158 | data_list = [] 159 | for pin in VALID_BCM_PIN_NUMBERS: 160 | data_list.append(pin_status(pin)) 161 | 162 | data = {'data': data_list} 163 | return jsonify(data) 164 | 165 | 166 | @app.route("/api/v1/gpio/all-high/", methods=['POST']) 167 | @crossdomain(origin='*') 168 | def gpio_all_high(): 169 | data_list = [] 170 | for pin in VALID_BCM_PIN_NUMBERS: 171 | data_list.append(pin_update(pin, 1)) 172 | 173 | data = {'data': data_list} 174 | return jsonify(data) 175 | 176 | 177 | @app.route("/api/v1/gpio/all-low/", methods=['POST']) 178 | @crossdomain(origin='*') 179 | def gpio_all_low(): 180 | data_list = [] 181 | for pin in VALID_BCM_PIN_NUMBERS: 182 | data_list.append(pin_update(pin, 0)) 183 | 184 | data = {'data': data_list} 185 | return jsonify(data) 186 | 187 | 188 | if __name__ == "__main__": 189 | app.run(host='0.0.0.0', port=80, debug=True) 190 | --------------------------------------------------------------------------------