강좌 전체보기

.

앞 장에서 ESP32 모듈에 다양한 WiFi 기능을 구현하는 방법들을 다뤘습니다. 이제 센서장치가 외부 서버든 모바일이든 WiFi 로 연결되어 통신할 수 있는 준비는 갖춰졌습니다. 큰 산을 넘은 것이라 할 수 있습니다.

하지만 사물인터넷 서비스는 센서장치가 통신 기능을 갖추는 것만으로 완성되지 않습니다. 실제 서비스를 사용하는 사용자에 따른 동작 시나리오와 UI를 결정해야 하고, 사물인터넷에 참여하는 각 주체가 가장 효율적으로 동작할 수 있는 방식을 고려해야 하며, 사물 인터넷의 각 노드들이 통신할 때 사용할 데이터 전송규칙(프로토콜)을 정해야 합니다.

만약 상용 서비스를 구현하는 단계가 아닌 프로토타이핑 수준이라면 이 모든 것들을 고려해서 시작할 필요는 없습니다. 일단은 자주 사용되는 사물인터넷 서비스 구현 방법 몇 가지 중 자신에게 필요한 방법을 선택하고, 해당 서비스 구현을 위해 필요한 기본 코드부터 구현하고 살을 붙여나가는 것이 빠르고 효율적입니다.

이번 장의 목표가 바로 이것입니다. 센서장치,  서버, 모바일이 함께 동작할 수 있는 몇 가지 서비스 시나리오를 제시하고, 각각을 구현하는 기본 코드를 제공해서 빠르게 프로토타이핑을 시작할 수 있도록 하는 것입니다.

이번 파트에서는 Node.js 를 이용해 웹 서버를 구축하고 센서장치가 랜덤한 숫자 데이터를 생성해서 HTTP POST 로 보내주도록 만들겠습니다. 서버는 이 데이터를 수집해서 저장하고 있다가, 외부에서 HTTP 요청이 오면 수집된 데이터 중 최신 데이터를 HTML 파일 형태로 보여줄 수 있도록 예제 서비스를 만들어 보겠습니다.

.

.

NodeJS 를 이용한 웹 서버 구축

웹 서버 또는 API 서버를 구축해서 서버를 중심으로 데이터가 모이고 분산되는게 기본입니다. 이걸 어떤 플랫폼/언어로 구현할지가 문제이지요. 웹을 기본으로 하는 서버를 구축할때는, 특히 프로토타이핑 수준에서라면, Node.js 가장 합리적인 해답이 될 수 있습니다.

Node.js 는 Java Script 기반의 웹 서버 플랫폼입니다. 많이 사용되는 플랫폼이기 때문에 관련된 자료 구하기도 쉽고 각종 모듈/라이브러리 구하기도 쉽습니다. 그리고 라즈베리파이에 올려 구동 시키는데도 무리가 없습니다.

이번 파트에서는 Node.js를 이용해서 웹 서버를 구축해 보겠습니다. 센서장치가 수집한 데이터는 Node.js 에 모이고, 웹으로 확인이 가능하도록 만들겠습니다.

라즈베리파이에 Node.js 를 설치부터 해야겠지요. 여기서 설치과정 전부를 다루기엔 내용이 길어 별도의 포스트로 만들었으니 링크 3개를 참고해서 라즈베리파이에 Node.js 를 설치하고 기본적인 테스트를 해주세요.

그럼 라즈베리파이에서 Node.js 를 굴리는 기본 방법은 습득하셨을 겁니다.

위 링크에서는 Node.js 서버 구축하고 간단한 HTML 코드를 보여주는 정도의 예제였습니다. 하지만 우리는 이보다는 좀 더 복잡한 서버가 필요합니다. 아래 일들을 처래해줘야 합니다.

  • 센서장치에서 서버에 데이터 전송할 수 있는 API
    • 센서장치에서는 HTTP POST 방식으로 데이터를 전송
  • 센서장치에서 수집한 데이터를 저장
  • 외부에서 HTTP 요청이 오면 수집된 데이터 중 최신 데이터를 HTML 화면으로 전송

