레이블이 원격 제어인 게시물을 표시합니다. 모든 게시물 표시
레이블이 원격 제어인 게시물을 표시합니다. 모든 게시물 표시

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)











2010년 6월 15일 화요일

mbed RPC 인터페이스 (RPC interface for mbed)



mbed는 인터페이스 오브젝트에 이름을 부여해주면 텍스트 기반 인터페이스를 통해 제어할 수 있다.

메소드는 url과 유사한 방식으로 호출된다.

/<objectname>/<method> <arg1> <arg2> ...

가장 기본적인 예제이다.

#include "mbed.h"

DigitalOut led(p1, "led");

int main() {
    char result[64];
    
    rpc("/led/write 1", result);
    printf("%s\n", result);
    
    rpc("/led/read", result);
    printf("%s\n", result);
}


Remote Control

mBed를 터미널에 연결하고 시리얼 포트 입력을 기다리도록 하면 시리얼을 통해 직접 명령어를 입력하고 그 결과를 돌려받을 수 있다.

#include "mbed.h"

Serial pc(USBTX, USBRX);

DigitalIn led(p1, "led");
AnalogIn pot(p20, "pot");

int main() {
    char buf[256], outbuf[256];
    while(1) {
        pc.gets(buf, 256);
        rpc(buf, outbuf);
        pc.printf("%s\n", outbuf);
    }
}

터미널이 엔터키가 눌렸을 때 LF(또는 CR+LF)를 보내도록 설정되어 있어야 한다.
 

위의 프로그램이 실행되고 있으면 터미널에서 직접 명령어를 넣어주면 된다.

/led/write 1<return>     LED를 켠다.
/pot/read<return>        아날로그 핀 20의 값을 읽는다.  

Remote Creation of mbed interfaces

RPC인터페이스를 사용하면 원격에서 인터페이스를 만들거나 삭제할 수 있다.


새 인터페이스를 만들려면 mbed에게 만들려고 하는 인터페이스에 대해 알려줘야 한다. 아래 코드는 원격 어플리케이션에서 이런 동작을 할 수 있도록 해 주는 시리얼 어플리케이션 예제이다.

/* RPCSerial - RPC over a serial port transport
 * sford
 *
 * When running, you can send RPC commands to the mbed
 * via the serial port, to create and manipulate objects
 *
 * Example:
 * > "/DigitalOut/new p2\n"
 * > > "objXYZ\n"
 * > "/objXYZ/write 1\n"
 * > "\n"
 * > "/objXYZ/read"
 * > "1\n"
 */
#include "mbed.h"
#include "rpc.h"
Serial pc(USBTX, USBRX);
int main() {
    // setup the classes that can be created dynamically
    Base::add_rpc_class<AnalogIn>();
    Base::add_rpc_class<AnalogOut>();
    Base::add_rpc_class<DigitalIn>();
    Base::add_rpc_class<DigitalOut>();
    Base::add_rpc_class<PwmOut>();
    Base::add_rpc_class<Timer>();
    Base::add_rpc_class<SPI>();
    Base::add_rpc_class<BusOut>();
    Base::add_rpc_class<BusIn>();
    char command[256] = {0};
    char response[256] = {0};
    // receive commands, and send back the responses
    while(1) {
        pc.gets(command, 256);
        rpc(command, response);
        pc.printf("%s\n", response);
    }
}


실행 예제 (볼드체로 된 부분이 직접 입력한 내용이고 이태릭체로 된 부분은 mbed의 응답이다.)

/DigitalOut/new LED1
obj400058B0
/obj400058B0/write 1



/AnalogIn/new p3 volume

volume
/volume/read
0.297
/volume/delete

시리얼 인터페이스 뿐 아니고 mbed에 내장된 웹서버를 사용해 HTTP 프로토콜로 RPC 호출을 할 수도 있다. URL의 페이지 주소가 '/rpc/'로 시작하면 웹 서버가 RPC 리퀘스트로 처리한다.


예를 들어 URL이 'http://mbed-ip-or-hostname/rpc/led1/write+1' 이면 시리얼 터미널에서 '/led1/write 1'을 입력한것과 동일한 동작을 수행한다. (URL주소에서 space는 '+'로 대치)


#include "mbed.h"
#include "HTTPServer.h"
#include "HTTPRPC.h"
#include "HTTPFS.h"
HTTPServer http;
DigitalOut led1(LED1, "led1");
DigitalOut led2(LED2, "led2");
DigitalOut led3(LED3, "led3");
DigitalOut led4(LED4, "led4");
LocalFileSystem local("local");
int main(void) {
    Base::add_rpc_class<AnalogIn>();
    Base::add_rpc_class<AnalogOut>();
    Base::add_rpc_class<DigitalIn>();
    Base::add_rpc_class<DigitalOut>();
    Base::add_rpc_class<PwmOut>();
    Base::add_rpc_class<Timer>();
    Base::add_rpc_class<SPI>();
    Base::add_rpc_class<BusOut>();
    Base::add_rpc_class<BusIn>();
    http.addHandler(new HTTPRPC());
    http.addHandler(new HTTPFileSystemHandler("/", "/local/"));
    http.bind();
    while(1) {
        http.poll();
    }
}