├── .gitignore ├── GoProController ├── __init__.py ├── models.py ├── settings.py ├── urls.py └── wsgi.py ├── LICENSE ├── README.md ├── apache.conf ├── diagram.png ├── diagram.xml ├── manage.py ├── proxy.py ├── scripts ├── goprologger └── goprospammer ├── setup.py ├── setup.sh └── upstart.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | *~ 38 | sqlite3.db 39 | static/ 40 | output/ 41 | -------------------------------------------------------------------------------- /GoProController/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshvillbrandt/GoProController/454b1256044a411ce208a42191154309e4e2fb7a/GoProController/__init__.py -------------------------------------------------------------------------------- /GoProController/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Camera(models.Model): 5 | ssid = models.CharField(max_length=255) 6 | password = models.CharField(max_length=255) 7 | date_added = models.DateTimeField(auto_now_add=True) 8 | last_attempt = models.DateTimeField(auto_now=True) 9 | last_update = models.DateTimeField(null=True, blank=True) 10 | image_last_update = models.DateTimeField(null=True, blank=True) 11 | image = models.TextField(blank=True) 12 | summary = models.TextField(blank=True) 13 | status = models.TextField(blank=True) 14 | connection_attempts = models.IntegerField(default=0) 15 | connection_failures = models.IntegerField(default=0) 16 | 17 | def __unicode__(self): 18 | return self.ssid 19 | 20 | 21 | class Command(models.Model): 22 | camera = models.ForeignKey(Camera) 23 | command = models.CharField(max_length=255) 24 | value = models.CharField(max_length=255, blank=True) 25 | date_added = models.DateTimeField(auto_now_add=True) 26 | time_completed = models.DateTimeField(null=True, blank=True) 27 | 28 | def __unicode__(self): 29 | return self.camera.__unicode__() + ' > ' + self.command 30 | -------------------------------------------------------------------------------- /GoProController/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for GoProController project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/dev/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/dev/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'b=0h7=c6fp=czo4yg)b8!x73d!9)am-&j!mjbfb5)t*!z53=k8' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'rest_framework', 40 | 'corsheaders', 41 | 'GoProController', 42 | ) 43 | 44 | REST_FRAMEWORK = { 45 | # Use Django's standard `django.contrib.auth` permissions, 46 | # or allow read-only access for unauthenticated users. 47 | 'DEFAULT_PERMISSION_CLASSES': [ 48 | 'rest_framework.permissions.AllowAny' 49 | ], 50 | 'PAGINATE_BY_PARAM': 'limit', 51 | } 52 | 53 | MIDDLEWARE_CLASSES = ( 54 | 'corsheaders.middleware.CorsMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.messages.middleware.MessageMiddleware', 60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 61 | ) 62 | 63 | ROOT_URLCONF = 'GoProController.urls' 64 | 65 | WSGI_APPLICATION = 'GoProController.wsgi.application' 66 | 67 | 68 | # Database 69 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 70 | 71 | DATABASES = { 72 | 'default': { 73 | 'ENGINE': 'django.db.backends.sqlite3', 74 | 'NAME': os.path.join(BASE_DIR, 'sqlite3.db'), 75 | } 76 | } 77 | 78 | # Internationalization 79 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 80 | 81 | LANGUAGE_CODE = 'en-us' 82 | 83 | TIME_ZONE = 'UTC' 84 | 85 | USE_I18N = True 86 | 87 | USE_L10N = True 88 | 89 | USE_TZ = True 90 | 91 | 92 | # Static files (CSS, JavaScript, Images) 93 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 94 | 95 | STATIC_ROOT = os.path.join(BASE_DIR, "GoProController", "static") 96 | 97 | STATIC_URL = '/static/' 98 | 99 | CORS_ORIGIN_ALLOW_ALL = True 100 | -------------------------------------------------------------------------------- /GoProController/urls.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.conf.urls import url, include 3 | from GoProController.models import Camera, Command 4 | from rest_framework import serializers, viewsets, routers, filters 5 | from django.http import HttpResponse 6 | from goprohero import GoProHero 7 | 8 | 9 | # Serializers define the API representation. 10 | class CameraSerializer(serializers.ModelSerializer): 11 | class Meta: 12 | model = Camera 13 | 14 | 15 | class CommandSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = Command 18 | 19 | 20 | # ViewSets define the view behavior. 21 | class CameraViewSet(viewsets.ModelViewSet): 22 | queryset = Camera.objects.all() 23 | serializer_class = CameraSerializer 24 | filter_backends = (filters.OrderingFilter,) 25 | 26 | 27 | class CommandViewSet(viewsets.ModelViewSet): 28 | queryset = Command.objects.all() 29 | serializer_class = CommandSerializer 30 | filter_backends = (filters.OrderingFilter,) 31 | 32 | 33 | # Routers provide a way of automatically determining the URL conf. 34 | router = routers.DefaultRouter(trailing_slash=False) 35 | router.register(r'cameras', CameraViewSet) 36 | router.register(r'commands', CommandViewSet) 37 | 38 | 39 | # A view to return the GoProHero config dictionary 40 | def ConfigView(request): 41 | data = GoProHero.config() 42 | return HttpResponse(json.dumps(data), content_type="application/json") 43 | 44 | 45 | # Wire up our API using automatic URL routing. 46 | urlpatterns = [ 47 | url(r'^', include(router.urls)), 48 | url(r'^config', ConfigView) 49 | ] 50 | -------------------------------------------------------------------------------- /GoProController/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for GoProSite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import sys 11 | sys.path.append('/home/GoProController') 12 | 13 | import os 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "GoProController.settings") 15 | 16 | from django.core.wsgi import get_wsgi_application 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoProController 2 | 3 | An http API to control multiple GoPro cameras over wifi. 4 | 5 | ## Description 6 | 7 | This program can be used to control multiple GoPro cameras via the [goprohero](https://github.com/joshvillbrandt/goprohero) Python library. When ran from a Linux machine with compatible wireless card, this program is capable of automatically negotiating between the different ad-hoc wireless networks that each cameras makes. 8 | 9 | A user interface is available for this API as a standalone package. See [GoProControllerUI](https://github.com/joshvillbrandt/GoProControllerUI). 10 | 11 | ## How it works 12 | 13 | The backbone of GoProApp is a program called `GoProProxy` that runs asynchronously to the server. This proxy periodically grabs the status of every camera in the database and sends commands to cameras when appropriate. The proxy uses [wifi](https://github.com/rockymeza/wifi) to jump between networks and [goprohero](https://github.com/joshvillbrandt/goprohero) to handle the communication to the cameras. A Django app is used to persist data from the proxy and serve API endpoints. 14 | 15 | ![diagram](diagram.png) 16 | 17 | Note: The xml version of the above diagram can be modified with [https://www.draw.io/](https://www.draw.io/). 18 | 19 | ## Production Setup (Ubuntu) 20 | 21 | First, download the code: 22 | 23 | ```bash 24 | git clone https://github.com/joshvillbrandt/GoProController.git ~/GoProController 25 | sudo ln -s ~/GoProController /home/GoProController 26 | ``` 27 | 28 | If you are running Ubuntu, use the `setup.sh` script to automatically set up the application in a production mode: 29 | 30 | ```bash 31 | sudo /home/GoProController/setup.sh 32 | ``` 33 | 34 | Upon completion of `setup.sh`, you should now be able to navigate to [http://localhost/](http://localhost/) and see the API. In addition, the `GoProApp/proxy.py` file is also now running continuously to the local wifi adapter and communicate with the cameras. 35 | 36 | You can interact with the server and proxy using `service` and `initctl`: 37 | 38 | ```bash 39 | sudo service apache2 status 40 | sudo initctl status gopro-proxy 41 | ``` 42 | 43 | Logs for both are also available: 44 | 45 | ```bash 46 | tail /var/log/apache2/error.log 47 | tail -f /var/log/gopro-proxy.log 48 | ``` 49 | 50 | To set a specific interface for wifi control, add the following to [upstart.conf](upstart.conf): 51 | 52 | ``` 53 | env GOPRO_WIFI_INTERFACE=wlan1 54 | ``` 55 | 56 | ## Development Setup (Ubuntu, Mac) 57 | 58 | To run GoProApp without Apache and Upstart, launch the site with the Django development server: 59 | 60 | ```bash 61 | git clone https://github.com/joshvillbrandt/GoProController.git ~/GoProController 62 | cd ~/GoProController 63 | sudo python setup.py install 64 | python manage.py runserver 0.0.0.0:8000 65 | ``` 66 | 67 | In another terminal window, launch the proxy to communicate with the cameras: 68 | 69 | ```bash 70 | sudo python ~/GoProController/proxy.py # sudo needed for logging (or add yourself to syslog in Ubuntu) 71 | ``` 72 | 73 | You should now be able to navigate to [http://localhost:8000/](http://localhost:8000/) and see the API. 74 | 75 | To set a specific interface for wifi control, add the following environment variable before the proxy command: 76 | 77 | ```bash 78 | sudo GOPRO_WIFI_INTERFACE='wlan1' python ~/GoProController/proxy.py 79 | ``` 80 | 81 | ## API 82 | 83 | This API provides the following endpoints: 84 | 85 | Endpoint | Actions 86 | --- | --- 87 | `/cameras` | GET, POST 88 | `/cameras/:id` | GET, PUT 89 | `/commands`| GET, POST 90 | `/commands/:id` | GET, PUT 91 | 92 | The API if build on the [Django REST Framework](http://www.django-rest-framework.org/). Please reference their documentation for detailed querying information. 93 | 94 | ## Change History 95 | 96 | This project uses [semantic versioning](http://semver.org/). 97 | 98 | ### v0.2.4 - 2015/01/29 99 | 100 | * Added `GOPRO_SNAPSHOTS` environment flag to turn off grabbing snapshots images if desired 101 | * The proxy now fails commands when it can't find the camera instead of leaving them in the queue to block everybody else 102 | * Added [spammer.py](spammer.py) - try `sudo goprospammer -p record -v on` 103 | * Added [logger.py](logger.py) - try `goprologger -d output` 104 | 105 | ### v0.2.3 - 2015/01/14 106 | 107 | * Updated `goprohero` and `wireless` library versions 108 | 109 | ### v0.2.2 - 2015/01/06 110 | 111 | * Pagination is now supported; try `?page=1&limit=20` 112 | * Basic sorting/ordering is now supported; try `?ordering=-date_added` 113 | * Fixed a schema bug that prevented commands without values from being saved (commands like `delete_all` and `delete_last`) 114 | 115 | ### v0.2.1 - 2014/12/03 116 | 117 | * Updated for library change from `gopro` to `goprohero` 118 | * Fixed bugs in `setup.sh` 119 | * Moved API root to `/api` when using the Apache config 120 | * Added the `/api/config` endpoint 121 | * Added support to serve [GoProControllerUI](https://github.com/joshvillbrandt/GoProControllerUI) static content at `/` 122 | 123 | ### v0.2.0 - 2014/11/24 124 | 125 | * Renamed project from `GoProApp` to `GoProController` 126 | * Refactored user interface out of the project and into [GoProControllerUI](https://github.com/joshvillbrandt/GoProControllerUI) 127 | * Now contains a RESTful API for cameras and commands 128 | 129 | ### v0.1.1 - 2014/09/11 130 | 131 | * Bug fixes 132 | 133 | ### v0.1.0 - 2013/10/31 134 | 135 | * Initial release 136 | 137 | ## Known Issues 138 | 139 | There is a memory leak that will cause the program to crash after a couple hours of use with two or more cameras. (It will crash quicker with just one camera since the program runs quicker without network hops.) I've spent a few hours trying to track down the issue to no avail. It seems as though there are uncollectable objects from both the Django side and the GoPro/urllib2 side. I tried replacing urllib2 with python-requests/urllib3, but that didn't help. I used [gc](https://docs.python.org/2/library/gc.html) and [objgraph](http://neverfear.org/blog/view/155/Investigating_memory_leaks_in_Python) to help debug. 140 | 141 | ## Contributions 142 | 143 | Pull requests to the `develop` branch are welcomed! 144 | -------------------------------------------------------------------------------- /apache.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName GoProController 3 | 4 | DocumentRoot /home/GoProControllerUI/_public/ 5 | 6 | 7 | 8 | Order allow,deny 9 | Allow from all 10 | 11 | = 2.3> 12 | Require all granted 13 | 14 | 15 | 16 | WSGIScriptAlias /api /home/GoProController/GoProController/wsgi.py 17 | 18 | 19 | 20 | 21 | Order allow,deny 22 | Allow from all 23 | 24 | = 2.3> 25 | Require all granted 26 | 27 | 28 | 29 | 30 | Alias /static/ /home/GoProController/GoProController/static/ 31 | 32 | 33 | 34 | Order allow,deny 35 | Allow from all 36 | 37 | = 2.3> 38 | Require all granted 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshvillbrandt/GoProController/454b1256044a411ce208a42191154309e4e2fb7a/diagram.png -------------------------------------------------------------------------------- /diagram.xml: -------------------------------------------------------------------------------- 1 | 3Zpfc6M2EMA/TWauD84YcJy7xyR3STvTzqST6bR9VIyM1crIFTiO79PfCnYBIbCdGIhdvxgWAdJv/2rti+Bu+fqg2Wrxmwq5vPDH4etF8PXC972x/xm+jGSbSz5fe7kg0iLEQaXgSXzndCdK1yLkiTUwVUqmYmULZyqO+Sy1ZHMl7VesWESPLwVPMyZd6Z8iTBc4ZX9ayn/mIlrQa7zpl/xKkm7pGSGfs7VMR5kIrpnLS0bPylYVfANiWil4jDlavt5xaagRkHzp9y1Xi0lqHuNEdt+Ak3hhco1zfOL6hWtn6slGLCWL4eyWDu+FlHdKKp2NCObZB65vFiLlTys2M7duQPUgW6RLCWceHOIbuU45WYI760yEU37gaslTvYUheIM/RgtAwwmu8tNNqZgJGcmiopQgQCFDA4mKR5eM4AAxNSObOMge1KNWIAIIuJQKOK3WccjNvWbx/bOZXtts6LTCxkOjsNhMO0CDamhGg6Z2Qmi8qwHZYJxoZhOcHBufXGUINpgH6mzuVJxqJeXJhyOKNZTHXHTBlwZ0Hg08hh1mmQq75D8JKzZ59dnltmArczjbSgH2BawG4BNcY2Agv5u4gMiMqny6wEOqqfC5efzlwp9KeMltKF7gMDKHDaJPX/9hcQQuen8DeBb8JxoDkoY769IP9mhK/QX1qZsle/Noz3VpB0gERFatC8WijT3TcJzV4QAm9XDvHQjA66JM8HAdFQIrrV63l6v8Me82DcNKQF16I0UUgyxV7RAtWJlKXFrD0HALzUgBjwU3KdD1vMdtulAx3CDFs2Z62+p6/TtZC7ZiF2PZWENk86hYrUKlcUcxxaqhwnQjNJc8Sf5PSBuyaX9IsUqrLJ+HsA3EU6UBYaRiJr+V0tsS0NiGwV9F+lfl+G8z5PLKnEFhsy0umRO6lr3evHMPPZijWutMCxVTSJmOeOG3zZTBQlgqXuwXHAXN3ejuhzZbwzaTjOqNBGGfUUFobrEQQkkIHNIbrdXGlDqSJYmYkdiUidkTDyDdABqNrAo6W/7OuGqmTL0NK7Tmos41Qnut6rb+918dLYGPGo+v8NY8Ed8x5RquDHPMDJhkxWI9+SxFGGb6lOyZy1s2+9dk9ThsKsR35/V9MaOFMt4xGl9COwm9FwPHCCkcjBef/qgEvLWsXu2HUiajB6j5PAFLqGunmOJBCitMbSgX+jgPwmBhRaqWOs7yoEmh3u7dxU2k5+4ue0rjKs7CsDvxllEte4+oDu7WX1yN1ToGfzTt8T7dxNFaMmgn3IMlg2ZMI1hAwfSBtdDunfPE3jn71EYYYg9Hnc4egxIGIgpLOyojaCHUolKs8vZPJSThot8aksiarJhUduMtxVj9iZ7iUUNPx5jzOYejzJR2ZW/v+gpxUtnfTTiivkPR8u0jGrl97zspzOL9MfjAJmloYA4dSWh39BHNIOp9n8YGIbvYX31DxvDWYPLWYvXg4qalAXrO0SSzp13RBEo6u2zvZiswqu8F8LzbaHI9sLdkqbXNW9y82+Yt5rawlpxBcmRqfqc3oWK6T81uX/vcU3Nmbjs31tMpmiT93N6NM9n5vhdfCvovYXvYV1ueVA4qnem9zSv6k0zFmVoKs7fpFubKzF00YGU0DU3oNtVP6Yd30j32eIt/qewbj/3f0jDyGbzbTDDKDGomJ2cKGYQB+sXB0OXg2TllS31z2k5JW4XOnBKTjvXz1lycdbLNTX9n5Wo3fEZUDR69D679Y+74ZAun5f8U8+Hl3zyDbz8A -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "GoProController.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # proxy.py 4 | # Josh Villbrandt 5 | # 2013/08/24 6 | 7 | import django 8 | import logging 9 | import json 10 | import time 11 | import sys 12 | import os 13 | from goprohero import GoProHero 14 | from wireless import Wireless 15 | from django.utils import timezone 16 | from colorama import Fore 17 | 18 | # import django models 19 | sys.path.append('/home/GoProController') 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "GoProController.settings") 21 | django.setup() 22 | from GoProController.models import Camera, Command 23 | 24 | 25 | # asynchronous daemon to do our` bidding 26 | class GoProProxy: 27 | maxRetries = 3 28 | 29 | # init 30 | def __init__(self, log_level=logging.INFO): 31 | # setup log 32 | log_file = '/var/log/gopro-proxy.log' 33 | log_format = '%(asctime)s %(message)s' 34 | logging.basicConfig(format=log_format, level=log_level) 35 | 36 | # file logging 37 | fh = logging.FileHandler(log_file) 38 | fh.setLevel(log_level) 39 | fh.setFormatter(logging.Formatter(log_format)) 40 | logger = logging.getLogger() 41 | logger.setLevel(log_level) 42 | logger.addHandler(fh) 43 | 44 | # setup camera 45 | self.camera = GoProHero() 46 | self.snapshots = os.environ.get('GOPRO_SNAPSHOTS', True) 47 | 48 | # setup wireless 49 | interface = os.environ.get('GOPRO_WIFI_INTERFACE', None) 50 | self.wireless = Wireless(interface) 51 | 52 | # connect to the camera's network 53 | def connect(self, camera): 54 | func_str = 'GoProProxy.connect({}, {})'.format( 55 | camera.ssid, camera.password) 56 | 57 | # jump to a new network only if needed 58 | if self.wireless.current() != camera.ssid: 59 | self.wireless.connect( 60 | ssid=camera.ssid, password=camera.password) 61 | 62 | # evaluate connection request 63 | if self.wireless.current() == camera.ssid: 64 | # reconfigure the password in the camera instance 65 | self.camera.password(camera.password) 66 | 67 | logging.info('{}{}{}'.format(Fore.CYAN, func_str, Fore.RESET)) 68 | return True 69 | else: 70 | logging.info('{}{} - network not found{}'.format( 71 | Fore.YELLOW, func_str, Fore.RESET)) 72 | return False 73 | 74 | # send command 75 | def sendCommand(self, command): 76 | result = False 77 | 78 | # make sure we are connected to the right camera 79 | if self.connect(command.camera): 80 | # try to send the command, a few times if needed 81 | i = 0 82 | while i < self.maxRetries and result is False: 83 | result = self.camera.command(command.command, command.value) 84 | i += 1 85 | else: 86 | # mini-status update if we couldn't connect 87 | command.camera.last_attempt = timezone.now() 88 | command.camera.summary = 'notfound' 89 | 90 | # did we successfully talk to the camera? 91 | self.updateCounters(command.camera, result) 92 | command.camera.save() 93 | 94 | # save result 95 | command.time_completed = timezone.now() 96 | command.save() 97 | 98 | # get status 99 | def getStatus(self, camera): 100 | # make sure we are connected to the right camera 101 | camera.last_attempt = timezone.now() 102 | connected = self.connect(camera) 103 | 104 | # could we find the camera? 105 | if connected: 106 | # update counters 107 | camera.last_update = camera.last_attempt 108 | self.updateCounters(camera, True) 109 | 110 | # try to get the camera's status 111 | status = self.camera.status() 112 | camera.summary = status['summary'] 113 | 114 | # extend existing status if possible 115 | if camera.status != '': 116 | # allows us to retain knowledge of settings when powered off 117 | try: 118 | old_status = json.loads(camera.status) 119 | if old_status != '': 120 | old_status.update(status) 121 | status = old_status 122 | except ValueError: 123 | logging.info('{}{} - existing status malformed{}'.format( 124 | Fore.YELLOW, 'GoProProxy.getStatus()', Fore.RESET)) 125 | 126 | # save status to camera 127 | camera.status = json.dumps(status) 128 | 129 | # grab snapshot when the camera is powered on 130 | if self.snapshots is True and 'power' in status \ 131 | and status['power'] == 'on': 132 | camera.save() 133 | image = self.camera.image() 134 | if image is not False: 135 | camera.image = image 136 | camera.image_last_update = camera.last_attempt 137 | else: 138 | # update counters 139 | self.updateCounters(camera, False) 140 | 141 | # update status 142 | camera.summary = 'notfound' 143 | 144 | # save result 145 | camera.save() 146 | 147 | def updateCounters(self, camera, success): 148 | camera.connection_attempts += 1 149 | if success is not True: 150 | camera.connection_failures += 1 151 | 152 | # main loop 153 | def run(self): 154 | logging.info('{}GoProProxy.run(){}'.format(Fore.GREEN, Fore.RESET)) 155 | logging.info('Wifi interface: {}, wifi driver: {}'.format( 156 | self.wireless.interface(), self.wireless.driver())) 157 | logging.info('Attempt snapshots: {}'.format(self.snapshots)) 158 | 159 | # keep running until we land on Mars 160 | # keep the contents of this loop short (limit to one cmd/status or one 161 | # status) so that we can quickly catch KeyboardInterrupt, SystemExit 162 | while 'people' != 'on Mars': 163 | 164 | # PRIORITY 1: send command for the current network on if possible 165 | commands = Command.objects.filter( 166 | time_completed__isnull=True, 167 | camera__ssid__exact=self.wireless.current()) 168 | if len(commands) > 0: 169 | self.sendCommand(commands[0]) 170 | 171 | # get the status now because it is cheap 172 | if self.wireless.current() == commands[0].camera.ssid: 173 | self.getStatus(commands[0].camera) 174 | 175 | # PRIORITY 2: send the oldest command still in the queue 176 | else: 177 | commands = Command.objects.filter( 178 | time_completed__isnull=True).order_by('-date_added') 179 | if len(commands) > 0: 180 | self.sendCommand(commands[0]) 181 | 182 | # get the status now because it is cheap 183 | if self.wireless.current() == commands[0].camera.ssid: 184 | self.getStatus(commands[0].camera) 185 | 186 | # PRIORITY 3: check status of the most stale camera 187 | else: 188 | cameras = Camera.objects.all().order_by('last_attempt') 189 | if len(cameras) > 0: 190 | self.getStatus(cameras[0]) 191 | 192 | # protect the cpu in the event that there was nothing to do 193 | time.sleep(0.1) 194 | 195 | 196 | # run proxy if called directly 197 | if __name__ == '__main__': 198 | proxy = GoProProxy() 199 | proxy.run() 200 | -------------------------------------------------------------------------------- /scripts/goprologger: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # logger.py 4 | # Josh Villbrandt 5 | # 2015/01/27 6 | 7 | 8 | import argparse 9 | import django 10 | import logging 11 | import json 12 | import time 13 | import sys 14 | import os 15 | from colorama import Fore 16 | 17 | # import django models 18 | sys.path.append('/home/GoProController') 19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "GoProController.settings") 20 | django.setup() 21 | from GoProController.models import Camera 22 | 23 | 24 | # make sure the cameras are always in the state we want them in 25 | class GoProLogger: 26 | startTime = None 27 | updates = {} 28 | summaryMap = { 29 | 'notfound': 0, 30 | 'sleeping': 1, 31 | 'on': 2, 32 | 'recording': 3 33 | } 34 | 35 | # init 36 | def __init__(self, log_level=logging.INFO): 37 | # setup log 38 | log_format = '%(asctime)s %(message)s' 39 | logging.basicConfig(format=log_format, level=log_level) 40 | 41 | # parse command line arguments 42 | parser = argparse.ArgumentParser(description=( 43 | 'Create .csv files with GoPro data.')) 44 | parser.add_argument( 45 | '-i, --interval', dest='interval', type=int, default=1, 46 | help='the interval to query the database in seconds') 47 | parser.add_argument( 48 | '-d, --directory', dest='directory', required=True, 49 | help='the output directory for csv files') 50 | args = parser.parse_args() 51 | self.interval = args.interval 52 | self.directory = args.directory 53 | 54 | # report status of all cameras 55 | def checkForUpdates(self): 56 | cameras = Camera.objects.all() 57 | for camera in cameras: 58 | # generate filename 59 | filename = os.path.join( 60 | self.directory, '{}.csv'.format(camera.ssid)) 61 | 62 | # has camera been updated? 63 | if camera.id not in self.updates: 64 | # this is the first time we've seen this camera! 65 | 66 | # delete old file 67 | if os.path.exists(filename): 68 | os.unlink(filename) 69 | 70 | # write headers 71 | self.writeCsv( 72 | filename, 73 | ['time', 'status', 'batt1', 'batt2', 'batt'], 74 | verbose=False) 75 | 76 | # log data 77 | self.writeCsv(filename, self.getFields(camera)) 78 | self.updates[camera.id] = camera.last_update 79 | elif camera.last_update > self.updates[camera.id]: 80 | # log data 81 | self.writeCsv(filename, self.getFields(camera)) 82 | self.updates[camera.id] = camera.last_update 83 | else: 84 | # this camera has not been updated 85 | pass 86 | 87 | # get fields for logging 88 | def getFields(self, camera): 89 | fields = [] 90 | 91 | if camera.last_update is not None: 92 | delta = camera.last_update - self.startTime 93 | fields = [delta.total_seconds(), self.summaryMap[camera.summary]] 94 | 95 | if camera.summary == 'on' or camera.summary == 'recording': 96 | status = json.loads(camera.status) 97 | fields.append(status['batt1']) 98 | fields.append(status['batt2']) 99 | fields.append(int(status['batt1']) + int(status['batt2'])) 100 | else: 101 | fields = fields + [0, 0, 0] 102 | 103 | return fields 104 | 105 | # append data to file 106 | def writeCsv(self, filename, fields, verbose=True): 107 | if verbose: 108 | logging.info('{}: {}'.format(filename, fields)) 109 | 110 | if len(fields) > 0: 111 | str_fields = [] 112 | for field in fields: 113 | str_fields.append(str(field)) 114 | 115 | with open(filename, "a") as f: 116 | f.write(','.join(str_fields)) 117 | f.write(os.linesep) 118 | 119 | # main loop 120 | def run(self): 121 | logging.info('{}GoProLogger.run(){}'.format(Fore.CYAN, Fore.RESET)) 122 | logging.info('{}Update interval: {}s{}'.format( 123 | Fore.CYAN, self.interval, Fore.RESET)) 124 | logging.info('{}Output directory: {}{}'.format( 125 | Fore.CYAN, self.directory, Fore.RESET)) 126 | 127 | # make sure directory exists 128 | if not os.path.exists(self.directory): 129 | os.makedirs(self.directory) 130 | 131 | # get start time 132 | cameras = Camera.objects.all() 133 | for camera in cameras: 134 | if self.startTime is None or camera.last_update < self.startTime: 135 | self.startTime = camera.last_update 136 | 137 | # keep running until we land on Mars 138 | last = None 139 | while 'people' != 'on Mars': 140 | # check if we should run the spammer now or not 141 | now = time.time() 142 | if last is None or (now - last) > self.interval: 143 | last = now 144 | self.checkForUpdates() 145 | 146 | # protect the cpu in the event that there was nothing to do 147 | time.sleep(0.1) 148 | 149 | 150 | # run if called directly 151 | if __name__ == '__main__': 152 | logger = GoProLogger() 153 | logger.run() 154 | -------------------------------------------------------------------------------- /scripts/goprospammer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # spammer.py 4 | # Josh Villbrandt 5 | # 2015/01/26 6 | 7 | 8 | import argparse 9 | import django 10 | import logging 11 | import time 12 | import sys 13 | import os 14 | from colorama import Fore 15 | from django.utils import timezone 16 | 17 | # import django models 18 | sys.path.append('/home/GoProController') 19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "GoProController.settings") 20 | django.setup() 21 | from GoProController.models import Camera, Command 22 | 23 | 24 | # make sure the cameras are always in the state we want them in 25 | class GoProSpammer: 26 | statuses = None 27 | 28 | # init 29 | def __init__(self, log_level=logging.INFO): 30 | # setup log 31 | log_file = '/var/log/gopro-spammer.log' 32 | log_format = '%(asctime)s %(message)s' 33 | logging.basicConfig(format=log_format, level=log_level) 34 | 35 | # file logging 36 | fh = logging.FileHandler(log_file) 37 | fh.setLevel(log_level) 38 | fh.setFormatter(logging.Formatter(log_format)) 39 | logger = logging.getLogger() 40 | logger.setLevel(log_level) 41 | logger.addHandler(fh) 42 | 43 | # parse command line arguments 44 | parser = argparse.ArgumentParser(description=( 45 | 'Automatically re-issue GoPro commands as needed.')) 46 | parser.add_argument( 47 | '-i, --interval', dest='interval', type=int, default=1, 48 | help='the interval to query the database in seconds') 49 | parser.add_argument( 50 | '-p, --param', dest='param', 51 | help='the parameter to be changed or "status" for status') 52 | parser.add_argument( 53 | '-v, --value', dest='value', 54 | help='the value to set the parameter to') 55 | # parser.add_argument( 56 | # '-f, --file', dest='file', 57 | # help='a timeline file with each line as "time, param, value"') 58 | # parser.add_argument( 59 | # '-t, --time', dest='time', type=int, 60 | # help='elapsed time at init in seconds; ' + 61 | # 'defaults to the lowest time in --file') 62 | args = parser.parse_args() 63 | self.interval = args.interval 64 | self.param = args.param 65 | self.value = args.value 66 | # self.file = args.file 67 | # self.time = args.time 68 | 69 | # spam the command 70 | def spam(self): 71 | if self.param is not None and self.param is not 'status': 72 | queued_commands = Command.objects.filter( 73 | time_completed__isnull=True) 74 | 75 | # only add another round of commands if command queue is empty 76 | if len(queued_commands) == 0: 77 | logging.info('{}{} "{}={}"{}'.format( 78 | Fore.RESET, 79 | 'Command queue empty; setting', 80 | self.param, 81 | self.value, 82 | Fore.RESET)) 83 | cameras = Camera.objects.all() 84 | for camera in cameras: 85 | param = self.param 86 | value = self.value 87 | 88 | # override the command if the camera isn't powered on 89 | if self.param != 'power' and ( 90 | camera.summary != 'on' and 91 | camera.summary != 'recording'): 92 | param = 'power' 93 | value = 'on' 94 | 95 | # create a command just for this camera 96 | command = Command( 97 | camera=camera, command=param, value=value) 98 | command.save() 99 | 100 | # report status of all cameras 101 | def getStatus(self): 102 | statuses = [] 103 | cameras = Camera.objects.order_by('ssid') 104 | for camera in cameras: 105 | statuses.append([camera.ssid, camera.summary]) 106 | 107 | return statuses 108 | 109 | # report status of all cameras 110 | def printStatus(self): 111 | # color statuses 112 | colored = [] 113 | for group in self.statuses: 114 | ssid = group[0] 115 | status = group[1] 116 | color = None 117 | 118 | if status == 'recording': 119 | color = Fore.RED 120 | elif status == 'on': 121 | color = Fore.GREEN 122 | elif status == 'sleeping': 123 | color = Fore.YELLOW 124 | else: 125 | color = Fore.RESET 126 | 127 | colored.append('{}{}{}'.format( 128 | color, ssid, Fore.RESET)) 129 | 130 | # now print 131 | logging.info('Status change: {}'.format(', '.join(colored))) 132 | 133 | # report status of all cameras 134 | def status(self): 135 | # get status 136 | statuses = self.getStatus() 137 | 138 | # print if different 139 | if statuses != self.statuses: 140 | self.statuses = statuses 141 | self.printStatus() 142 | 143 | # clear queued commands 144 | def clearCommands(self): 145 | # get queued commands 146 | queued_commands = Command.objects.filter( 147 | time_completed__isnull=True) 148 | 149 | # forcibly set time_complete even though we aren't attempting to send 150 | for command in queued_commands: 151 | command.time_completed = timezone.now() 152 | command.save() 153 | 154 | # main loop 155 | def run(self): 156 | logging.info('{}GoProSpammer.run(){}'.format(Fore.CYAN, Fore.RESET)) 157 | logging.info('{}Update interval: {}s{}'.format( 158 | Fore.CYAN, self.interval, Fore.RESET)) 159 | logging.info('{}Command: {}={}{}'.format( 160 | Fore.CYAN, self.param, self.value, Fore.RESET)) 161 | logging.info('{}Status meanings: {}{}, {}{}, {}{}, {}{}'.format( 162 | Fore.CYAN, 163 | Fore.YELLOW, 'sleeping', 164 | Fore.GREEN, 'on', 165 | Fore.RED, 'recording', 166 | Fore.RESET, 'notfound')) 167 | logging.info('') 168 | 169 | # we want our commands to go right away; clear queued commands 170 | self.clearCommands() 171 | 172 | # keep running until we land on Mars 173 | last = None 174 | while 'people' != 'on Mars': 175 | # check if we should run the spammer now or not 176 | now = time.time() 177 | if last is None or (now - last) > self.interval: 178 | last = now 179 | self.spam() 180 | self.status() 181 | 182 | # protect the cpu in the event that there was nothing to do 183 | time.sleep(0.1) 184 | 185 | 186 | # run spammer if called directly 187 | if __name__ == '__main__': 188 | spammer = GoProSpammer() 189 | spammer.run() 190 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='goprocontroller', 7 | version='0.0.0', 8 | description='An http API to control multiple GoPro cameras over wifi.', 9 | long_description='', 10 | url='https://github.com/joshvillbrandt/goprocontroller', 11 | author='Josh Villbrandt', 12 | author_email='josh@javconcepts.com', 13 | license=open('LICENSE').read(), 14 | packages=[], 15 | setup_requires=[], 16 | install_requires=[ 17 | 'goprohero==0.2.6', 18 | 'wireless==0.3.0', 19 | 'django==1.7.1', 20 | 'djangorestframework', 21 | 'django-cors-headers', 22 | 'python-dateutil', 23 | 'colorama' 24 | ], 25 | scripts=[ 26 | 'scripts/goprospammer', 27 | 'scripts/goprologger' 28 | ], 29 | test_suite='tests', 30 | zip_safe=False 31 | ) 32 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # should be in the same directory as requirements.txt 4 | cd /home/GoProController 5 | 6 | echo "Installing packages..." 7 | apt-get update 8 | apt-get install -y python python-dev python-pip wpasupplicant 9 | # apt-get install -y network-manager --no-install-recommends 10 | python setup.py install 11 | 12 | echo "Configuring Django..." 13 | key=$(tr -dc "[:alpha:]" < /dev/urandom | head -c 48) 14 | sed "s/^SECRET_KEY =.*/SECRET_KEY = '$key'/g" GoProController/settings.py --quiet 15 | python manage.py syncdb --noinput # remove --noinput to create a super user 16 | chmod a+rw sqlite3.db # so apache can write to the db 17 | chmod a+w ./ # so apache can write to the db 18 | python manage.py collectstatic --noinput # for the Django REST framework 19 | 20 | # remove the steps below if you don't want Apache and Upstart 21 | 22 | echo "Configuring Apache..." 23 | apt-get install -y apache2 libapache2-mod-wsgi 24 | rm /etc/apache2/sites-enabled/000-default* 25 | ln -s /home/GoProController/apache.conf /etc/apache2/sites-enabled/GoProController.conf 26 | a2enmod wsgi 27 | a2enmod version 28 | service apache2 restart 29 | PYTHON_EGG_CACHE='/var/www/.python-eggs' 30 | mkdir $PYTHON_EGG_CACHE 31 | chmod 777 $PYTHON_EGG_CACHE 32 | 33 | echo "Configuring Upstart..." 34 | # upstart does not support symlinks 35 | cp /home/GoProController/upstart.conf /etc/init/gopro-proxy.conf 36 | start gopro-proxy 37 | 38 | echo "Good to go!" 39 | -------------------------------------------------------------------------------- /upstart.conf: -------------------------------------------------------------------------------- 1 | description "GoProController Proxy" 2 | author "Josh Villbrandt" 3 | 4 | start on (local-filesystems) 5 | stop on shutdown 6 | 7 | # Restart the process if it dies with a signal 8 | # or exit code not given by the 'normal exit' stanza. 9 | respawn 10 | 11 | # Give up if restart occurs 10 times in 90 seconds. 12 | # http://upstart.ubuntu.com/cookbook/#respawn-limit 13 | respawn limit 10 90 14 | 15 | exec python /home/GoProController/proxy.py 16 | --------------------------------------------------------------------------------