데이터 저장을 위해서는 DB를 쓰는 것이 정석이겠지만, 여기서는 구현의 편리를 위해 파일로 저장하겠습니다. 대신, 마지막 장에서 홈 오토메이션 시스템을 구축할 때 DB를 쓰도록 하겠습니다.

서버 코드 수정에 앞서 HTTP 요청을 통해 들어오는 body를 파싱하기 위해 body-parser 라는 모듈을 설치해야 합니다.

npm install body-parser

이상의 요구사항을 충족하는 코드는 아래와 같습니다. index.js 파일을 아래와 같이 수정해주세요.

.

var express = require('express')
var fs = require('fs')
var path = require('path');
var bodyParser = require('body-parser');
var app = express()

app.locals.pretty = true
app.set('views', './view_file')
app.set('view engine', 'pug')
app.use(bodyParser.urlencoded({ extended: true }));
app.listen(3000, () => {
  console.log("Server has been started")
})

var dataFolderPath = path.join(__dirname, '/data')
var dataPath = path.join(dataFolderPath, '/data.txt') 

app.get("/", (req, res) => {
  res.redirect('/hello')
})

// 저장된 데이터가 있으면 데이터 출력
app.get("/hello", (req, res) => {
  if(!fs.existsSync(dataFolderPath) || !fs.existsSync(dataPath)) {
    res.render('hello', { title: 'Hello', message: 'Hello World!!!'})
  }
  else {
    fs.readFile(dataPath, 'utf-8', (err, data)=> {
      res.render('data', { title: 'Hello', data: data.split('\n')})
    })
  }
})

// 센서장치에서 데이터를 업데이트하는 API
app.post("/data", (req, res) => {
  var recvData = req.body.data
  // 데이터 저장 폴더 및 데이터 저장 파일 생성
  if(!fs.existsSync(dataFolderPath)) {
    fs.mkdir(dataFolderPath)
  }

  if(!fs.existsSync(dataPath)) {
    fs.appendFile(dataPath, recvData+'\n', (error) => {
      if(error) {
        res.status(500).json({ 'msg': 'Internal server error' });
      }
      else {
        res.status(200).json({ 'msg': 'Data registered successfully' });
      }
    })    
  }
  else {
    fs.readFile(dataPath, 'utf-8', (err, data)=> {
      // 10개 이상 데이터 추가 시 10개만 저장
      var dataArr = data.split('\n')
      if(dataArr.length < 10) {
        fs.appendFile(dataPath, recvData+'\n', (error) => {
          if(error) {
            res.status(500).json({ 'msg': 'Internal server error' });
          }
          else {
            res.status(200).json({ 'msg': 'Data registered successfully' });
          }
        })
      }
      else {
        dataArr.splice(dataArr.length-1, 1)
        dataArr.splice(0,dataArr.length - 9)
        dataArr.push(recvData)
        var file = fs.createWriteStream(dataPath);
        file.on('error', (err) => { if(err) console.log(err) })
        dataArr.forEach((item) => { file.write(item + '\n') })
        file.end();
      }
    })
  }
})

.

