본문 바로가기

Software/Processing

Processing에서 시리얼통신으로 받은 데이터를 그래프로 표현하기

요즘 제가 MATLAB을 시작으로, Arduino, Python 등에서 시리얼 통신으로 데이터를 핸들링하는 아주 기초적인 글을 몇개 다뤘는데요. 이번에는 Processing이라고 하는 툴을 최근에 소개했었는데[바로가기] 그 Processing으에 관련된 글을 하나 더 다룰려고 합니다. 가지고 놀다보니 Processing도 많은 장점들이 있더라구요. 그것도 정말 쉽게 잘 쓸수 있구요. Processing에 관련된 기초적인 이야기는 공식홈페이지[바로가기]만 가도 엄청 많으니 패스하구요. 이번글의 목적은 Processing에서 시리얼 통신을 통해 들어온 데이터를 그래프로 표현하는 것입니다. 거기에 살짝 양념을 쳐서 GUI를 살짝 흉내도 내도록 하구 있구요.  아무튼 뭐 그렇습니다.^^.

monitorNTARSv1.zip

일단 Processing에서 기본 지원하는 배포본인데요. 어차피 소스코드까지 열람이 가능하니까 큰 상관은 없을듯 합니다.

받은 압축화일을 풀고나면 있는 저 응용프로그램을 실행하시면 됩니다.

바로 저기 있는 AVAILABLEPORTS 때문에 controlP5라는 라이브러리를 사용했습니다. 해당 라이브러리는 제가 이전글에서 언급[바로가기]했던 GUI 라이브러리입니다. 위에 표시된 부분은 Drop down list라고 하는데요. 살짝 코드를 한번 보죠.

import controlP5.*;

ControlP5 cp5;

DropdownList l;

void setup() {
  ... ... ...
  cp5 = new ControlP5(this);
  l = cp5.addDropdownList("AvailablePorts");
  ... ... ...
  comPortList(l);
}
void comPortList(DropdownList ddl) {
  availablePort = arsPort.list();
  
  ddl.setPosition(10, 70);
  ddl.setSize(80, 60);
  ddl.setItemHeight(15);
  ddl.setBarHeight(15);
  ddl.setColorBackground(ColorOfListBoxBackground);
  ddl.setColorActive(ColorOfListBoxActive);
  ddl.setColorForeground(ColorOfListBoxForeground);
  ddl.setValue(0);
  ddl.captionLabel().toUpperCase(true);
  ddl.captionLabel().set("AvailablePorts");
  ddl.captionLabel().setColor(ColorOfListBoxSetColor);  
  ddl.captionLabel().style().marginTop = 3;
  ddl.valueLabel().style().marginTop = 3; 

  for (int i=0; i<availablePort.length; i++) {
    l.addItem(availablePort[i], i);
  }
}

void controlEvent(ControlEvent theEvent) {
  
  if (theEvent.isGroup() && theEvent.name().equals("AvailablePorts")){
    int connectPortNo = (int)theEvent.group().getValue();
    connectPort = availablePort[connectPortNo];
  }
  if (!arsConnection) { 
    arsPort = new Serial(this, connectPort, 115200); 
    arsConnection = true;
  }
  
  delay(100);  
  arsPort.write("<CAH>");    
}

위 코드가 controlP5에서 드롭다운 리스트를 다룬 부분인데요. controlP5를 import하구요. cp5로 선언하고, 드롭다운 리스트 변수를 l로 두었습니다. 그리고 setup()함수에서 실제 다루고 있는 부분만 void setup()내에서 두었습니다. 본 함수를 이야기하기 전에 전 드롭다운리스트에서 하고싶었던건 그저 현재 가용한 시리얼 포트의 리스트를 나열하고 싶었던 거에요. 그래서 comPortList()함수를 확인해보면, 15번행에서 시리얼 통신 라이브러리에서 제공하는 list()함수를 사용해서 가용한 포트를 받고, 그걸 addItem으로 리스트에 제공하는 부분입니다. 그 후에 controlEvent()함수에서 이벤트를 핸들링하고 있구요. 최근 시리얼 통신을 다루던 제 글은 다 그렇지만 뭔가 실습할 대상이 있어야하니 전 그걸 NT-ARSv1[바로가기]으로 잡은거에요. 그래서 그에 맞는 설정을 43번행에서 하고 있는 겁니다. 그리고 드롭다운 리스트에서 포트가 선택되면 ARS한테 각도를 넘겨 달라는 <CAH> 명령을 전송하는거죠.

