Move everything to docker containers
6
Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
build:
|
||||
docker-compose up --build -d
|
||||
|
||||
clean:
|
||||
docker-compose down
|
||||
docker system prune -fa
|
73
README.md
|
@ -1,8 +1,10 @@
|
|||
## The Things Network Tracker (TTN-Tracker)
|
||||
|
||||
This is a [Flask](http://flask.pocoo.org/) app served via [Gunicorn](https://github.com/benoitc/gunicorn) and [Nginx](http://nginx.org/) using [docker](https://www.docker.com/) containers orchestrated by docker-compose.
|
||||
|
||||
For use with [kizniche/ttgo-tbeam-ttnmapper](https://github.com/kizniche/ttgo-tbeam-ttnmapper) for the T-Beam TTGO (tracker node) that transmits data to [The Things Network](https://thethingsnetwork.org) (TTN) for [TTN Mapper](https://ttnmapper.org/).
|
||||
|
||||
This Flask app hosts a web-enabled front end using [gunicorn](https://github.com/benoitc/gunicorn) and [nginx](http://nginx.org/). It pulls coordinate data acquired from the tracker node that's been stored on TTN. It stores these coordinates in an SQLite database and displays the coordinates on a map ([leaflet](https://github.com/Leaflet/Leaflet)) in your web browser. This is useful for testing the signal range from gateways while driving, so you can see when and where your signal was able to reach a gateway.
|
||||
This app pulls coordinate data acquired from the tracker node that's been stored on TTN. It stores these coordinates in an SQLite database and displays the coordinates on a map ([leaflet](https://github.com/Leaflet/Leaflet)) in your web browser. This is useful for testing the signal range from gateways while driving, so you can see when and where your signal was able to reach a gateway.
|
||||
|
||||
This is very similar to the TTN Mapper frontend, however TTN Mapper takes a long time to update the data points on its map. This software runs locally on your own hardware and responds instantly to new data on TTN, making it a good companion in your vehicle if you want to get instant updates as to whether your tracker node successfully communicated its coordinates or not.
|
||||
|
||||
|
@ -15,28 +17,71 @@ Features include:
|
|||
|
||||
### Setup:
|
||||
|
||||
I've succesfully set this up on a Raspberry Pi, but these instructions should work for any debian-variant operating system. Other systems you may have to adapt how you install the prerequisites.
|
||||
I've successfully set this up on a Raspberry Pi.
|
||||
|
||||
Make sure you have your application set up on The Things Network with the integration "Data Storage". The integration "TTN Mapper" is optional but is recommended to be able to provide signal data to the public.
|
||||
|
||||
### Install docker and docker-compose
|
||||
|
||||
```
|
||||
curl -sSL https://get.docker.com | sh
|
||||
sudo pip install docker-compose
|
||||
```
|
||||
|
||||
### Clone this repository
|
||||
|
||||
```
|
||||
git clone https://github.com/kizniche/ttn-tracker.git
|
||||
cd ttn-tracker
|
||||
sudo pip install virtualenv --upgrade
|
||||
PYTHON_BINARY_SYS_LOC="$(python3.5 -c "import os; print(os.environ['_'])")"
|
||||
virtualenv --system-site-packages -p ${PYTHON_BINARY_SYS_LOC} ./env
|
||||
./env/bin/pip install -r requirements.txt
|
||||
sudo ln -s /home/pi/ttn-tracker/flask_nginx.conf /etc/nginx/sites-enabled/ttn-tracker_nginx.config
|
||||
sudo service nginx restart
|
||||
sudo systemctl enable /home/pi/ttn-tracker/ttn-tracker.service
|
||||
```
|
||||
|
||||
Make sure you have your application set up on The Things Network with the integration "Data Storage". Edit config.py with your application API Key, application ID, Device ID(s), and gateway location(s). The integration "TTN Mapper" is optional but is recommended to be able to provide signal data to the public.
|
||||
### Edit the config file
|
||||
|
||||
### Run
|
||||
Edit ttn-tracker/flask_app/config.py with your application API Key, application ID, Device ID(s), and gateway location(s) before building the docker image. If you need to edit this file after the image is created, you can rebuild the image (destroying data) or copy the new config file over the old while the flask_app container is running (see below).
|
||||
|
||||
```sudo service ttn-tracker start```
|
||||
### Build and start the app
|
||||
|
||||
This will build and start the app and keep it running across reboots.
|
||||
|
||||
```sudo make build```
|
||||
|
||||
### Stop app (preserving data)
|
||||
|
||||
```sudo docker-compose stop```
|
||||
|
||||
### Start app after stopping
|
||||
|
||||
```sudo docker-compose start```
|
||||
|
||||
### Web Address
|
||||
|
||||
Note: there is no security preventing someone from viewing this page if they happen to request "/dsf673bh" on the server (however, knowing this is the page is unlikely). Therefore, make sure you are comfortable with this or implement your own security measures such as not allowing port 5500 to be publicly accessible (connect to your home network via VPN to access the app) or add a login system such as [Flask-Login](https://github.com/maxcountryman/flask-login).
|
||||
Open a browser to this address, replacing IP_ADDRESS with the IP address of the system running the docker containers.
|
||||
|
||||
http://127.0.0.1:5500/dsf673bh
|
||||
http://IP_ADDRESS:5550/dsf673bh
|
||||
|
||||
Note: there is no security preventing someone from viewing this page if they happen to request "/dsf673bh" on the server (however, knowing this is the page is unlikely). Therefore, make sure you are comfortable with this or implement your own security measures such as not allowing port 5550 to be publicly accessible (connect to your home network via VPN to access the app) or add a login system such as [Flask-Login](https://github.com/maxcountryman/flask-login).
|
||||
|
||||
### Notes
|
||||
|
||||
#### Stop app and delete data (keep containers)
|
||||
|
||||
```sudo docker-compose down```
|
||||
|
||||
#### Stop app and delete containers
|
||||
|
||||
```sudo docker-compose rm -fs```
|
||||
|
||||
#### List docker containers
|
||||
|
||||
```sudo docker ps```
|
||||
|
||||
#### Start a shell in a docker container
|
||||
|
||||
```sudo docker exec -i -t CONTAINER_ID /bin/bash```
|
||||
|
||||
#### Copy files to/from a docker container
|
||||
|
||||
```
|
||||
docker cp foo.txt CONTAINER_ID:/foo.txt
|
||||
docker cp CONTAINER_ID:/foo.txt foo.txt
|
||||
```
|
||||
|
|
24
config.py
|
@ -1,24 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Where the map initially loads
|
||||
start_lat = 35.978781
|
||||
start_lon = -77.855346
|
||||
|
||||
# Where to store SQLite database
|
||||
path_db = '/home/pi/ttn-tracker/ttnmapper_retrieve.db'
|
||||
|
||||
# TTN Application
|
||||
application = "ttn_application"
|
||||
app_key = "key ttn-account-TTN_APP_KEY"
|
||||
|
||||
# Application devices
|
||||
devices = [
|
||||
"device_01",
|
||||
"device_02"
|
||||
]
|
||||
|
||||
# Where to place gateway markers
|
||||
gateway_locations = [
|
||||
('Gateway 01', 35.978781, -77.855346),
|
||||
('Gateway 02', 35.978781, -77.655346)
|
||||
]
|
21
docker-compose.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
flask_app:
|
||||
container_name: flask_app
|
||||
restart: always
|
||||
build: ./flask_app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command: gunicorn -w 1 --worker-class gthread --bind :8000 app:app
|
||||
|
||||
|
||||
nginx:
|
||||
container_name: nginx
|
||||
restart: always
|
||||
build: ./nginx
|
||||
ports:
|
||||
- "5550:5550"
|
||||
depends_on:
|
||||
- flask_app
|
8
flask_app/Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
|||
FROM python:3.6-slim-stretch
|
||||
|
||||
RUN mkdir -pv /home/project/flask_app
|
||||
WORKDIR /home/project/flask_app
|
||||
COPY requirements.txt /home/project/flask_app
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . /home/project/flask_app
|
|
@ -6,27 +6,26 @@ from math import radians
|
|||
from math import sin
|
||||
from math import sqrt
|
||||
|
||||
import os
|
||||
import requests
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from flask import Flask
|
||||
from flask import render_template
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from config import app_key
|
||||
from config import application
|
||||
from config import bing_api_key
|
||||
from config import config_app
|
||||
from config import devices
|
||||
from config import gateway_locations
|
||||
from config import path_db
|
||||
from config import start_lat
|
||||
from config import start_lon
|
||||
from flask import Flask
|
||||
from flask import render_template
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__, template_folder="./templates")
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{db}'.format(db=path_db)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app = config_app(Flask(__name__, template_folder="./templates"))
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
|
@ -66,10 +65,15 @@ class LastAcquisition(db.Model):
|
|||
return '<ID %r>' % self.id
|
||||
|
||||
|
||||
if not os.path.exists(path_db):
|
||||
db.create_all()
|
||||
|
||||
|
||||
@app.route('/dsf673bh')
|
||||
def hello_world():
|
||||
def main_page():
|
||||
get_new_data()
|
||||
return render_template('map.html',
|
||||
bing_api_key=bing_api_key,
|
||||
gateway_locations=gateway_locations,
|
||||
location_data=Location.query.all(),
|
||||
start_lat=start_lat,
|
||||
|
@ -147,4 +151,4 @@ def distance_coordinates(lat1, lon1, lat2, lon2):
|
|||
|
||||
if __name__ == '__main__':
|
||||
db.create_all()
|
||||
app.run(debug=True, host='0.0.0.0', port=5500)
|
||||
app.run(debug=True, host='0.0.0.0', port=8000)
|
51
flask_app/config.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Where to store SQLite database
|
||||
path_db = '/home/project/ttn_tracker_database.db'
|
||||
|
||||
# Where the map initially loads
|
||||
start_lat = 35.978781
|
||||
start_lon = -77.855346
|
||||
|
||||
# TTN Application
|
||||
application = "ttn_application"
|
||||
app_key = "key ttn-account-TTN_APP_KEY"
|
||||
|
||||
# Application devices
|
||||
devices = [
|
||||
"device_01",
|
||||
"device_02"
|
||||
]
|
||||
|
||||
# Where to place gateway markers
|
||||
gateway_locations = [
|
||||
('Gateway 01', 35.978781, -77.855346),
|
||||
('Gateway 02', 35.978781, -77.655346)
|
||||
]
|
||||
|
||||
bing_api_key = ''
|
||||
|
||||
|
||||
def config_app(app, **kwargs):
|
||||
"""Dash app configuration
|
||||
|
||||
Parameters
|
||||
----------
|
||||
app: Dash app
|
||||
debug: optional, default=False
|
||||
|
||||
Returns
|
||||
-------
|
||||
app: Dash app
|
||||
With added css, ga and layout container with:
|
||||
app-layout: main div, should not be a target of an ouput callback
|
||||
page-content: container div, target for an ouput callback
|
||||
url: Location, target of an input callback
|
||||
"""
|
||||
|
||||
if kwargs.get('debug', False):
|
||||
app.server.debug = True
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{db}'.format(db=path_db)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
return app
|
Before ![]() (image error) Size: 397 B After ![]() (image error) Size: 397 B ![]() ![]() |
Before (image error) Size: 588 B After (image error) Size: 588 B |
Before ![]() (image error) Size: 762 B After ![]() (image error) Size: 762 B ![]() ![]() |
Before ![]() (image error) Size: 387 B After ![]() (image error) Size: 387 B ![]() ![]() |
Before (image error) Size: 522 B After (image error) Size: 522 B |
Before ![]() (image error) Size: 692 B After ![]() (image error) Size: 692 B ![]() ![]() |
Before ![]() (image error) Size: 326 B After ![]() (image error) Size: 326 B ![]() ![]() |
Before (image error) Size: 607 B After (image error) Size: 607 B |
Before ![]() (image error) Size: 462 B After ![]() (image error) Size: 462 B ![]() ![]() |
Before ![]() (image error) Size: 595 KiB After ![]() (image error) Size: 595 KiB ![]() ![]() |
Before ![]() (image error) Size: 192 B After ![]() (image error) Size: 192 B ![]() ![]() |
Before (image error) Size: 2.1 KiB After (image error) Size: 2.1 KiB |
Before ![]() (image error) Size: 277 B After ![]() (image error) Size: 277 B ![]() ![]() |
Before ![]() (image error) Size: 491 B After ![]() (image error) Size: 491 B ![]() ![]() |
Before (image error) Size: 628 B After (image error) Size: 628 B |
Before ![]() (image error) Size: 1,003 B After ![]() (image error) Size: 1,003 B ![]() ![]() |
Before ![]() (image error) Size: 279 B After ![]() (image error) Size: 279 B ![]() ![]() |
Before (image error) Size: 2.5 KiB After (image error) Size: 2.5 KiB |
Before ![]() (image error) Size: 460 B After ![]() (image error) Size: 460 B ![]() ![]() |
Before ![]() (image error) Size: 1.2 KiB After ![]() (image error) Size: 1.2 KiB ![]() ![]() |
Before ![]() (image error) Size: 696 B After ![]() (image error) Size: 696 B ![]() ![]() |
Before ![]() (image error) Size: 2.4 KiB After ![]() (image error) Size: 2.4 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.4 KiB After ![]() (image error) Size: 1.4 KiB ![]() ![]() |
Before ![]() (image error) Size: 618 B After ![]() (image error) Size: 618 B ![]() ![]() |
|
@ -35,8 +35,8 @@
|
|||
esri_Map = L.esri.basemapLayer("Topographic"),
|
||||
otm_Map = L.tileLayer(otm_Url, {attribution: otm_Attrib}),
|
||||
google_Map = L.tileLayer(google_Url, {attribution: google_Attrib}),
|
||||
bing_dark = L.tileLayer.bing({bingMapsKey: 'Ahpy5iReID6QJSUjazLaJbAkUMg2R990DMsYlbxMbf3irXoOVgFb0eyV3JPntW2Q', imagerySet: 'CanvasDark', attribution: bing_Attrib}),
|
||||
bing_sat = L.tileLayer.bing({bingMapsKey: 'Ahpy5iReID6QJSUjazLaJbAkUMg2R990DMsYlbxMbf3irXoOVgFb0eyV3JPntW2Q', imagerySet: 'Aerial', attribution: bing_Attrib}),
|
||||
bing_dark = L.tileLayer.bing({bingMapsKey: '{{bing_api_key}}', imagerySet: 'CanvasDark', attribution: bing_Attrib}),
|
||||
bing_sat = L.tileLayer.bing({bingMapsKey: '{{bing_api_key}}', imagerySet: 'Aerial', attribution: bing_Attrib}),
|
||||
bing_sat_labels = L.tileLayer.bing({bingMapsKey: 'Ahpy5iReID6QJSUjazLaJbAkUMg2R990DMsYlbxMbf3irXoOVgFb0eyV3JPntW2Q', imagerySet: 'AerialWithLabels', attribution: bing_Attrib});
|
||||
|
||||
//Create a map that remembers where it was zoomed to
|
|
@ -1,15 +0,0 @@
|
|||
server {
|
||||
listen 5500;
|
||||
|
||||
client_max_body_size 30M;
|
||||
|
||||
location / {
|
||||
include proxy_params;
|
||||
proxy_pass http://unix:/var/run/ttn_tracker_flask.sock;
|
||||
}
|
||||
|
||||
error_page 502 /502.html;
|
||||
location = /502.html {
|
||||
root /home/pi/ttn-tracker/templates;
|
||||
}
|
||||
}
|
7
gitignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
__pycache__
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
.cache/
|
||||
.DS_Store
|
7
nginx/Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM nginx:1.15.9
|
||||
|
||||
RUN rm /etc/nginx/nginx.conf
|
||||
COPY nginx.conf /etc/nginx/
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY project.conf /etc/nginx/conf.d/
|
52
nginx/nginx.conf
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Define the user that will own and run the Nginx server
|
||||
user nginx;
|
||||
|
||||
# Define the number of worker processes; recommended value is the number of
|
||||
# cores that are being used by your server
|
||||
worker_processes 1;
|
||||
|
||||
# Define the location on the file system of the error log, plus the minimum
|
||||
# severity to log messages for
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Define the file that will store the process ID of the main NGINX process
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
# events block defines the parameters that affect connection processing.
|
||||
events {
|
||||
# Define the maximum number of simultaneous connections that can be opened by a worker process
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
# http block defines the parameters for how NGINX should handle HTTP web traffic
|
||||
http {
|
||||
# Include the file defining the list of file types that are supported by NGINX
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
# Define the default file type that is returned to the user
|
||||
default_type text/html;
|
||||
|
||||
# Define the format of log messages.
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# Define the location of the log of access attempts to NGINX
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Define the parameters to optimize the delivery of static content
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
|
||||
# Define the timeout value for keep-alive connections with the client
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Define the usage of the gzip compression algorithm to reduce the amount of data to transmit
|
||||
#gzip on;
|
||||
|
||||
# Include additional parameters for virtual host(s)/server(s)
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
12
nginx/project.conf
Normal file
|
@ -0,0 +1,12 @@
|
|||
server {
|
||||
|
||||
listen 5550;
|
||||
server_name ttn_tracker;
|
||||
|
||||
location / {
|
||||
proxy_pass http://flask_app:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
[Unit]
|
||||
Description=gunicorn daemon for ttn-mapper
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/ttn-tracker
|
||||
ExecStart=/home/pi/ttn-tracker/env/bin/gunicorn \
|
||||
--workers 1 \
|
||||
--worker-class gthread \
|
||||
--threads 2 \
|
||||
--timeout 300 \
|
||||
--pid /var/lock/ttn_tracker_flask.pid \
|
||||
--bind unix:/var/run/ttn_tracker_flask.sock app:app
|
||||
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
ExecStop=/bin/kill -s TERM $MAINPID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|