2021년 2월 16일 화요일

Arduino nano BLE, Node-red기반 IoT 데이터 MongoDB 저장 및 모니터링하기

이 글은 Arduino nano BLE, Node-red기반 IoT 데이터 MongoDB 저장 및 모니터링하는 방법을 간략히 공유한다. 이 글에서 개발되는 시스템은 nano의 모든 센서 데이터를 획득해, node-red로 전달하고, node-red에서 mongodb로 저장한다. 이 글을 따라하면 다음과 같은 결과를 얻을 수 있다.

아두이노 나노 보드 사용 방법은 아래 링크를 참고한다.
mongodb에 저장되면, 이를 통해, 데쉬보드, 오토데스크 포지 뷰어 등에서 모니터링할 수 있을 것이다.

아키텍처 디자인
아키텍처는 간단히 다음과 같이 디자인한다. Forge와 DT app은 이전 블로그에서 개발한 오토데스크 포지 기반 앱을 사용한다.

아두이노 nano sense 코딩
다음과 같이 코딩하고 업로드한다. 업로드가 제대로 되지 않으면, 코드 에러, 나노 보드 회로 연결이 제대로 된 것인지, 연결된 부품이 제대로 동작되는 것인지 확인한다. SSD1306 OLED 및 센서 라이브러리 설치 관련 내용은 이전 블로그 글을 참고한다.
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Arduino_LSM9DS1.h>
#include <Arduino_HTS221.h>
#include <Arduino_LPS22HB.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_RESET  -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(9600);
  Serial.println("Sensing");

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
  Serial.print("Gyroscope sample rate = ");
  Serial.print(IMU.gyroscopeSampleRate());
  Serial.println(" Hz");
  Serial.println();
  Serial.println("Gyroscope in degrees/second");
  Serial.println("X\tY\tZ");  

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(1000); 

  // Clear the buffer
  display.clearDisplay();
  display.setTextSize(1); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  // display.setFont(ArialMT_Plain_10);

  if (!HTS.begin()) {
    Serial.println("Failed to initialize humidity temperature sensor!");
    while (1);
  }
  
  if (!BARO.begin()) {
    Serial.println("Failed to initialize pressure sensor!");
    while (1);
  }
  testdrawline();      // Draw many lines
}

void loop() {
  sensingNano();    
}

void testdrawline() {
  int16_t i;
  display.clearDisplay(); // Clear display buffer

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
}

void sensingNano(void) {
  display.clearDisplay();

  float temperature = HTS.readTemperature();
  float humidity    = HTS.readHumidity();
  float pressure = BARO.readPressure();

  // print each of the sensor values
  Serial.print(temperature);
  Serial.print(", ");
  Serial.print(humidity);
  Serial.print(", ");
  Serial.print(pressure);
  
  display.setCursor(0, 24);
  String data = String("THP: ") + String(temperature, 1) + ", " + String(humidity, 1) + ", " + String(pressure, 1);
  display.println(data);


  float x, y, z;
  if (IMU.gyroscopeAvailable()) {
    IMU.readGyroscope(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 0);
    String data = String("G: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);
  }

  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 8);
    String data = String("A: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);    
  }

  if (IMU.magneticFieldAvailable()) {
    IMU.readMagneticField(x, y, z);

    Serial.print(", ");
    Serial.print(x);
    Serial.print(", ");
    Serial.print(y);
    Serial.print(", ");
    Serial.print(z);

    display.setCursor(0, 16);
    String data = String("M: ") + String(x, 1) + ", " + String(y, 1) + ", " + String(z, 1);
    display.println(data);       
  }

  Serial.println(" ");

  display.display();      // Show initial text
  delay(500);
}

시리얼 모니터에서 센서 데이터들이 제대로 출력되면 성공한 것이다.

mongo DB 설정
mongodb를 설치한다. 몽고디비 데이터 저장 폴더를 만들고, 경로를 다음과 같이 지정한다.
mkdir c:\data
cd data
mkdir db
export MONGO_PATH=/usr/local/mongodb
export PATH=$PATH:$MONGO_PATH/bin

node-red 를 실행한다. 노드래드의 팔레트 관리자 메뉴에서 node-red-node-mongodb2 패키지를 설치한다. 
npm install node-red-node-mongodb

몽고디비를 실행한다.
mongod

몽고디비 실행후 MongoDB compass 프로그램에서 sensor 데이터베이스와 data 컬랙션을 생성하거나, 다음과 같이 몽고디비 콘솔창을 실행해 명령을 입력한다(mongo).
use sensor
db.data.insert({sensor: "temp", date: "2021-2.19", value: "20.9"})