그렇게 동작되는 모습이 위 그림입니다. 이제 다시 전체 화면 구성을 보면

이런데요. 이런 화면을 구성하기 위해서 몇몇 기능들을 사용하고 있습니다. 먼저

void drawGraphPanel(){
  stroke(ColorOfListBoxForeground);
  strokeWeight(1.5);
  fill(color(255,255,255));
  rect(graphPanelXPos, graphPanelYPos, graphPanelXSize, graphPanelYSize); 
  
  fill(ColorOfBackground);
  rect(selectPanelXPos, selectPanelYPos, selectPanelXSize, selectPanelYSize);
  
  pauseBt.drawButton();
  stopBt.drawButton();
  rollAngBt.drawButton();
  pitchAngBt.drawButton();
  rollAngVelBt.drawButton();
  pitchAngVelBt.drawButton();

  strokeWeight(1);
  for (int i=(graphPanelYPos+panelGridYDivision); 
              i&lt;(graphPanelYPos+graphPanelYSize); i=i+panelGridYDivision) {
    for (int j=(graphPanelXPos+buttonXDiv);
              j&lt;(graphPanelXPos+graphPanelXSize); j=j+buttonXDiv) {
      line(j+panelGridHalfLength, i, j-panelGridHalfLength, i);
      line(j,i+panelGridHalfLength, j, i-panelGridHalfLength);   
    }
  }
    
  int tmpX = graphPanelXPos+graphPanelXSize/10;
  for (int i=0; i&lt;9; i++) {
    line(tmpX-panelGridHalfLength+i*graphPanelXSize/10, graphPanelYCenterPos, 
          tmpX+panelGridHalfLength+i*graphPanelXSize/10, graphPanelYCenterPos);
  }  
}

void drawText() {
  textFont(bigfontText);
  fill(100);  
  text("NT-ARSv1 Monitor",10,35);
  textFont(fontText);
  text("This Processing code is monitoring program for NT-ARSv1",280,51);
  text("NT-ARSv1 Monitoring Ver. 0.80 by PinkWink in NTRexLAB.",280,63);
  //text("by PinkWink in http://pinkwink.kr/",420,75);
  text("x division = 1 second",
    graphPanelXPos+graphPanelXSize/2-50,graphPanelYPos+graphPanelYSize+15);
  for (int i=0; i&lt;7; i++){
    text(graphYLabel[i], graphPanelXPos-20, graphPanelYPos+panelGridYDivision*i+5);
  }
  text("RollAngle", buttonXStartPos+11, buttonYPos+5);
  text("PitchAngle", buttonXStartPos+buttonXDiv+11, buttonYPos+5);
  text("RollAngVel", buttonXStartPos+buttonXDiv*2+11, buttonYPos+5);
  text("PitchAngVel", buttonXStartPos+buttonXDiv*3+11, buttonYPos+5);  
  text("Pause", graphPanelXPos+13, graphPanelYPos-20);
  text("STOP", graphPanelXPos+93, graphPanelYPos-20);    
}

인데요. drawText()는 화면에서 보이는 글자들을 모두 여기서 표현하고 있습니다. 뭐 원체 딱 봐도 될 정도로 쉽지요.^^. 그리고 drawGraphPanel()은 그래프를 그릴 곳의 경계선과 눈금등을 표시하고 있습니다. 여기서 하나가 뭐냐면 드롭다운리스트는 controlP5라는 라이브러리를 가져다 사용했지만, 나머지 6개의 버튼은 모두 직접 만들었습니다. 그게 10번부터 15번행에서 그걸 사용하고 있는건데요. 그렇게 만들어서 사용한 6개의 GUI 스러운 버튼은 클래스(class)로 선언했습니다. 그부분도 한번 보죠.

class ArsButtons {
  int posX, posY;
  String type;
  boolean buttonClicked;    
  color ColorOfClickedButton = color(150,150,150);    
  
  ArsButtons(int tmpX, int tmpY, boolean initValue, String tmpType) {
    buttonClicked = initValue;
    posX = tmpX;
    posY = tmpY;
    type = tmpType;
    
    drawButton();
  }
  
  boolean isButtonClicked(int X, int Y, int buttonArea) {
    boolean buttonClickedResult = false;
    
    if (type=="rec") {
      if(X&lt;(posX+buttonArea) &amp;&amp; X&gt;posX &amp;&amp; Y&lt;(posY+buttonArea) &amp;&amp; Y&gt;posY) {
        buttonClickedResult = true; }
    } else if (type=="ell") {      
      if(X&lt;(posX+buttonArea/2) &amp;&amp; X&gt;(posX-buttonArea/2) 
          &amp;&amp; Y&lt;(posY+buttonArea/2) &amp;&amp; Y&gt;(posY-buttonArea/2)) {
            buttonClickedResult = true; }
    }
    return buttonClickedResult;
  }
  
