sâmbătă, 4 februarie 2017

Server GPIO cu Raspberry Pi si Flask



Pentru inceput...

     Salutare tuturor! Iata ca am gasit ragazul sa sa mai adaug un articol pe blog.

     Daca pana acum comenzile pe care le-am trimis Pi-ului erau preluate de la tastatura prin SSH, am zis sa ridic un pic stacheta si sa programez pentru Raspberry un server web. Zis si facut! Cum Python-ul este obiectivul meu pe termen lung am ales ca server o veche cunostinta: Flask-ul. Server-ul este usor de programat iar documentatie se gaseste garla pe internet. Ce a iesit puteti vedea in filmuletul de mai jos:









Si o poza cu realizarea practica:




Aaa si il  puteti vedea live la http://84.117.194.136:64000/

username: guest ; password 1234


Cum functioneaza?

     Daca sunteti interesati de fenomenul IOT (internet of things) si ati mai configurat ceva servere, fie cu Arduino fie cu NodeMcu, ati observat, probabil, faptul ca este realtiv greu sa realizati o protectie minimala impotriva "vizitatorilor" nepoftiti. Cat timp rulati serverul in reteaua locala nu este nici o problema, insa cand "iesiti" cu el pe internet va puteti trezi ca aerul conditionat porneste din senin sau lumina se stinge si se aprinde haotic. Cu alte cuvinte cineva se joaca la butoanele voastre! 

     Ceea ce voi prezenta in continuare este o solutie sigura, deoarece permite autentificarea pe baza de user-name si parola. 