node-red와 센서 연결
node-red를 다음과 같이 디자인한다. 시리얼 노드를 더블클릭해, 연결된 아두이노 나노 보드의 COM 포트를 입력한다.
serial 노드를 생성해, function 노드와 연결한다. 그리고, function 노드를 더블클릭해 다음과 같이 코딩한다. function 출력은 12개로 설정한다. 시리얼에서 문자열로 가져온 메시지 데이터는 콤마 형식으로 구분되어 있다. 이를 분리해 배열로 만들고, 실수값으로 변환해 메시지 리스트를 만든다. 이를 리턴한다.

var output = msg.payload.split(",");

var msgList = [];
output.forEach(function(v, index)
{
    var msgIoT = {payload: parseFloat(v)};
    msgList.push(msgIoT);
});

return msgList;


node-red와 MongoDB 연결
디버깅을 위해 msg.payload, text 노드를 생성해 연결한다. mongodb out 노드를 생성해, 함수와 연결한다.

다음과 같이 몽고디비에 저장할 데이터베이스, 유저, 암호 등 연결 정보를 설정한다.


배포를 하면, 다음과 같이 몽고디비에 저장되는 센서 데이터 값을 확인할 수 있다.
show dbs
use sensor
db.data.find()

아울러, 노드레드 데쉬보드에서 다음과 같이 센서값들을 확인할 수 있다.

센서 값에 타임스탬프를 추가하기 위해, 함수를 하나 더 추가하고, 처음 만든 함수의 첫번째 출력과 노드를 연결한다. 함수의 출력은 기존에 만든 몽고디비 노드 입력과 연결한다. 

그리고, 새로 만든 함수는 다음과 같이 코딩한다.
var d = new Date();
var date = d.getFullYear() + '-' + d.getMonth() + '-' 
        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()
        + ':' + d.getSeconds();

var newMsg = {
                payload: {
                    'sensor': 'temp',
                    'date' : date,                    
                    'value': msg.payload
                }
            };

return newMsg;

노드레드 프로그램 배포 전에 다음과 같이 mongodb collection data를 모두 삭제한다. 
db.data.remove({})

이제 배포하면, 다음과 같이 몽고디비에 저장된 결과를 확인할 수 있다.

