├── .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 | 
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 |
--------------------------------------------------------------------------------