2017-10-29 14:59:48 +00:00
import matplotlib
matplotlib . use ( ' Agg ' )
2017-10-20 12:23:02 +00:00
from flask import Flask , flash , redirect , render_template , request , make_response
2017-10-12 12:12:03 +00:00
import matplotlib . pyplot as plt
import io
2017-10-20 12:23:02 +00:00
import sqlite3
import matplotlib . dates as mdates
2017-10-20 14:19:13 +00:00
from flask_googlemaps import GoogleMaps
2017-11-12 16:28:59 +00:00
import csv
2017-11-12 16:48:26 +00:00
from flask_basicauth import BasicAuth
import configparser
2017-12-20 00:58:24 +00:00
import subprocess
import urllib2
import time
import thread
import logging
import re
import smtplib
from email . mime . text import MIMEText
2017-10-12 12:12:03 +00:00
2017-11-12 16:48:26 +00:00
# read settings
CONFIG_FILE = " ../config/CosmicPi.config "
# read configuration
# Todo: Put the config parser into a propper class
# Todo: Implement proper error catching for configparser (e.g. non existent keys or file)
# read configuration
config = configparser . ConfigParser ( )
config . read ( CONFIG_FILE )
SQLITE_LOCATION = config . get ( " Storage " , " sqlite_location " )
UI_USER = config . get ( " UI " , " username " )
UI_PASS = config . get ( " UI " , " password " )
2017-12-20 00:58:24 +00:00
DEFAULT_WIFI_NAME = config . get ( " Default WiFi " , " name " )
DEFAULT_WIFI_PASS = config . get ( " Default WiFi " , " password " )
WPA_SUPPLICANT_LOCATION = str ( config . get ( " MISC " , " wpa_supplicant_location " ) )
global_user_address = ' '
GMAIL_USER = " cosmicpidevice@gmail.com "
GMAIL_PW = ' meineguete '
2017-11-12 16:48:26 +00:00
# start flask
2017-10-12 12:12:03 +00:00
app = Flask ( __name__ )
2017-11-12 16:48:26 +00:00
app . config [ ' BASIC_AUTH_USERNAME ' ] = UI_USER
app . config [ ' BASIC_AUTH_PASSWORD ' ] = UI_PASS
basic_auth = BasicAuth ( app )
2017-10-12 12:12:03 +00:00
2017-11-12 16:28:59 +00:00
2017-10-20 12:23:02 +00:00
def initDB ( ) :
2017-11-12 16:48:26 +00:00
conn = sqlite3 . connect ( SQLITE_LOCATION , timeout = 60.0 )
2017-10-20 12:23:02 +00:00
cursor = conn . cursor ( )
cursor . execute ( " SELECT name FROM sqlite_master WHERE type= ' table ' AND name= ' Events ' " )
if cursor . fetchone ( ) == None :
cursor . execute ( ''' CREATE TABLE Events
( UTCUnixTime INTEGER , SubSeconds REAL , TempreatureC REAL , Humidity REAL , AccelX REAL ,
AccelY REAL , AccelZ REAL , MagX REAL , MagY REAL , MagZ REAL , Pressure REAL , Longitude REAL ,
Latitude REAL , DetectorName TEXT , DetectorVersion TEXT ) ; ''' )
conn . commit ( )
2017-10-12 12:12:03 +00:00
@app.route ( " /base/ " )
def base ( ) :
return render_template (
' cosmic_base.html ' , * * locals ( ) )
2017-10-20 13:25:51 +00:00
@app.route ( " /hw_serial.txt " )
def getserial ( ) :
# Extract serial from cpuinfo file
cpuserial = " 0000000000000000 "
try :
f = open ( ' /proc/cpuinfo ' , ' r ' )
for line in f :
if line [ 0 : 6 ] == ' Serial ' :
cpuserial = line [ 10 : 26 ]
f . close ( )
except :
cpuserial = " ERROR000000000 "
return cpuserial
icon_dict = {
' UTCUnixTime ' : " fa fa-clock-o fa-5x " ,
' TempreatureC ' : " fa fa-thermometer-half fa-5x " ,
' Humidity ' : " fa fa-tint fa-5x " ,
' AccelX ' : " fa fa-tachometer fa-5x " ,
' AccelY ' : " fa fa-tachometer fa-5x " ,
' AccelZ ' : " fa fa-tachometer fa-5x " ,
' MagX ' : " fa fa-compass fa-5x " ,
' MagY ' : " fa fa-compass fa-5x " ,
' MagZ ' : " fa fa-compass fa-5x " ,
' Pressure ' : " fa fa-thermometer-half fa-5x " ,
' Longitude ' : " fa fa-map-marker fa-5x " ,
' Latitude ' : " fa fa-map-marker fa-5x " ,
' DetectorName ' : " fa fa-info-circle fa-5x " ,
' DetectorVersion ' : " fa fa-info-circle fa-5x " ,
}
2017-10-12 12:12:03 +00:00
@app.route ( ' / ' , methods = [ ' GET ' , ' POST ' ] )
@app.route ( ' /dashboard/ ' , methods = [ ' GET ' , ' POST ' ] )
2017-10-20 15:15:28 +00:00
def dashboard_page ( ) :
2017-10-20 13:25:51 +00:00
values_to_display = [ { ' name ' : ' Hardware Serial ' , ' value ' : getserial ( ) , ' icon ' : ' fa fa-microchip fa-5x ' } ]
2017-10-20 14:19:13 +00:00
# get the latest datapoint
2017-11-12 16:48:26 +00:00
conn = sqlite3 . connect ( SQLITE_LOCATION , timeout = 60.0 )
2017-10-20 14:19:13 +00:00
cursor = conn . cursor ( )
cursor . execute ( " SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC; " )
latest_datapoint = cursor . fetchone ( )
# get collumn names
cursor . execute ( " PRAGMA table_info(Events); " )
col_names = cursor . fetchall ( )
2017-11-12 16:28:59 +00:00
conn . close ( )
2017-10-20 14:19:13 +00:00
# extract data
for i in range ( 0 , len ( col_names ) ) :
# skip the subseconds
if i == 1 :
continue ;
# field name
f_name = col_names [ i ] [ 1 ]
# fill in values
if f_name in icon_dict . keys ( ) :
values_to_display . append ( { ' name ' : f_name , ' value ' : latest_datapoint [ i ] , ' icon ' : icon_dict [ f_name ] } )
2017-10-20 14:34:02 +00:00
return render_template ( ' dashboard.html ' , values_to_display = values_to_display )
2017-10-20 14:19:13 +00:00
@app.route ( ' /plotting/ ' , methods = [ ' GET ' , ' POST ' ] )
2017-10-20 15:15:28 +00:00
def plotting_page ( ) :
2017-10-20 14:19:13 +00:00
location_vars = { ' Latitude ' : 0 , ' Longitude ' : 0 }
2017-10-20 13:25:51 +00:00
# get the latest datapoint
2017-11-12 16:48:26 +00:00
conn = sqlite3 . connect ( SQLITE_LOCATION , timeout = 60.0 )
2017-10-20 13:25:51 +00:00
cursor = conn . cursor ( )
cursor . execute ( " SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC; " )
latest_datapoint = cursor . fetchone ( )
# get collumn names
cursor . execute ( " PRAGMA table_info(Events); " )
col_names = cursor . fetchall ( )
2017-11-12 16:28:59 +00:00
conn . close ( )
2017-10-20 13:25:51 +00:00
2017-10-20 14:19:13 +00:00
# extract data
2017-10-20 13:25:51 +00:00
for i in range ( 0 , len ( col_names ) ) :
# skip the subseconds
if i == 1 :
continue ;
2017-10-20 14:19:13 +00:00
# field name
f_name = col_names [ i ] [ 1 ]
# fill in location
if f_name in location_vars . keys ( ) :
location_vars [ f_name ] = latest_datapoint [ i ]
2017-10-20 13:25:51 +00:00
2017-10-20 14:19:13 +00:00
return render_template ( ' plotting.html ' , location_vars = location_vars )
2017-10-20 12:23:02 +00:00
2017-11-12 16:28:59 +00:00
@app.route ( ' /settings/ ' , methods = [ ' GET ' , ' POST ' ] )
2017-11-12 16:48:26 +00:00
@basic_auth.required
2017-11-12 16:28:59 +00:00
def settings_page ( ) :
2017-12-20 00:58:24 +00:00
email = request . args . get ( ' email ' )
global global_user_address
if not email == None :
global_user_address = email
current_WiFi , avail_WiFi = get_current_and_available_networks ( )
return render_template ( ' settings.html ' , available_wifis = avail_WiFi , current_wifi = current_WiFi , current_email = global_user_address )
def get_current_and_available_networks ( ) :
wifiNetworkList = ' '
# Presently connected network
connectedNetworkNameResponse = ' '
try :
connectedNetworkNameResponse = subprocess . check_output ( [ ' sudo ' , ' iwgetid ' ] )
except subprocess . CalledProcessError as e :
print ' ERROR get connected network: '
print e
except WindowsError as e :
print ( " Well, windows just can ' t do this... " )
connectedNetworkNameResponse = ' " maybe a windows network? " '
connectedNetworkNameStr = re . findall ( ' \" (.*?) \" ' , connectedNetworkNameResponse ) #" Find the string between quotes
if len ( connectedNetworkNameStr ) < 1 :
connectedNetworkNameStr = ' '
else :
connectedNetworkNameStr = connectedNetworkNameStr [ 0 ]
wifiNetworkList = [ connectedNetworkNameStr ]
# Available networks
availableNetworksResponse = ' '
try :
availableNetworksResponse = subprocess . check_output ( [ ' sudo ' , ' iw ' , ' dev ' , ' wlan0 ' , ' scan ' ] )
except subprocess . CalledProcessError as e :
print ' ERROR get list of networks: '
print e
except WindowsError as e :
print ( " Well, windows just can ' t do this either... " )
availableNetworksResponse = ' SSID: Network A \n SSID: Network B \n '
availableNetworksLines = availableNetworksResponse . split ( ' \n ' )
for availableNetworksLine in availableNetworksLines :
if ' SSID ' in availableNetworksLine :
# Typical line:
# SSID: elithion belkin
essid = availableNetworksLine . replace ( ' SSID: ' , ' ' ) . strip ( )
wifiNetworkList . append ( essid )
return connectedNetworkNameStr , wifiNetworkList
@app.route ( ' /connect_to_wifi ' , methods = [ ' GET ' , ' POST ' ] )
@basic_auth.required
def wifi_connector ( ) :
# get user set parameters
wifi_name = request . args . get ( ' selected_wifi ' )
wifi_pw = request . args . get ( ' password ' )
# check that the user has given a mail address
global global_user_address
if global_user_address == " " :
return ' Please save an e-mail address before connecting to any other WiFi! <a href= " /settings/ " >Go back to the settings page</a> '
# launch the wifi login in a different thread
# we need to to it like this, since otherwise the user would never recieve an answer...
thread . start_new_thread ( connect_to_wifi , ( wifi_name , wifi_pw ) )
msg = ' The CosmicPi will now try to connect to the WiFi " {} " . ' . format ( wifi_name )
msg + = " When the CosmicPi has connected to the internet you will receive an e-mail. "
msg + = " If no internet connection is found or the connection was not sucessfull the CosmiPi will recreate the WiFi hotspot. Please wait at least two minutes. "
return msg
2017-11-12 16:28:59 +00:00
2017-12-20 00:58:24 +00:00
import socket
import struct
def get_ip_address ( ifname ) :
s = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM )
try :
import fcntl
result = socket . inet_ntoa ( fcntl . ioctl (
s . fileno ( ) ,
0x8915 , # SIOCGIFADDR
struct . pack ( ' 256s ' , ifname [ : 15 ] )
) [ 20 : 24 ] )
except IOError :
print ( " Error getting the IP address, either you are not doing this on raspbian or the queried device does not exist. " )
result = " some_ip "
except WindowsError :
print ( " Windows can not do such things, don ' t force it ^^ " )
result = " some_ip "
except ImportError :
print ( " This OS can not do such things, don ' t force it ^^ " )
result = " some_ip "
return result
def send_status_mail ( ) :
# prepare everything we need in advance
current_wifi = get_current_and_available_networks ( ) [ 0 ]
listtatata = { ' Currently connected network ' : current_wifi ,
' Current IP address ' : get_ip_address ( " wlan0 " ) ,
' Serial number ' : getserial ( )
}
content = " Hello there, \n \n "
content + = ' Your CosmicPi has just connected to the network " {} " . ' . format ( current_wifi )
content + = " When your computer is connected to {} , you can reach the cosmic Pi via this address: http:// {} / \n \n " . format ( current_wifi , listtatata [ ' Current IP address ' ] )
content + = " Please do not answer to this e-mail, the address is only used by the CosmicPi devices and can not answer you. \n "
content + = " If you have any questions or could use some help, do not hesitate to contact us at: contact@cosmicpi.org \n \n "
content + = " All the best, \n Your CosmicPi-Team \n \n \n "
content + = " Some information about your CosmicPi: \n "
for key in listtatata . keys ( ) :
content + = " {} : {} \n " . format ( key , listtatata [ key ] )
msg = MIMEText ( content )
msg [ ' Subject ' ] = ' Your CosmicPi has connected to the internet! '
msg [ ' From ' ] = GMAIL_USER
msg [ ' To ' ] = global_user_address
# connect to google
server = smtplib . SMTP ( ' smtp.gmail.com:587 ' )
server . ehlo ( )
server . starttls ( )
# login
server . login ( GMAIL_USER , GMAIL_PW )
# send the mail
try :
server . sendmail ( msg [ ' From ' ] , msg [ ' To ' ] , msg . as_string ( ) )
except smtplib . SMTPRecipientsRefused :
print ( " The e-mail address was invalid! Falling back to AP mode, otherwise the CosmicPi is potentially lost! " )
fall_back_to_ap ( )
# end contact
server . quit ( )
def fall_back_to_ap ( ) :
# empty the wpa supplicant to it's default
wpa_supplicant_string = " country=GB \n " # Todo: Check, that this string in front is still correct!
wpa_supplicant_string + = " ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev \n "
wpa_supplicant_string + = " update_config=1 \n "
wpa_supplicant_string + = " \n network= { \n "
net = ( DEFAULT_WIFI_NAME , DEFAULT_WIFI_PASS )
wpa_supplicant_string + = ' \t ssid= " {} " \n ' . format ( net [ 0 ] )
# check if we need a password
if str ( net [ 1 ] ) == " " :
wpa_supplicant_string + = ' \t psk= " {} " \n ' . format ( net [ 1 ] )
wpa_supplicant_string + = " } \n "
with open ( WPA_SUPPLICANT_LOCATION , ' w ' ) as file :
file . write ( wpa_supplicant_string )
2018-01-04 14:11:40 +00:00
# configure controler to accept the new configuration
try :
import fcntl
time . sleep ( 2 )
subprocess . call ( " wpa_cli -i wlan0 reconfigure " , shell = True )
time . sleep ( 2 )
except ImportError :
print ( " This OS is not linux enough to handle a hotspot in this way " )
2017-12-20 00:58:24 +00:00
# restart the hotspot
try :
import fcntl
subprocess . call ( " systemctl start create_ap " ,
shell = True ) # well, here we actually care if the hotspot starts, but oh well...
except ImportError :
print ( " This OS is not linux enough to handle a hotspot in this way " )
def connect_to_wifi ( name , pw ) :
# build the string for the WPA supplicant
wpa_supplicant_string = " country=GB \n " # Todo: Check, that this string in front is still correct!
wpa_supplicant_string + = " ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev \n "
wpa_supplicant_string + = " update_config=1 \n "
networks = [ ( name , pw ) , ( DEFAULT_WIFI_NAME , DEFAULT_WIFI_PASS ) ]
for net in networks :
wpa_supplicant_string + = " \n network= { \n "
wpa_supplicant_string + = ' \t ssid= " {} " \n ' . format ( net [ 0 ] )
# check if we need a password
if not str ( net [ 1 ] ) == " " :
wpa_supplicant_string + = ' \t psk= " {} " \n ' . format ( net [ 1 ] )
wpa_supplicant_string + = " } \n "
2018-01-04 14:11:40 +00:00
# deactivate htospot and turn the WiFi back on
2017-12-20 00:58:24 +00:00
try :
import fcntl
subprocess . call ( " systemctl stop create_ap " , shell = True ) # we very much don't care if this fails
2018-01-04 14:11:40 +00:00
time . sleep ( 2 )
subprocess . call ( " ifconfig wlan0 up " , shell = True )
2017-12-20 00:58:24 +00:00
except ImportError :
print ( " This OS is not linux enough to handle a hotspot in this way " )
# write wpa_supplicant string to the file
with open ( WPA_SUPPLICANT_LOCATION , ' w ' ) as file :
file . write ( wpa_supplicant_string )
2018-01-04 14:11:40 +00:00
# configure controler to accept the new configuration
try :
import fcntl
time . sleep ( 2 )
subprocess . call ( " wpa_cli -i wlan0 reconfigure " , shell = True )
except ImportError :
print ( " This OS is not linux enough to handle a hotspot in this way " )
2017-12-20 00:58:24 +00:00
# wait for an internet connection (max 2 min)
start_time = time . time ( )
have_internet = False
while ( have_internet == False ) and ( ( start_time + 120 ) > time . time ( ) ) :
have_internet = internet_on ( )
# if we have no internet, restart the hotspot, otherwise we are done for now
if have_internet :
print ( " Sucessfully connected to the internet (yeah) " )
2018-01-04 14:11:40 +00:00
# wait a bit before we send the mail
time . sleep ( 5 )
2017-12-20 00:58:24 +00:00
send_status_mail ( )
return
else :
print ( " No internet connection here, falling back to hotspot! " )
fall_back_to_ap ( )
return
def internet_on ( ) :
try :
urllib2 . urlopen ( ' http://heise.de ' , timeout = 2 )
return True
except urllib2 . URLError as err :
return False
2017-11-12 16:28:59 +00:00
@app.route ( ' /CosmicPi_data.csv ' , methods = [ ' GET ' ] )
2017-12-20 00:58:24 +00:00
@basic_auth.required
2017-11-12 16:28:59 +00:00
def csv_export ( ) :
2017-11-12 16:48:26 +00:00
conn = sqlite3 . connect ( SQLITE_LOCATION , timeout = 60.0 )
2017-11-12 16:28:59 +00:00
cursor = conn . cursor ( )
# get collumn names
cursor . execute ( " PRAGMA table_info(Events); " )
col_data = cursor . fetchall ( )
col_names = [ ]
for i in range ( 0 , len ( col_data ) ) :
col_names . append ( col_data [ i ] [ 1 ] )
# get data from db
cursor . execute ( " SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC; " )
data = cursor . fetchall ( )
conn . close ( )
# write CSV export to memory
output = io . BytesIO ( )
writer = csv . writer ( output )
writer . writerow ( col_names )
writer . writerows ( data )
response = make_response ( output . getvalue ( ) )
response . headers [ ' Content-Type ' ] = ' text/csv '
return response
2017-10-12 12:12:03 +00:00
2017-10-20 15:15:28 +00:00
@app.route ( ' /about/ ' , methods = [ ' GET ' , ' POST ' ] )
def about_page ( ) :
return render_template ( ' about.html ' )
2017-10-20 12:23:02 +00:00
@app.route ( ' /histogram.png ' )
2017-10-12 12:12:03 +00:00
def build_plot ( ) :
2017-10-20 12:23:02 +00:00
# get user set parameters
start_time = request . args . get ( ' start_time ' , type = int )
end_time = request . args . get ( ' end_time ' , type = int )
bin_size_seconds = request . args . get ( ' bin_size_seconds ' , type = int )
plot_title = ' '
2017-10-12 12:12:03 +00:00
2017-10-20 12:23:02 +00:00
# get some data
data = [ ]
2017-11-12 16:48:26 +00:00
conn = sqlite3 . connect ( SQLITE_LOCATION , timeout = 60.0 )
2017-10-20 12:23:02 +00:00
cursor = conn . cursor ( )
# only get the last n seconds if the start time was negative
if start_time < 0 :
2017-11-12 19:24:23 +00:00
plot_title + = " Histogram of events over the last {0:.1f} minutes \n bin size: {1:d} [s] " . format ( - start_time / 60. , bin_size_seconds )
2017-10-20 12:23:02 +00:00
cursor . execute ( " SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC; " )
start_time = cursor . fetchone ( ) [ 0 ] + start_time
end_time = 9000000000
else :
plot_title + = " Histogram of events over a set time \n bin size: " + str ( bin_size_seconds ) + " [s] "
cursor . execute ( " SELECT * FROM Events WHERE UTCUnixTime BETWEEN ? AND ? ORDER BY UTCUnixTime DESC, SubSeconds DESC; " ,
( start_time , end_time ) )
data = cursor . fetchall ( )
2017-11-12 16:28:59 +00:00
conn . close ( )
2017-10-20 12:23:02 +00:00
# massage data
if len ( data ) == 0 :
plt . hist ( [ ] )
plt . title ( " No data to display " )
else :
event_time_list = [ data [ i ] [ 0 ] + data [ i ] [ 1 ] for i in range ( len ( data ) ) ]
#event_time_list = [data[i][0] for i in range(len(data))]
bin_edges = range ( int ( event_time_list [ len ( event_time_list ) - 1 ] ) , int ( event_time_list [ 0 ] ) , bin_size_seconds )
2017-11-12 19:24:23 +00:00
x_axis_limits = ( start_time , int ( event_time_list [ 0 ] ) + 1 )
2017-10-20 12:23:02 +00:00
# convert our unix timestamps to Matplotlib format
event_time_list = mdates . epoch2num ( event_time_list )
bin_edges = mdates . epoch2num ( bin_edges )
x_axis_limits = mdates . epoch2num ( x_axis_limits )
2017-10-12 12:12:03 +00:00
2017-10-20 12:23:02 +00:00
# make the plot
plt . hist ( event_time_list , bins = bin_edges )
plt . title ( plot_title )
plt . xlabel ( " Time [UTC] " )
2017-12-20 00:58:24 +00:00
plt . ylabel ( " Number of Events per {0:d} seconds [1] " . format ( bin_size_seconds ) )
2017-10-20 12:23:02 +00:00
plt . subplots_adjust ( bottom = 0.2 )
plt . xticks ( rotation = 25 )
plt . tick_params ( which = ' both ' , width = 2 , direction = " out " , top = False , right = False )
plt . tick_params ( which = ' major ' , length = 5 )
plt . tick_params ( which = ' minor ' , length = 3 , color = ' r ' )
# do the date formatting
ax = plt . gca ( )
locator = mdates . AutoDateLocator ( minticks = 7 )
locator . intervald [ mdates . SECONDLY ] = [ 1 , 10 , 30 ]
formatter = mdates . AutoDateFormatter ( locator )
formatter . scaled [ 1 / ( 24. * 60. ) ] = ' % H: % M: % S '
ax . xaxis . set_major_locator ( locator )
ax . xaxis . set_major_formatter ( formatter )
ax . set_xlim ( x_axis_limits )
# return the generated plot
img = io . BytesIO ( )
2017-10-12 12:12:03 +00:00
plt . savefig ( img , format = ' png ' )
img . seek ( 0 )
2017-10-20 12:23:02 +00:00
plt . close ( )
response = make_response ( img . getvalue ( ) )
response . headers [ ' Content-Type ' ] = ' image/png '
return response
2017-10-12 12:12:03 +00:00
if __name__ == ' __main__ ' :
2017-10-20 14:19:13 +00:00
# do necessary inits
2017-10-20 12:23:02 +00:00
initDB ( )
2017-10-20 14:19:13 +00:00
GoogleMaps ( app , key = " AIzaSyD_RgwMc6X6LpkAmskk4fWmafNFXtlB7_s " )
2017-10-12 12:12:03 +00:00
app . run ( )