[Arduino/C++] 시리얼 모니터 데이터 공백구분으로 입력받기, 명령어 처리


// 생성자의 여러번 호출을 막기 위해 전역 변수로 String 변수 2개를 호출한다.
String input;
String args;

void setup()
{
  Serial.begin(9600);
}

void loop()
{
    
    //시리얼 데이터가 들어왔을때 (버퍼에 내용이 채워졌을때) 작업
    if(Serial.available() > 0)
    {
        input = Serial.readStringUntil('\n'); //엔터까지 입력받기
        Serial.println("INPUT : " + input);
        
        for(int i = 0; i < input.length(); i++ ) {
          char c = input[i];
          //공백이 아닌경우만 문자열에 결합시킨다. (담아준다.)
          if(c != ' ') {
            args.concat(c);
          }

          // 데이터를 읽는 조건은 공백이거나, 마지막 인덱스에 도달했을 때만임. (OR의 쇼트서킷 조건은 앞의 조건식이 true일때만이므로 크게 신경쓰지 않아도 됨.)
          if(c == ' ' || i == input.length() - 1){
            //실제로 arg를 읽어내는 부분
              Serial.println("arg : " + args);
              args = ""; //다 읽고 문자열 비우기
            }
        }
        input = "";
    }
    
}

별다른 설명 없이 코드 떠먹여주기(?) 시간입니다. 다음과 같은 코드를 사용하면 아두이노에서 터미널 모니터에 "Hello C World" 를 입력했을 때, Hello , C , World 로 분해해서 읽어낼 수 있습니다.

(종료 구분 문자는 엔터로, 엔터를 칠때까지만 버퍼에 담아서 읽고 , 그것을 이용해 처리합니다)

 

물론 본 예제는 단순히 입력받은 문자열을 공백으로 구분해서 단순히 출력하는 예제기 때문에 스페이스바로 분리된 String 문자열을 저장하고 싶다면 따로 내부적으로 구현하셔야 합니다.

 

당장 생각나는건 C++의 vector<string> 타입으로 저장하거나 하는건데 아두이노에서 기본적으로 vector을 제공하지 않는거 같고, 그렇다면 String Class를 배열로 동적할당 해야할 거 같습니다.

 

저는 목적이 "MV 10 10" 과 같이 시리얼 통신으로 PC에서 아두이노에 데이터(명령어)를 주면, 아두이노 레오나르도 쪽에서 10, 10 만큼 마우스를 이동 시키는것이 목적이였습니다.

 

공백을 기준으로 각 명령어 인자를 구분하여, ARG0 = MV , ARG1 = 10 , ARG2  = 10 이라고 정의하면 먼저 ARG0이 MV인지 확인하고, ARG1과 ARG2의 값을 파싱해야 합니다.

 

위 예제의 경우 단순히 스페이스바를 기준으로 계속 문자열을 파싱하기 때문에 첫 번째 인자를 미리 읽어들이고 그에 따라 대응하기 어렵습니다.

 

저와 같이 공백으로 구분해서 인자 형태로 명령어를 실행시키고 싶은 경우 아래와 같이 아두이노에서 제공하는 indexOf() 함수와 substring() 함수를 이용해서 처리하시면 됩니다.

 

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    // 입력 데이터 양식 ex) ARG0 / ARG0 ARG1 / ARG0 ARG1 ARG2 / ...

    //시리얼 데이터가 들어왔을때 (버퍼에 내용이만 채워졌을때) 작업
    if (Serial.available() > 0)
    {
        String input = Serial.readStringUntil('\n'); //엔터까지 입력받기
        Serial.println("INPUT : " + input);
        int space_idx = input.indexOf(" "); //문자의 첫번째 위치부터 다음 공백까지 읽어낸다

        //첫번째 인자의 경우, 공백을 만나지못하면 인자 딱 하나만 입력한 경우이므로 처음부터 끝까지 읽고,
        //공백을 만나면 그 공백 전까지 짤라서 읽어내면 됨. (인자가 2개 이상인 경우)
        String arg0 = space_idx == -1 ? input.substring(0, input.length()) : input.substring(0, space_idx);

        // 다음 공백 위치 찾는 코드
        int backup_space = space_idx;                  //공백 위치를 백업해둠
        space_idx = input.indexOf(" ", space_idx + 1); //다음 공백 위치를 찾는다.
        String arg1 = space_idx == -1 ? input.substring(backup_space + 1, input.length()) : input.substring(backup_space + 1, space_idx);

        Serial.println("arg0 : " + arg0);
        Serial.println("arg1 : " + arg1);

        // 현재 좌표에서 X축만큼 이동
        if (arg0 == "MX")
        {
            // 참고) Mouse.move는 좌표 순간이동이 아니라 현재 커서 위치에서 x축, y축만큼 이동하는 함수다.
            Mouse.move(arg1.toInt(), 0, 0);
        }
    }
}

MX 10 을 입력한 경우, ARG0은 MX로, ARG1은 10으로 파싱하는 예시입니다.

코드 만드느라 힘들었긴 한데 오픈소스 공개 문화에 따라 그냥 배포하겠습니다 ㅎㅎ;

급하게 만들어서 버그있는진 잘 몰?루

 

(여기 아래부턴 제 개인적인 고찰입니다. 코드만 필요하신 분들은 안 읽으셔도 됩니다.)

 


 

C++에서 기본 제공하는 String이나 Vector 는 내부적으로 동적 할당한 배열을 통해 데이터를 관리하고 있기 때문에 아두이노 같은 임베디드 환경에서는 Heap 동적 할당이 느리기 때문에 String 대신 C에서 제공하는 char * 을 이용하란 글을 봐서 솔직히 C의 char * 과 strtok 를 이용할지 , 아니면 그냥 C++의 편한 String 을 사용할지 사실 고민을 많이 하긴했습니다.

 

그런데 어떤 사람은 char * 을 쓰는것보다 버그가 줄어들어서 차라리 String이 훨씬 낫다는 의견도 있더라구요 ㅡㅡ;

 

뭐 아두이노 공식 문서에서도 String 을 이용한 기본 함수들을 레퍼런스로 잘 정리해뒀기도 했고, 그래봤자 처리하는 문자열도 몇 바이트 안되기 때문에 그냥 String 을 이용해서 공백 구분 입력을 구현해봤습니다.

(사실 옛날에 생각없이 코딩할 시절엔 char* 이나 String 타입의 차이도 모르고 코딩했기 때문에 ㅎㅎ;; 수준이 는 만큼 고민도 깊어지는군요. - 사실 아두이노나 임베디드 시스템이 어떻게 돌아가는진 잘 모릅니다 )

 

COMMENT WRITE