2019년 12월 12일 목요일

라즈베리 파이에서 REST API 사용해서 스마트폰으로 제어하기 (Use REST API in Raspberry Pi for controlling from smartphone)

라즈베리 파이에서 flask를 사용해 REST API를 구현하여 스마트폰이나 컴퓨터에서 원격으로 센서값을 읽거나 장치를 제어할 수 있도록 간단하게 구현 해 보았다.
HTTP GET과 POST method를 사용할 수 있도록 해 놓았고 POST method에서 파라미터는 JSON으로 넘겨주면 된다.
아래 코드가 구현해 놓은 API는 다음과 같다.

* /api/led :
    method: POST
    parameter: {'ledId': [id], 'val': [OnOff]}  - [id]는 조작하고자 하는 LED번호, [OnOff]에는 LED를 켜고 싶으면 1, 끄고 싶으면 0
    return: 명령이 실행되고난 후 LED 상태


* /api/led :
    method: POST
    parameter: {'ledId': [id], 'val': [OnOff]}  - [id]는 조작하고자 하는 LED번호, [OnOff]에는 LED를 켜고 싶으면 1, 끄고 싶으면 0
    return: 명령이 실행되고난 후 LED 상태

* /api/sw :
    method: POST
    parameter: {'id': [id]}  - [id]는 상태를 알고 싶은 스위치 번호
    return: 현재 스위치 상태

* /api/sensor/[sensorName] :현재 sensorName에 올 수 있는 센서는 temperature, humidity, pressure, light
    method:GET
    return:지정한 센서에서 읽은 값

* /api/sensors :
    method: GET
    return: 전체 센서 값 (JSON 포맷)

* /api/sensors :
    method: POST
    parameter: {'sensorList': [sensor list]}  -값을 읽고 싶은 센서 이름. 한개 이상이 올 수 있고 복수개일 경우 ','로 구분
    return: 지정한 센서에서 읽은 값 (JSON 포맷)

* /api/exec :
    method: POST
    parameter: {'cmd': [command line]}  - [commnad line]은 라즈베리 파이에서 실행할 명령
    return: 명령 실행 결과

코드를 실행하면 라즈베리 파이에 다음과 같은 화면이 나오게 된다.

PC의 브라우저에 [ip]:5000/api/sensors 를 입력해 주면 다음과 같은 결과를 얻을 수 있다. 이 경우 4개의 센서값을 모두 JSON 포맷으로 리턴해 준다.


[ip]:5000/api/sensor/temperature 를 읿력해 주면 다음과 같이 온도센서 값만 리턴 해 준다.


지금까지는 브라우저의 주소창에 입력했는데 이 경우 GET method를 사용하는 것이다. POST method를 확인하려면 curl, postman, rested 같은 툴을 사용해야 한다. 여기서는 파이어폭스에 설치한 rested를 사용했다.

앞으로 파란색 부분은 저 값으로 고정시켜 놓고 빨간색 부분만 바꿔주면서 테스트 하면 된다. 위의 사진은 api/led 를 호출해 원하는 LED를 on/off 하는 예제이다. 소스코드의 @app.route('/api/led', methods = ['POST']) 에서 빨간색 굵은 부분이 주소가 된다. 단 여기서는 flask 서버가 5000번 포트를 사용하고 있으므로 주소 뒤에 ':5000'을 추가해 줘야 한다.
Request Body 부분에 id와 val 이라는 두개의 키가 있어 id의 값 부분에 조작할 LED 번호를 넣어주고, val의 값 부분에 LED on/off 를 넣어주면 된다.
실행이 되면 바뀐 LED 상태값이 리턴된다. 위의 예에서는 4번 LED를 ON 시킨 것이다. 맨 마지막 부분에 결과로 'ON'이 리턴되었다.


라즈베리 파이 화면을 보면 이번엔 /api/led 주소로 POST emethod를 사용했음을 확인할 수 있다.

위의 화면은 LED를 끄는 것이다. 나머지 부분은 동일하고 val 의 값만 '0'으로 보내주면 된다.

다음은 /api/sw를 통해 스위치 상태를 확인해 본 것이다. URL 부분이 /api/led에서 /api/sw로 바뀌었다. 또한 이번엔 파라미터로 id만 보내주면 된다. 여기서는 2번 스위치의 상태를 확인해 본 것이다. 현재 스위치 상태 값이 리턴된 걸 확인할 수 있다.


