// 생성자의 여러번 호출을 막기 위해 전역 변수로 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 타입의 차이도 모르고 코딩했기 때문에 ㅎㅎ;; 수준이 는 만큼 고민도 깊어지는군요. - 사실 아두이노나 임베디드 시스템이 어떻게 돌아가는진 잘 모릅니다 )
'프로그래밍 > C++' 카테고리의 다른 글
[C++] 연산자 오버로딩 (Operator Overloading) (0) | 2022.11.09 |
---|---|
[C++] 모든 인자값을 레퍼런스로 넘겨야 성능상 유리할까? (0) | 2022.10.27 |
[C/C++] 선언(Declaration)과 정의(Definition)의 차이 (0) | 2022.06.21 |
[C++] Natural Sort 사용하기 (0) | 2022.06.14 |
[C++] Visual Studio <std::filesystem> 사용하기 (2) | 2022.06.11 |