Cum se face?

     Pentru a putea instala cu succes server-ul trebuie sa urmati tutorialele anterioare, pe care evident, le gasiti pe blog.

     Fiindca sunt baiat bun voi face public codul ba mai mult va voi explica in cateva cuvinte cum lucreaza pentru a-l putea adapta la nevoile voastre. Sa incepem:

     Pe lanaga Python care, impreuna cu libraria Flask, ruleaza pe Raspberry Pi a mai fost necesar sa scriu codul HTML pentru paginile web (cea de logare si cea de control) si sa folosesc  un pic de JavaScript impreuna cu Ajax si jQuery. Pheeewww :) !

     Daca insiruirea de mai sus pare impresionanta (mi-a mancat ceva din viata :) ), pentru cei care vor sa foloseasca serverul "as it is" este necesara modificarea doar a catorva linii de cod. Chiar este simplu, credeti-ma!

   In prezent serverul are implementat controlul GPIO impreuna cu senzorul de temperatura DS18B20. In viitor daca va fi cazul probabil voi mai adauga si alti senzori. Asadar, pentru a adapta codul necesitiatilor voastre tot ce aveti de facut este sa urmati pasii:


  1. Deschideti o noua sesiune SSH (recomand cu caldura Moba Xterm) cu Raspberry Pi si instalati libraria Flask:

    sudo pip3 install flask
    


         2. Descarcati proiectul de aici 

         3. Creati un folder nou in ' /home/pi/' pe care il puteti denumi, de exmplu, Server_Login

         4. In noul folder desarcati proiectul dezarhivat. Din nou MobaXterm este prietenul vostru.



         Haideti sa vedem un pic structura folderelor si continutul acestora:

         In folderul radacina al proiectului:


    Folderul radacina

    -folderul 'static' contine fisierele necesare pentru pagina web: stiluri css de formatare, codul javascript.

    Folderul 'static'

    -folderul 'templates' contine codul paginilor web dinamice. Daca va 'bagati nasul' un pic prin ele veti observa ca acesta difera de codul html traditional. Asta deoarece paginile se genereaza in timp real in functie de user (admin sau guest), de numarul de iesiri si de cel de senzori. 



    Folderul templates

          Dupa cum spuneam si la inceputul articolului, daca doriti sa folositi serverul asa cum este el, nu aveti de modificat absolut nimic in fisierele rezidente in folderele 'static' si 'templates'.
    Folderul radacina mai contine doua fisiere: 'flask_server_login.py' si 'ds18b20.py'. Primul reprezinta serverul propriu-zis, iar cel de-al doilea driverul pentru senzorul de temperatura DS18B20.

    Codul pe care va trebui sa-l modificati este cel din fisierul 'flask_server_login.py'. Listingul este prezentat mai jos:



      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    #https://pythonspot.com/login-authentication-with-flask/
    #https://www.tutorialspoint.com/flask/flask_sessions.htm
    from flask import Flask
    from flask import Flask, flash, redirect, render_template, request, session, abort
    import os
    import time
    import threading as th
    import RPi.GPIO as g
    import ds18b20 as ts
    
    time.sleep(20)
     
    app = Flask(__name__)
    
    #date pentru tabel
    
    users=[{'Name':'admin','Pass':'1234','Rank':'admin'}, #users names, passwords, and ranks
    {'Name':'User','Pass':'1234','Rank':'admin'},
    {'Name':'guest','Pass':'1234','Rank':'guest'}]
    
    title="PANOU CONTROL RASPBERRY PI" # title of the table
    
    
    switches=(('Priza AC','p0'),('Bec Intrare','p1'),('Releu centrala','p2'),('Masina de spalat','p3'),('Veioza','p4'))# tupple of switches: switch designation and html id
    sensors = (('S.T. Dormitor','s1'),) # tupple of sensors: sensor name and html id
    
    a='enabled'  #template prototipe -> html enabled for switches. Only if rank is admin
    b='disabled' #template prototipe -> html enabled for switches. If rank is guest
    
    x=0
    y=0
    
    g.setmode(g.BCM)
    pins=[26,19,13,6,5]
    
    status=[0,0,0,0,0] #status array -> contains the status of switches
    sensorStatus=['-125'] #status of sensors
    
    def readTemp(): #read the temperature 
     ds=ts.ds18b20(1)
     sensorStatus=ds.readTemp()
     th.Timer(10,readTemp).start()
    
    def checkQuerry(a): # check if a valid request was sent. 
     for i in switches:
      if i[1]==a:
       return True
     return False  
    
    
    def setStringChange(): # the return string of change request
     j=0
     a=""
     for i in switches:
      a=a+i[1]+'_'+str(status[j])+':'
      g.output(pins[j],status[j])
      j=j+1 
     return a[0:len(a)-1]
    
    def setStatusString(): #the return of a status request
     a=setStringChange()
     b=""
     j=0
     #for i in switches:
      #a=a+i[1]+'_'+str(status[j])+':'
      #j=j+1 
     #a=a[0:len(a)-1] 
     #j=0
     for i in sensors:
      b=b+i[1]+'_'+str(sensorStatus[j])+':'
      j=j+1 
     b=b[0:len(b)-1]
     return a+"#"+b # return string has following pattern: p0_1:p1_0:p3_0:p4_0#s1_24.5:s2_19.1
     
    
    @app.before_first_request #before first request initiate the temperature loop
    def initTemp():
     readTemp()
     os.system('clear')
     cnt=0
     for i in pins:
      g.setup(i,g.OUT)
      g.output(pins[cnt],0)
      cnt=cnt+1
     
    @app.route('/') #
    def home():
     if not session.get('logged_in'): #if there is no field called 'logged_in' redirect the client to login form
      print('nelogat_home()')
      return render_template('login.html', mesaj='Ok')
     if session['rank']=='admin' and session.get('logged_in'): # if the client has already logged in return the control page for admin 
      return render_template('ctrl.html',title=title,switches=switches,sensor=sensors,st=a)
     if session['rank']=='guest' and session.get('logged_in'): # if the client has already logged in return the control page for guest
      return render_template('ctrl.html',title=title,switches=switches,sensor=sensors,st=b)
      
    @app.route('/login', methods=['POST'])
    def do_admin_login():
     u=request.form['username'] # read the values of fields 'username' and 'password'
     p=request.form['password']
     for us in users: # check if the user is in the dictionary
      if us['Name']==u and us['Pass']==p and us['Rank']=='admin':
       session['logged_in'] = True # asign to the session the filed logged_in 
       session['rank']='admin' # asign to the session the filed rank
       session['name']=u # asign to the session the name of user
      if us['Name']==u and us['Pass']==p and us['Rank']=='guest':
       session['logged_in'] = True
       session['rank']='guest'
       session['name']='Guest'
     return home()
     
    @app.route("/logout")
    def logout():
        session['logged_in'] = False
        return home()
     
    @app.route('/change',methods=['GET']) #change request
    def change():
     if not session.get('logged_in'): #if only the client is not logged_in
      return render_template('login.html',mesaj='Nu esti autentificat !') 
     if session.get('logged_in') == True and session.get('rank')=='admin': #if the client has the rank of admin
      req = request.args.get('change') # read the value of change request
      if checkQuerry(req): #check if it's a valid request 
       a=int(req.strip('p'))
       if status[a]==0:
        status[a]=1
       else:
        status[a]=0
       return setStringChange() #return the current status of switches
      else:
       session['logged_in']=False #if change request was written by hand logout the user
       return render_template('login.html',mesaj='Gotcha...')   
     elif session.get('logged_in')==True and session.get('rank')=='guest': #if a guest wants to change the switch from html logg him out
      session['logged_in']=False
      return render_template('login.html',mesaj='Autentifica-te ca admin !')
    
    
    @app.route('/status',methods=['GET'])
    def getStatus():
     if not session.get('logged_in'):
      return render_template('login.html',mesaj='Nu esti autentificat !') 
     elif session.get('logged_in') == True : 
      return setStatusString()
    
    
      
    if __name__ == "__main__":
     app.secret_key = os.urandom(12)
     app.run(debug=True,host='192.168.0.199', port=6000, threaded=True)
    
     
    

    Asadar sa incepem :) :

         Crearea userilor se face  modificand array-ul de dicitionare users (linia 17). Fiecare user trebuie sa contina un nume, o parola si un privilegiu (rank). Cei care au voie sa modifice starea switch-urilor vor avea rolul de 'admin', iar cei care pot doar vizualiza starea acestora sunt 'guest'. Puteti adauga oricat de multi useri cu conditia sa respecati tiparul din cod, cu alte cuvinte atentie la sintaxa.

         Dupa cum am zis crearea paginilor este dinamica, cu alte cuvinte codul html poate fi modificat direct de aici fara a necesita interventia asupra paginii. Variabila 'titlu' va  contine informatia necesara pentru afisarea denumirii panoului de comanda. Eu am ales sa se numeasca "PANOU CONTROL RASPBERRY PI", voi puteti sa modificati.

         Tupple-urile 'switches' si 'sensors' contin informatiile despre comutatoare si senzori. Prima intrare defineste denumirea senzorului iar cea de-a doua id-ul sau din pagina html. Puteti adauga sau sterge oricat de multe comutatoare, cu conditia sa fiti atenti la sintaxa. Ca ultim cuvant, in cazul in care aveti un singur switch sau senzor virgula de final este obligatorie (vezi cazul de fata tupple-ul sensors)

        Cele doua variabile 'a' si 'b', folosite in functie de 'rank' dau sau nu privilegiul de a schimba sau nu starea comutatoarelor.

      Mai departe (in linia 33) definim cum va fi numerotarea pinilor. In cazul de fata s-a ales numerotarea fizica la terminalele microprocesorului. In urmatoarea linie se defineste un array care contine numarul pinilor. Array-ul status (linia 36) contine starea initiala a switch-urilor. El trebuie sa contina tot atatea  elemente cate  cate comutatoare au fost definite. In cazul de fata sunt utilizate 5 comutatoare, deci 'status' contine tot cinci elemente.  Analog si pentru sensorStatus. 

    Astea sunt toate modificarile pe care le aveti de facut.

         Acum haideti sa vedem care este functionarea. Flask foloseste un sistem 'decorator', sistem pe care inca nici eu nu il stapanesc prea bine. Din ceea ce stiu va pot spune doar ca in functie de context sunt apelate functiile specifice. In cazul de fata au fost utilizate urmatoarele 'decoratoare':

    @app.before_first_request - inainte de a executa primul request se executa functia asociata:
     in cazulde fata - def initTemp():

    @app.route('/') - primul request atunci cand user-ul acceseaza adresa- se executa functia
    def home():

    @app.route('/login', methods=['POST']) - atunci cand userul trimite datele de autentificare
     se executa functia def do_admin_login():
    
    
    @app.route("/logout") - atunci cand userul doreste sa incheie sesiunea - se executa functia
    def logout():

    @app.route('/change',methods=['GET']) - atunci cand userul doreste sa schimbe starea unui
    comutator - se executa functia def change():

    @app.route('/status',methods=['GET']) - atunci cand userul doreste sa fle starea sistemului
    se apeleaza functia def getStatus():

    Cam atat ar fi de zis. Pentru intrebari va rog sa nu sfiiti sa lasati comentarii.

    Sa auzim de bine !


    2 comentarii:

    1. E ceva foarte interesant, pentru mine, ce e descris in acest articol. Saint foarte interesant, ca an un proect aproape identic si am nevoe de ajutor contra cost. Daca e posibil as dori sa am contact direct di Dvs.

      RăspundețiȘtergere
    2. Salut,cum se adauga un senzor nou?am modificat linia 25 dar degeaba ,nu mai afiseaza nici temp de la primul senzor.

      RăspundețiȘtergere