다음은 /api/sensors를 호출하는데 위쪽에서는 GET method로 호출해서 모든 센서 값을 읽어왔는데 여기서는 POST method를 사용해 값을 읽기를 원하는 센서만 지정할 수 있다. sensorList 의 값 부분에 원하는 센서 이름을 넣어주면 된다. 두개 이상을 넣을 땐 센서 이름 사이에 ','로 구분해 주면 된다. 여기서는 temperature와 humidity 센서 값을 읽도록 해 보았다. 결과값으로 두 센서값이 JSON 포맷으로 리턴되었다.


마지막으로는 라즈베리 파이에서 명령을 실행할 수도 있다. /api/exe에 POST method로 실행하기 원하는 명령을 보내주면 된다. 실행된 결과가 리턴된다. 이 방법을 이용하면 라즈베리 파이에 연결해 놓은 장치가 API를 제공해 주지 않거나 python module이 없고 CLI로 제어하는 프로그램만 있어도 원격에서 제어가 가능해 진다.


전체 코드는 다음과 같다. 이 코드는 Raspberry Pi 4에서 python3를 사용해 동작시켜 보았는데 python3만 지원하는 환경이면 어디에서나 동작하는데 문제는 없을 것이다.

* 여기서는 Raspberry Pi에서 실행되는 서버 코드만 설명하고 있다. 스마트폰 앱에서 제어하려면 해당하는 GET, POST 메소드로 API를 사용하면 된다. 해당 부분 코드 작성은 직접 하지 않아도 postman같이 자동으로 생성해주는 툴을 사용하면 매우 간단하게 사용할 수 있다. 다음 포스트에서는 postman을 사용해 클라이언트의 http request 부분의 코드를 자동으로 생성하는걸 설명하겠다.


#!/usr/bin/python3

from flask import Flask, request, jsonify
import os, ntpath
import json
import subprocess

app = Flask(__name__)

# 실제로는 GPIO library를 사용해 읽은 스위치 값을 리턴해줘야 함
def readSwitch(id):
  print ("# readSwitch is called for switch %d" % id)
  return 1

# 실제로는 온도센서 값을 읽어 리턴해 줘야 함
def readTemp():
    return str(25.3)

# 실제로는 습도센서 값을 읽어 리턴해 줘야 함
def readHumid():
    return str(73)

# 실제로는 압력센서 값을 읽어 리턴해 줘야 함
def readPressure():
    return str(1025)

# 실제로는 조도센서 값을 읽어 리턴해 줘야 함
def readLight():
    return str(419)

def readSensor(s):
    return sensorFunc[s]()

sensorFunc = {'temperature': readTemp, 'humidity': readHumid, 'pressure': readPressure, 'light': readLight}

@app.route('/api/led', methods = ['POST'])
def ledSvc():
    data = request.json
    #ledId = data['id']
    #onOff = data['val']
    ledId = int(data['id'])
    onOff = int(data['val'])
    if onOff == 0:
      newLedState = 'OFF'
      # 여기에 해당 LED(LED번호는 변수 ledId에 들어 있음)를 끄는 코드를 집어 넣어 줌
    else:
      newLedState = 'ON'
      # 여기에 해당 LED(LED번호는 변수 ledId에 들어 있음)를 켜는 코드를 집어 넣어 줌
    print ("Turn LED %d to %s" % (ledId, newLedState))
    return newLedState

@app.route('/api/sw', methods = ['POST'])
def swStatus():
    data = request.json
    ledId = int(data['id'])
    swState = readSwitch(ledId)  # readSwitch() 함수에서 스위치(ledId) 값을 읽어 리턴해 줌
    print ("Switch %d state : %d" % (ledId,swState))
    return str(swState)

@app.route('/api/sensor/<string:sensorName>', methods = ['GET'])
def readSensor(sensorName):
    try:
        ret = sensorFunc[sensorName]()
        return ret
    except:
        return 'Wrong Sensor'

@app.route('/api/sensors', methods = ['GET', 'POST'])
def readSensors():
    if request.method == 'GET':
        result = {}
        for s in sensorFunc:
            result[s] = sensorFunc[s]()
    elif request.method == 'POST':
        result = {}
        sl = request.json['sensorList']
        print (sl)
        sl = sl.split(',')
        for s in sl:
            print (s)
            result[s] = readSensor(s)

    return json.dumps(result)

@app.route('/api/exec', methods = ['POST'])
def externalPgm():
    data = request.json
    cmds = data['cmd']
    print (cmds)
    cmdl = cmds.split(' ')
    result = subprocess.check_output(cmdl)
    return result

if __name__=='__main__':
    app.run(host='0.0.0.0', debug=True)