  void buttonClick() {
    if (buttonClicked) {
      buttonClicked = false;
    } else {
      buttonClicked = true;
    }
  }  
  
  void drawButton() {
    fill(ColorOfBackground);
    if (type=="rec") {
      rect(posX, posY, 10, 10);
      if (buttonClicked) { fill(ColorOfClickedButton); } 
      else { fill(ColorOfBackground); } 
      rect(posX+2, posY+2, 6, 6);
    } else if (type=="ell") {      
      ellipse(posX, posY, 10, 10);
      if (buttonClicked) { fill(ColorOfClickedButton); } 
      else { fill(ColorOfBackground); }
      ellipse(posX, posY, 6, 6);
    } 
  }
}

이렇습니다.^^ 전체적으로는 버튼은 사각형모양과 원형모양을 지원하구요. 버튼을 그릴 좌표와 크기를 입력하면됩니다만, 크기는 10으로 고정해야합니다. 비율을 생각안했거든요.ㅠㅠ. 그리고 내부에서는 버튼이 클릭되었는지 확인하느 isButtonClicked()함수와 클릭된 버튼의 속성을 변경하는 buttonClick()함수, 그리고 버튼을 그려주는 drawButton()함수를 가지고 있습니다. 이제 버튼을 마우스로 클릭하는 액션에 대한 함수가 필요하겠죠?^^

void mousePressed(){
  if(mouseButton==LEFT){
    if(rollAngBt.isButtonClicked(mouseX, mouseY, 10)) {
      rollAngBt.buttonClick(); }
    if(pitchAngBt.isButtonClicked(mouseX, mouseY, 10)) {
      pitchAngBt.buttonClick(); }
    if(rollAngVelBt.isButtonClicked(mouseX, mouseY, 10)) {
      rollAngVelBt.buttonClick(); }
    if(pitchAngVelBt.isButtonClicked(mouseX, mouseY, 10)) {
      pitchAngVelBt.buttonClick(); }
    if(pauseBt.isButtonClicked(mouseX, mouseY, 10)) {
      pauseBt.buttonClick(); }
    if(stopBt.isButtonClicked(mouseX, mouseY, 10)) {
      if(stopBt.buttonClicked) {
        stopBt.buttonClick();
        arsPort.write("&lt;CAH&gt;");
        delay(20);
      }else {
        stopBt.buttonClick(); 
        arsPort.write("&lt;CAE&gt;");
        delay(20);
      }
    }
  }
}

이겁니다. Processing은 기본적으로 마우스나 키보드 액션에 대한 함수를 준비해주고 있는데요. 마우스 좌클릭이 되었을때의 좌표가 버튼 좌표위에 있는지 확인해서 버튼이 클릭되었다고 알려주게 됩니다. 위에서 이야기한 클래스에서 관련함수가 있습니다. 이제 그래프를 그리는 부분을 보도록 하겠습니다.

위 그림처럼 그래프를 표현하는데요. 기본적으로 500개(5초)의 이전데이터를 계속 유지하면서 그걸 그리도록 되어 있습니다. 새로 데이터가 오면 배열의 끝에 위치시키고, 첫 데이터는 지우는 형태로 만들었어요. 그리고 버튼을 클릭함에 따라 4개의 데이터중에서 하나이상을 선택할 수 있습니다.

그래프를 그리는 부분을 한분 보도록 하겠습니다.