몽고디비에 센서 값들을 이런 방식으로 저장할 수 있다. 참고로, 지금까지 개발된 노드레드 프로그램 코드는 다음과 같다. 참고로, 노드레드 import 메뉴를 이용해, 코드를 로딩할 수 있다.
[{"id":"4e59f461.a56f0c","type":"tab","label":"플로우 1","disabled":false,"info":""},{"id":"63af438.5058bbc","type":"serial in","z":"4e59f461.a56f0c","name":"","serial":"af5253fc.09b97","x":150,"y":160,"wires":[["c62c93a8.af08d","d243c665.c31f28","71fe46c0.dfaa18"]]},{"id":"d243c665.c31f28","type":"debug","z":"4e59f461.a56f0c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":330,"y":60,"wires":[]},{"id":"19e8218a.5b6f8e","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":2,"width":0,"height":0,"name":"s2","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":140,"wires":[]},{"id":"c62c93a8.af08d","type":"function","z":"4e59f461.a56f0c","name":"","func":"var output = msg.payload.split(\",\");\n\nvar msgList = [];\noutput.forEach(function(v, index)\n{\n    var msgIoT = {payload: parseFloat(v)};\n    msgList.push(msgIoT);\n});\n\nreturn msgList;","outputs":12,"noerr":0,"initialize":"","finalize":"","x":350,"y":300,"wires":[["1e8b1389.ebbb5c","eb4101fb.ecc5a"],["19e8218a.5b6f8e","42b4327e.a5e49c"],["fedf546e.b92f38","4156ebd0.2fada4"],["7f000ff8.72b0c"],["7b969ff5.17729"],["723708ed.d0fe68"],["cc30198b.56e498"],["a62c4830.afe078"],["22e28692.dae18a"],["758c4be3.100204"],["3804bcdf.9caf44"],["29d226c9.8c1c4a"]]},{"id":"1e8b1389.ebbb5c","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":1,"width":0,"height":0,"name":"s1","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":100,"wires":[]},{"id":"fedf546e.b92f38","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":3,"width":0,"height":0,"name":"s3","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":180,"wires":[]},{"id":"7f000ff8.72b0c","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":4,"width":0,"height":0,"name":"s4","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":220,"wires":[]},{"id":"7b969ff5.17729","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":5,"width":0,"height":0,"name":"s5","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":260,"wires":[]},{"id":"723708ed.d0fe68","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":6,"width":0,"height":0,"name":"s6","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":300,"wires":[]},{"id":"cc30198b.56e498","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":7,"width":0,"height":0,"name":"s7","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":340,"wires":[]},{"id":"a62c4830.afe078","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":8,"width":0,"height":0,"name":"s8","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":380,"wires":[]},{"id":"22e28692.dae18a","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":9,"width":0,"height":0,"name":"s9","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":420,"wires":[]},{"id":"758c4be3.100204","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":10,"width":0,"height":0,"name":"s10","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":460,"wires":[]},{"id":"3804bcdf.9caf44","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":11,"width":0,"height":0,"name":"s11","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":500,"wires":[]},{"id":"29d226c9.8c1c4a","type":"ui_text","z":"4e59f461.a56f0c","group":"2b0b67e5.8c6878","order":12,"width":0,"height":0,"name":"s12","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":590,"y":540,"wires":[]},{"id":"71fe46c0.dfaa18","type":"ui_text","z":"4e59f461.a56f0c","group":"c65726f5.12d638","order":1,"width":0,"height":0,"name":"","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":310,"y":100,"wires":[]},{"id":"eb4101fb.ecc5a","type":"function","z":"4e59f461.a56f0c","name":"","func":"var d = new Date();\nvar date = d.getFullYear() + '-' + d.getMonth() + '-' \n        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()\n        + ':' + d.getSeconds();\n\nvar newMsg = {\n                payload: {\n                    'sensor': 'temp',\n                    'date' : date,                    \n                    'value': msg.payload\n                }\n            };\n\nreturn newMsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":540,"y":40,"wires":[["5325fecc.91cdc","40d4c998.b69328"]]},{"id":"5325fecc.91cdc","type":"mongodb out","z":"4e59f461.a56f0c","mongodb":"129644e9.2c45fb","name":"","collection":"data","payonly":false,"upsert":false,"multi":false,"operation":"insert","x":750,"y":40,"wires":[]},{"id":"40d4c998.b69328","type":"ui_text","z":"4e59f461.a56f0c","group":"c65726f5.12d638","order":1,"width":0,"height":0,"name":"db","label":"text","format":"{{msg.payload}}","layout":"row-spread","x":730,"y":100,"wires":[]},{"id":"4156ebd0.2fada4","type":"ui_chart","z":"4e59f461.a56f0c","name":"","group":"c65726f5.12d638","order":4,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":750,"y":200,"wires":[[]]},{"id":"42b4327e.a5e49c","type":"ui_gauge","z":"4e59f461.a56f0c","name":"","group":"c65726f5.12d638","order":3,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":10,"colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":740,"y":160,"wires":[]},{"id":"af5253fc.09b97","type":"serial-port","serialport":"COM5","serialbaud":"9600","databits":"8","parity":"none","stopbits":"1","waitfor":"","dtr":"none","rts":"none","cts":"none","dsr":"none","newline":"\\n","bin":"false","out":"char","addchar":"","responsetimeout":"10000"},{"id":"2b0b67e5.8c6878","type":"ui_group","name":"Group 1","tab":"ad2f4438.b053a8","order":1,"disp":true,"width":"6","collapse":false},{"id":"c65726f5.12d638","type":"ui_group","name":"Group 2","tab":"ad2f4438.b053a8","order":2,"disp":true,"width":6},{"id":"129644e9.2c45fb","type":"mongodb","hostname":"127.0.0.1","topology":"direct","connectOptions":"","port":"27017","db":"sensor","name":""},{"id":"ad2f4438.b053a8","type":"ui_tab","name":"Tab 1","icon":"dashboard","order":1}]

이제 저장된 데이터 값을 데쉬보드나 오토데스크 포지에 연결해 보여줄 수 있다.

부록: 전류 소모에 따른 센서 오동작 문제
나노에 많은 전류 소모 센서나 장치를 부착하면, 충분하지 못한 전력때문에 센서 데이터는 부적확한 데이터를 출력하는 경우가 있다. 이에 주의한다.

댓글 4개:

  1. 안녕하세요 저는 서울시립대학교 건축공학과에 재학중인 학생입니다. 졸업논문 주제와 연관이 있어 글을 보다가 올려주신 과정대로 따라서 아두이노 IDE에 코드를 작성해 봤는데 궁금한 점이 생겨 댓글 남깁니다. 코드를 모두 입력하고 컴파일을 했는데 시리얼 모니터에 들어가니 아무 데이터가 입력되지 않았습니다. 코드를 입력하기만 하고 시리얼 모니터에 들어가면 바로 데이터가 나타나는 것인지, 아니면 따로 센서에 무언가를 해야 데이터가 나타나는 것인지 궁금합니다. 디지털트윈을 주제로 논문을 쓰게 되었는데, 처음 접하는 주제라서 글을 보고 많이 배우고 있습니다. 좋은 정보 올려주셔서 감사합니다. 시간이 있으시면 답변 부탁드립니다.

    답글삭제
    답글
    1. 시리얼 모니터에서 COM 포트, 9600 BPS 맞춰 보세요.



      삭제
  2. 안녕하세요 AI 관심이 있던 중 이글을 찾게 되었네요 좋은 내용 감사합니다.
    한가지 궁금한 사항이 혹시 BLE 33 sense 보드에 있는 블루투스로도 데이터 전송이 가능할까요?

    답글삭제