서버에서는 [ http://localhost:3000/data ] 경로로 POST 요청이 들어오면 HTTP body 에 담겨있는 데이터를 추출해서 data.txt 파일에 저장합니다. 그리고 외부의 웹브라우저에서 http://localhost:3000/ 로 접속을 하면 data.txt  파일을 읽어와 데이터를 보여줍니다. data.txt 파일은 최근에 들어온 데이터 10개만 저장합니다.

저장된 데이터를 외부 브라우저 요청이 오면 보여질 수 있도록 화면을 만들어야겠죠. 아래 코드를 data.pug 파일로 만들어서 view 파일을 모아둔 [view_file] 폴더에 넣어주세요.

.

html
  head
    title= title
  body
    if data
        for val in data
            h1= val

.

데이터가 존재할 시 렌더링할 웹페이지를 구현한 pug 파일입니다. data 배열이 전달되면 이를 한 라인씩 화면에 출력합니다. 수정된 파일을 다운로드 받아 사용하고 싶으시면 아래 링크를 사용하세요.

여기까지 준비가 되었으면 Node.JS 서버를 시작해주세요.

  • node index.js

이로써 서버 쪽 준비는 완료되었습니다.

.

.

센서장치 구현

센서장치 역할을 하는 ESP32 모듈에서 서버로 POST 요청을 하기 위한 코드를 짜 보도록 하겠습니다. 아래 링크의 소스를 참고하세요.

이미 HTTP POST 에 대해서는 [4-2 센서장치와 HTTP Request] 에서 다뤘으니 여기서는 주요 소스코드만 보여드리겠습니다. 아래 코드에서 ssid, password, (라즈베리파이)서버 주소는 자신의 환경에 맞게 수정해서 업로드 하셔야 합니다.

.

const char* ssid     = "your_ssid";    // 와이파이 SSID
const char* password = "your_pw";      // 와이파이 비밀번호

void setup()
{
    ......
    WiFi.begin(ssid, password);

    // 와이파이망에 연결
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    ......
}

void loop()
{
    HTTPClient http;
 
    http.begin("http://www.server_addr.com:3000/data");        // 서버 주소
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");

    // 랜덤 번호 생성
    esp_random();
    String num = String(random(0, 256));
    Serial.println("num : " + num);

    // POST 후 결과 받음
    int httpResponseCode = http.POST("data="+num);
    if(httpResponseCode>0){
        String response = http.getString();
        Serial.println(httpResponseCode);
        Serial.println(response);
    }else{
        Serial.print("Error on sending POST: ");
        Serial.println(httpResponseCode);
    }
 
    http.end();  // 리소스 해제
    delay(10000);
}

.

10초에 한 번씩 HTTP POST 요청을 서버의 [ http://라즈베리파이주소:3000/data ] 경로로 보냅니다. 이 때 HTTP body 에는 전송할 데이터가 [ data=랜덤숫자 ] 형태로 들어가 있습니다. GET 방식에서 사용하는 URL 파라미터 표기방식을 HTTP body 에다가 넣은겁니다. 그래서 HTTP header 에 아래처럼 Mime type을 지정해 줬습니다.

http.addHeader("Content-Type", "application/x-www-form-urlencoded");

ESP32 에 업로드를 하고 공유기에 정상 접속되면 10초에 한번씩 서버로 데이터를 보냅니다.

이제 서버에 데이터가 기록될테니 서버로 접속해서 확인해보면 되겠네요. 같은 공유기에 연결된 PC나 폰 등에서 브라우저로 아래 주소에 접속하세요.

  • http://라즈베리파이주소:3000/

아래처럼 화면이 보이면 성공입니다. 간단하지만 센서장치-서버-웹 UI 가 연동된 서비스가 완성되었습니다!!

.

활용

사물인터넷 서비스 구축에서 서버가 차지하는 비중이 매우 큽니다. 물론 웨어러블 장치처럼 센서장치와 모바일 폰 등이 직접 연결되어 동작하는 경우도 있지만, 대부분의 경우 센서장치가 수집한 데이터들은 한데 모여서 가공, 배포되어야 하기 때문입니다.

센서장치와 서버 코드의 기본 틀을 완성했다면 이후 세세한 기능들을 추가하고 다듬는 작업은 훨씬 속도가 날 것입니다.

참고자료

주의!!! [사물 인터넷 네트워크와 서비스 구축 강좌] 시리즈 관련 문서들은 무단으로 내용의 일부 또는 전체를 게시하여서는 안됩니다. 계속 내용이 업데이트 되는 문서이며, 문서에 인용된 자료의 경우 원작자의 라이센스 문제가 있을 수 있습니다.

.

.

강좌 전체보기

.