void drawGraph() {
  noFill();  
  if (rollAngBt.buttonClicked) {
    stroke(ColorOfRollAngLine);
    strokeWeight(2);
    beginShape();
      for (int xPos=0; xPos&lt;rollAng.length; xPos++) {
        float tmp = saturatingValue(rollAng[xPos], 90);        
        vertex(xPos+graphPanelXPos, graphPanelYCenterPos - tmp*resizingYSize);
      }
    endShape();
  }
  if (pitchAngBt.buttonClicked) {
    stroke(ColorOfPitchAngLine);
    strokeWeight(2);
    beginShape();
      for (int xPos=0; xPos&lt;pitchAng.length; xPos++) {
        float tmp = saturatingValue(pitchAng[xPos], 90); 
        vertex(xPos+graphPanelXPos, graphPanelYCenterPos - tmp*resizingYSize);
      }
    endShape();
  }
  if (rollAngVelBt.buttonClicked) {
    stroke(ColorOfRollAngVelLine);
    strokeWeight(1);
    beginShape();
      for (int xPos=0; xPos&lt;rollAngVel.length; xPos++) {
        float tmp = saturatingValue(rollAngVel[xPos], 500);
        vertex(xPos+graphPanelXPos, graphPanelYCenterPos - tmp*resizingAngVelYSize);
      }
    endShape();
  }
  if (pitchAngVelBt.buttonClicked) {
    stroke(ColorOfPitchAngVelLine);
    strokeWeight(1);
    beginShape();
      for (int xPos=0; xPos&lt;pitchAngVel.length; xPos++) {
        float tmp = saturatingValue(pitchAngVel[xPos], 500);
        vertex(xPos+graphPanelXPos, graphPanelYCenterPos - tmp*resizingAngVelYSize);
      }
    endShape();
  }
}

void serialEvent(Serial p) {
  String arsValues = "";
  
  arsValues = arsPort.readStringUntil(10);
  if (!pauseBt.buttonClicked &amp;&amp; (arsValues != null)) {
      calAngles(arsValues);
  }  
}

float saturatingValue(float target, float limitValue) {
  float resizingResult;
  float tmp = abs(target);
  if (tmp&gt;limitValue) {
    resizingResult = target/tmp*limitValue;
  } else {
    resizingResult = target;
  }        
  return resizingResult;
}

void calAngles(String s) {
  int lastPosInString = s.indexOf('&gt;');
  s = s.substring(1, lastPosInString);
  
  int arsResultArray[] = int(split(s, ','));
  
  rollAng = subset(rollAng, 1);
  pitchAng = subset(pitchAng, 1);
  rollAngVel = subset(rollAngVel, 1);
  pitchAngVel = subset(pitchAngVel, 1);
  
  rollAng = append(rollAng, float(arsResultArray[0])*0.001*180/PI);
  pitchAng = append(pitchAng, float(arsResultArray[1])*0.001*180/PI);
  rollAngVel = append(rollAngVel, float(arsResultArray[2])*0.001*180/PI);
  pitchAngVel = append(pitchAngVel, float(arsResultArray[3])*0.001*180/PI);   
}

일단 45번행의 시리얼 이벤트 함수는 시리얼통신으로 데이터가 들어오는 이벤트를 처리합니다. 거기서 calAngles()함수에 받은 데이터를 넘겨주죠. calAngles()함수는 사실 이번부터 시리얼통신에서 들어온 데이터를 핸들링하는 글에서 이야기를 했었습니다. 그 중에서 아두이노에서 핸들링한 글이 가장 가깝습니다.[바로가기] 아무튼 NT-ARSv1의 출력데이터는 항상 <>로 쌓여져 있으니 >의 위치를 알아내서 <와 >사이에 있는 값만 취하는 겁니다. 그걸 다시 split을 이용해서 ,를 기준으로 나눠서 저장하는거죠. 그 다음 문자열인 데이터를 숫자로 바꾸고 라디안을 일반적인 도로 바꾸는 작업들이 calAngles()함수입니다. 그리고 drawGraph()함수에서는 각각 500개의 크기를 가진 배열을 그리는 일을 합니다. 여기서 for문을 500까지 돌게하면 자주 에러가 납니다. 순간적으로 시리얼 이벤트의 문제나 내부적인 문제로 그런가본데요. 그래서 Processing 공식 홈페이지에서는 배열의 크기(length)를 반복문의 끝으로 잡아달라는 내용의 문서가 있습니다. 그래서 저도 for문은 7번처럼 이후에도 꾸몄습니다. 나머지 satratingValue()함수는 그래프를 그릴때 한계치를 순간적으로 넘는경우가 보기 싫어서 만들어 둔 겁니다. 기초부터 설명하는건 요즘같은 세상에는 안맞는듯합니다. 왜냐면 이제는 공식홈페이지 조차 개념을 잘 잡을 수 있도록 설명되어 있어서 말이죠.^^. 이번 작업을 하면서 꽤나 Processing에 대해 흥미를 느꼈습니다. 사실 이번 글은 Processing이긴하지만, 그래픽 핸들링의 간편함이라는 주제를 가지고 작업을 하나 더 해볼까.. 하는 생각이 들더라구요^^. 아 프로그램이 동작하는 동영상 한번 보시죠^^

반응형