ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Ch.06 Flask를 활용하여 센서 제어하기
    Raspberry Pi ( python ) 2023. 3. 7. 15:22

    1 _ 프레임워크, Flask란?

     : 프레임워크는 앱이나 웹, 소프트웨어를 개발하기위해 들어가는 노력과 시간을 줄여주고자

    다양한 기능을 집합시켜서 개발자가 개발에만 집중할 수 있도록 도와주는 역할을 한다.

    대표적으로 장고(Django)와 플라스크(Flask)가 있는데, 장고는 큰 프로젝트를 진행하는데 유리한 프레임워크이다.

    여기서는 가벼운 플라스크를 활용할 것이다.

     

    ** 라즈베리 파이에서 Flask를 사용하고싶다면,

    VS code와 flask를 라즈비안에 다운받고 Vs code의 터미널에서 python을 실행한 뒤 flask를 import한다.

    vs code의 터미널에서 flask를 설치
    최신버전의 라즈비안이라 flak가 이미 설치되어 있는 모습
    python에 flask를 import한 결과, 아무 반응이 없으므로 설치가 완료된 것
    마지막으로 python Extension을 다운받는다.

     


    2 _ Flask 웹 서버 구축하기

    폴더와 파일 구성
    webapps
    	-ch06
        	-helloworld
            	-app_start.py

    예제 코드 작성
    해당 코드를 실행시킬 경우
    웹 서버 결과물

    코드 리뷰
    
    from flask import Flask
    = from 모듈명 import 함수명
    -> flask 폴더에서 Flask 클래스를 import 하라.
    
    
    app = Flask(__name__)
    -> flask 객체 생성,
    flask 객체 : 웹 브라우저로 오는 모든 요청과 템플릿과의 연계와 같은 웹 애플리케이션 전반에
    대해서 영향을 미치는 메인 객체.
    __name__은 Flask 객체 생성자 인자로서 내 파일명 , 모듈명을 의미한다
    해당 코드에서는 app_start.py가 된다.
    
    
    @app.route("/")
    -> 클라이언트가 URI로 ("/") 요청을 하게되면 해당 route 데코레이터 아래에 있는 뷰 함수가 자동호출 된다.
    ex) http://localhost:5000/ 으로 요청하면 해당 route가 호출됨
    	@app.route("/helloworld") 라면
        http://localhost:5000/helloworld 가 된다.
        
        
    def helloworld():
    	return "Hello World"
    -> route 데코레이터를 통해서 뷰 함수가 호출될 때 함수명 helloworld는 마음대로 정해도 된다.
    여기서 주의해야 할 점은 return 문이다.
    클라이언트가 주소요청을 하게 되면 뷰 함수가 호출되는데, 이때 뷰 함수에는 필수적으로 return문이 있어야한다
    여기서 json을 리턴 할 수도 있고, dict 자료형 리턴을 할 수도 있고
    필요에 따라 html 페이지로 뷰렌더링을 할 수도 있다.
    
    
    if __name__ == "__main__":
    	app.run(host="0.0.0.0")
    -> app.run() 이 호출되면 웹 서버가 동작한다.
    
    __name__은 두 가지 이름을 가진다. 첫 번째는 내 모듈명 app_start.py이다.
    ( __name__ ==> app_start.py )
    
    두 번째는 app_start.py파일을 직접 터미널에서 실행하게 되면 __main__이라는 이름을 가지게 된다.
    ( __name__ ==> "__main__" )
    
    만약에 다른 파일에서 app_start.py파일을 import해서 사용하면,
    __name__이 __main__이라는 이름으로 될 수 없다. ( import가 아닌 해당 파일에서 직접 실행해야함 )
    
    따라서 위의 if 문이 하는 역할은 app_start.py파일을 직접 실행시켰을 때만
    app.run() 명령을 수행하라는 뜻이다.

     - if __name__ == "__main__": 덕분에 코드의 실행 순서는 main을 최우선으로 실행할 수 있다.

     

    ** __name__ 은 내장변수이면서 전역변수이다.

    ** 0.0.0.0은 어떤 ip에서 요청이 들어와도 응답을 해주겠다는 의미이다.

     


    3 _ Flask 라우팅

     : Flask 에서 URL을 처리하는 방법을 다른 말로 URI 디스패치라고 한다.

    디스패치는 사용자가 입력한 URI를 지켜보고 있다가 URI를 분석해서 올바른 길로 안내해준다.

    뷰 데코레이션을 통해서 뷰 함수를 생성하여 올바른 길로 안내해준다.

     

    ** 뷰 데코레이터 @app.route("/내용") 에 따라 다른 함수를 생성하여 실행시킨다.

     


    4 _ Flask LED 제어하기

    1) 정적 라우팅 : @app.route("/led/on") , @app.route("/led/off")를 이용해서 LED를 ON/OFF한다.

    from flask import Flask
    import RPi.GPIO as GPIO
    
    app = Flask(__name__)
    
    LED = 8
    GPIO.setmode(GPIO.BOARD) #BOARD는 커넥터 pin번호 사용
    GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW)
    
    @app.route("/")
    def helloworld():
        return "Hello World"
    
    @app.route("/led/on")
    def led_on():
        GPIO.output(LED, GPIO.HIGH)
        return "LED ON"
    
    @app.route("/led/off")
    def led_on():
        GPIO.output(LED, GPIO.LOW)
        return "LED OFF"
    
    @app.route("/gpio/cleanup")
    def gpio_cleanup():
        GPIO.cleanup()
        return "GPIO CLEANUP" 
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0")

     


    2) 동적 라우팅<산형 괄호>

     : 동적으로 변경되는 URL의 뷰 함수를 사용하기 위해서는 산형 괄호 <>를 이용해서 변수를 전달해야한다.

    해당 변수가 클라이언트로 부터 URL로 전달되면 함수의 매개변수로 받아줘야 사용이 가능하다.

    from flask import Flask
    import RPi.GPIO as GPIO
    
    app = Flask(__name__)
    
    LED = 8
    GPIO.setmode(GPIO.BOARD) #BOARD는 커넥터 pin번호 사용
    GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW)
    
    @app.route("/")
    def helloworld():
        return "Hello World"
    
    @app.route("/led/<state>")
    def led(state):
        if state == "on":
            GPIO.output(LED, GPIO.HIGH)
        else:
            GPIO.output(LED, GPIO.LOW)
        return "LED "+state
    
    @app.route("/gpio/cleanup")
    def gpio_cleanup():
        GPIO.cleanup()
        return "GPIO CLEANUP" 
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0")

     - 선형 괄호를 사용하게 되면 uri를 입력받을 때 변수로 취급된다. 이 때 주의할 점은 함수의 파라메터로

    def led(state)를 등록해줘야 한다는 점이다.

     


    3) 동적 라우팅 <쿼리 스트링>

     : 쿼리 스트링 : http://localhost:5000/led?state=on 같은 형식으로 주소 호출이 올 때

    ? 뒤에 오는 것을 말한다.

    ? 뒤에 오는 것들은 HTTP요청에서 GET 방식으로 요청이 가능하다.

    전달되는 데이터를 request 클래스의 args 객체를 사용하여 받을 수 있다. ( flask 모듈의 request를 import해야 request 사용 가능 )

    from flask import Flask, request
    import RPi.GPIO as GPIO
    
    app = Flask(__name__)
    
    LED = 8
    GPIO.setmode(GPIO.BOARD) #BOARD는 커넥터 pin번호 사용
    GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW)
    
    @app.route("/")
    def helloworld():
        return "Hello World"
    
    @app.route("/led")
    def led_on():
        state = request.values.get("state", "error")
        if state == "on":
            GPIO.output(LED, GPIO.HIGH)
        elif state == "off":
            GPIO.output(LED, GPIO.LOW)
        elif state == "error":
            return "쿼리스트링 state가 전달되지 않았습니다."
        else:
            return "잘못된 쿼리스트링이 전달되었습니다."
        return "LED "+state
    
    @app.route("/gpio/cleanup")
    def gpio_cleanup():
        GPIO.cleanup()
        return "GPIO CLEANUP" 
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0")

     ** request.value.get()의 매개변수로 "state"뿐만 아니라 "error"가 있는 이유는

    서버 연결에 문제가 생겼을 경우 문제에 대한 정보를 error에 저장할 수 있도록 해야하기 때문이다.

     

    ** 파이선 딕셔너리는 json 형태로 주고받을 수 있는데, 

    from flask import request
    request.get_json()
    -> json 데이터를 가져온다
    
    request.get_json('key')
    -> key 값으로 value를 바로 조회
    request.args.get('key')
    -> request.get_json('key') 와 같은 결과를 가져온다

     


    5 _ Flask 웹페이지

    1) Flask, HTML, CSS, Javascript를 이용한 웹페이지 만들기

     - 기본 폴더 구성

    webapps
    	- ch06
        		- home
            		- static
                		- templates

     - home 폴더가 최상위 root 폴더가 된다. 그 아래에 static 폴더는 flask에서 리소스(자원) 폴더로 사용한다. ( css, image, media 등의 파일을 static 폴더에 저장 )

    templates 폴더는 html 파일 폴더로 사용한다. templates 폴더를 사용할 때 유의사항은 flask는 기본적으로 view 랜더링을 할 때 html 파일을 호출하는데, html 파일은 무조건 templates 폴더에 있는 html 파일을 호출하게 설정되어 있다.

    그렇기 때문에 temp같이 다른 이름으로 폴더명을 설정하면 html파일을 랜더링 하지 않는다.

     

     - 파이썬 index를 home 폴더에 저장

    from flask import Flask, request
    from flask import render_template
    import RPi.GPIO as GPIO
    
    app = Flask(__name__)
    GPIO.setmode(GPIO.BOARD) # BOARD는 커넥터 pin번호 사용
    GPIO.setup(8, GPIO.OUT, initial=GPIO.LOW)
    
    @app.route("/")
    
    def home():
    	return render_template("index.html")
        
    @app.route("/led/on")
    def led_on():
    	try:
        	GPIO.output(8, GPIO.HIGH)
            return "ok"
        except expression as identifier:
        	return "fail"
            
    @app.route("/led/off")
    def led_off():
    	try:
        	GPIO.output(8, GPIO.HIGH)
            return "ok"
        except expression as identifier:
        	return "fail"
            
    if __name__ == "__main__":
    	app.run(host="0.0.0")

    ** home() 의 return render_template("index.html") 은 파이썬에서 .html파일을 렌더링하여 브라우저에 나타낼 수 있다.

     

     - html 파일을 templates 폴더에 저장

    <!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Home Network</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}">
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h2>HOME IoT</h2>
            </div>
            <div class="main">
                <div>
                    <button onclick="led_on()">LED ON</button>
                </div>
                <div>
                    <button onclick="led_off()">LED OFF</button>
                </div>
            </div>
            <div id="ressult">
    
            </div>
        </div>
        <script>
            function led_on(){
                fetch("/led/on")
                .then(response=> response.text())
                .then(data=> {
                    console.log(data);
                    let result = document.querySelector("#result");
                    if(data=="ok"){
                        result.innerHTML = "<h1>LED is running</h1>";
                    } else {
                        result.innerHTML = "<h1>error</h1>";
                    }
                });
            }
             function led_off(){
                fetch("/led/off")
                .then(response=> response.text())
                .then(data=> {
                    console.log(data);
                    let result = document.querySelector("#result");
                    if(data=="ok"){
                        result.innerHTML = "<h1>LED is stopping</h1>";
                    } else {
                        result.innerHTML = "<h1>error</h1>";
                    }
                });
            }
        </script>
        
    </body>
    </html>

     - button 태그에 onclick 이벤트를 넣어서 led_on() 과 led_off() 를 실행시킨다.

     

     - css 파일을 static 폴더에 저장

    body {
        background-color: antiquewhite;
    }
    
    .container {
        width: 700px;
        margin: 0 auto;
        text-align: center;
    }
    
    .main {
        display: flex;
    }
    
    .main div {
        flex:1;
    }
    
    .main div button {
        background-color: rgb(192, 114, 114);
        width: 150px;
        height: 80px;
        border-radius: 10px;
    }

     

     - 실행방법은 터미널을 열고 해당 폴더로 이동해서 다음과 같이 입력한다.

    python home.py

     

     - LED ON을 클릭하면 LED에 불이 들어오고 "LED is running" 문구 생성,

    LED OFF를 클릭하면 LED에 불이 꺼지고 "LED is stopping" 문구 생성

     


    2) fetch() 함수

     : 자바스크립트의 문법으로서, ajax를 이용한 비동기 통신을 할 때 사용하는 라이브러리이다.

    fetch를 통해서 index.py의 led_on() 메서드를 호출하고 그 결과를 response를 통해서 받는다.

    led_on 메서드를 호출할 시에 실제 led가 on이 되고 정상적으로 on이 되면 "ok" 를 return 하게 된다.

    return 된 값을 fetch().then(response) 이 부분에서 then()이 받게 된다.

    따라서 then()은 fetch가 완료된 후에 호출된다.

     

    fetch 요청 시에는 pending(대기)되고 있다가 요청이 완료되면 then(response)로 요청에 대한 결과를 return해주게 된다.

    ( 콜백 지옥에서 벗어날 수 있다 ) 이는 promise 기술이 들어가 있기 때문이다.

     

    return된 결과는 "ok" 메시지뿐만 아니라 수많은 정보를 가지고 있다. 이런 정보를 자바스크립트 오브젝트로 만들어서

    요청한 쪽으로 return하는 것이다.

    --> console 창으로 불러오면 다양한 정보들을 return하는 것을 확인할 수 있다.

     

    "ok" 메시지를 받으려면 then()이 한 번 더 필요하다.

    첫 번째 then()에서 전체응답 오브젝트를 받고, 두 번째 then()은 그 많은 데이터중 "ok"라는 메시지를 필터링해준다.

     

    ** 서버 쪽에서 return 되는 결과가 String이면 text()함수를 사용하고, JSON이면 json()함수를 이용하면 된다.

     


    3) 화살표 함수 이해하기

     - 위의 코드에서 fetch  함수의 then() 부분을 풀어쓰면 다음과 같은 형태가 된다.

    then(
    	function(response){
        	return response;
        }
    );

     - 위와 동일한 코드를 화살표 함수로 정리하면

    then(
    	response => response;
    );

     - 만약 중괄호를 넣는다면 return문이 필수로 들어가야한다. 대괄호가 없다면 return문이 생략되어있는 것과 같은 효과를 준다

    then(
    	response => { return response };
    );

     - 매개변수가 두개인 경우에 기존 코드는 다음과 같이 나타난다.

    then(
    	function(response1, response2){
        	return response1;
        }
    );

     - 매개변수가 두 개인 함수를 화살표로 나타내면 다음과 같이 소괄호를 넣어야한다

    then(
    	(response1, response2) => response1;
    );

     - 중괄호를 넣고싶다면 마찬가지로 return 문이 필요하다

    then(
    	(response1, response2) => {return response1 };
    );

     - then() 안에 있는 함수는 이름이 필요없는 익명함수이다.

    then(
    	function(response){
        	return response;
        }
    )

     

    ** 정리하자면 화살표 함수는 매개 변수를 받아서 그 값을 리턴하는 함수를 사용하고 싶을 때 간단한게

    작성할수 있는 함수라고 볼 수 있다. 그렇기 때문에 항상 리턴하려는 값이 있어야한다.

     


    6 _ Ip, Port, Process 개념

    1) IP 개념잡기

     : IP 는 internet protocol의 약자이다. 송신 호스트와 수신 호스트가 데이터를 서로 주고 받는데 필요한 프로토콜(규약)을 의미한다. 데이터는 패킷으로 분할되어 전송되고 받은 쪽에서는 분할된 패킷을 조립하게 된다.

    DATA -> 패킷으로 분할 -> 전송 -> 조립 -> 데이터
    수신 호스트 ---------------------------> 송신 호스트

     - 이 때 데이터가 패킷으로 분할되고 분할된 패킷이 송신될 때 순서가 보장되지는 않는다.

    따라서 받는 쪽에서 패킷을 조립할 때 기준이 필요한데, 이 규약들을 프로토콜이라고 한다.

     

    데이터를 분할하지 않고 보내는 서킷 스위칭 방법이 있지만 서킷 스위칭 방법은 송신 호스트와 수신 호스트가 다수라면

    회선도 다수가 필요하고 동시에 data를 송수신 할 수 없기 때문에

    다수와 다수가 동시에 데이터를 주고받기 위해 패킷 스위칭 방식을 쓴다.

     

    또한 수신 호스트의 경우 자신이 가진 프로토콜에 맞게 변형된 형태로 데이터를 얻을 수도 있게 된다.

     

     


    2) port 개념 잡기

     : port는 컴퓨터 간 상호 통신을 위해 프로토콜에서 이용하는 가상의 연결 단을 의미한다. 여기서 중요한 키워드는

    바로 가상의 연결 단이라는 것이다.

    가상의 구역을 만들어서 데이터를 주고받고자 하는 수신 호스트에게 port를 개방해주고 수신 호스트가 port에서 데이터를

    전달받을 수 있게 만들어준다.

    port는 총 2byte개로 정해져있다. 따라서 하나의 컴퓨터마다 약 65536개의 포트가 있다고 본다.

     

    65536개의 port 중에 well known port라고 잘 알려진 port가 있다.

    0~1023번 까지의 1024개의 포트는 용도가 정해져 있기 때문에 사용할 수 없게 설정되어있다.

     


    3) process 개념잡기

     : 프로세스와 프로그램은 명확한 차이가 있다.

    프로그램 자체는 생명이 없다. 프로그램은 보조 기억장치(하드디스크, ssd)에 존재하며 실행되기를 기다리는 명령어(코드)와 정적인 데이터의 묶음이다. 이 프로그램의 명령어와 정적 데이터가 메모리에 적재되면 생명이 있는 프로세스가 된다.

    즉 프로세스는 실행 중인 프로그램이다.

     

    프로그램이 프로세스가 되기 위해서는 실행을 해야한다

    $ cd ~/webapps/ch06/process
    $ python process.py

    프로세스를 확인하는 방법

    포그라운드 프로세스 확인
    $ ps -f
    
    백그라운드 프로세스 확인
    $ ps -ef		( -e 는 백그라운드 프로세스를 확인하는 옵션이다. )
    
    특정 프로세스만 확인하는 방법
    $ ps -ef | grep process.py
    -> grep 명령어는 파이프라인 명령어인데 파일에서 특정한 문자열을 찾는 명령어이다.
    두 개의 명령어를 연결하기위해서 | 를 써야한다.
    
    프로세스를 종료하는 방법
    $ sudo kill pid
    -> 프로세스가 실행중이면 특정 port를 차지하고 각 포트는 pid 값을 갖는다.
    OSError: [Errno 98] Address already in us
    메시지가 뜬다면 이전에 실행한 프로세스가 종료되지 않아서 포트가 충돌이 났다는 뜻이므로
    위의 명령어를 실행해서 이전 프로세스를 종료하자.

     


    7 _ cron과 daemon

    1) cron(크론)

     : cron은 주기적인 실행이 필요할 때 사용한다. 매일 자정에 데이터를 백업하고 싶다거나

    아침7시가 되면 led를 작동하고 싶다면 cron을 사용하자.

    $ crontap -1
    no crontab for pi

     - 처음에는 등록된 cron이 없다고 뜰 것이다. cron 폴더를 생성하고 내부에 실행 파일을 생성해야한다.

    cron 파일의 위치를 다음과 같이 지정하자.

    webapps
    	- ch06
        		-cron
            		-led.py

     

     - 실행 코드

    import RPi.GPIO as GPIO
    import time
    
    GPIO.cleanup()
    
    LED = 8
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW)
    
    try:
    	num = 0
        while(True):
        	time.sleep(1)
            GPIO.output(LED, GPIO.HIGH)
            time.sleep(1)
            GPIO.output(LED, GPIO.LOW)
            num = num +1
            if(num == 5):
            	GPIO.cleanup()
                break
    except KeyboardInterrupt:
    	GPIO.cleanup()

     - 이 코드는 LED가 5초 동안 깜빡거리에 된다.

     8번 채널을 중복사용하게 되는 error를 방지하기 위해 GPIO.cleanup() 을 상단에 작성했다.

     

     - 다음 코드를 실행해서 위의 코드를 cron에 등록하자.

    $ crontab -e

     - 1분마다 위의 실행파일을 실행하라는 명령어를 적어준다

    *****sudo python /home/gt/webapps/ch06/led.py

     - cron을 더이상 사용하고 싶지 않다면 $ crontab -e에 다시 들어가서 명령어를 지워주면 된다.

     

    ** crontab을 등록할 때 python 코드에 한글이 있으면 문자 encoding을 해줘야한다.

    ( # -*- encoding: utf-8 -*-)

     

    ** crontab의 시간 설정은 ***** 을 변경해서 한다. 외우기에는 조금 복잡하므로 필요할 때 찾아서 사용하면 좋을 것 같다.

     


    2) daemon(데몬)

     : 크론이 주기를 가지고 실행되는 것이라면 데몬은 계속 실행되는 것이다. 라즈베리 파이를 재부팅 했을 때부터

    종료 시 까지 자동으로 실행함과 동시에 계속적인 동작을 원한다면 데몬으로 등록한다.

     

     - 파일 위치

    webapps
    	-ch06
        		-daemon
                		-led2.py

     - 실행 파일

    #! /usr/bin/env python3.9.2
    #해당 파일을 다른곳에서 실행할 때 pyton실행파일을 사용하여 실행해라
    
    import RPi.GPIO as GPIO
    import time
    
    GPIO.cleanup()
    
    LED = 8
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW)
    
    try:
        num = 0
        while(True):
            time.sleep(1)
            GPIO.output(LED, GPIO.HIGH)
            time.sleep(1)
            GPIO.output(LED, GPIO.LOW)
            num = num + 1
            if(num == 5):
                GPIO.cleanup()
                break
    except KeyboardInterrupt:
        GPIO.cleanup()

     - 제일 상단에 적은 코드에 유의하자.

     

     - rc.local 파일을 수정하자

    $ sudo nano /etc/rc.local

     - fi 아래에 실행할 구문을 넣고 마지막에 &을 붙인다. ( &를 붙이면 백그라운드로 실행한다 )

    $ chmod 777 led2.py

     - led2.py 파일에 실행권한을 준다. 소유자, 소유그룹, 모든 유저에게 모든 권한을 준다.

    데몬의 결과로 재부팅 이후에 LED가 5초동안 깜빡이고 있는 것을 확인할 수 있다.

     

    ** 서버를 오픈하는 코드를 데몬에 넣어두면 서버 연결이 끊겨도 자동으로 서버를 갱신해주는 시스템이 된다.

Designed by Tistory.