산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노 우노 명령줄 인터페이스 (CLI): 빠르고 안정적인 시리얼 제어

필요한 부품 및 소모품

부품명 수량
아두이노 우노 1

프로젝트 개요

명령줄 인터페이스가 필요한 이유

센서 값을 확인하거나 로봇에 "왼쪽 이동" 같은 명령을 내리고 싶을 때가 있습니다. 프로그램의 런타임 변수 값을 묻거나 디지털 포텐셔미터 값을 설정해야 할 때도 있죠. 이럴 때 필요한 게 바로 명령줄 인터페이스(CLI)입니다. 아두이노가 텍스트 명령에 응답하게 만드는 건 생각보다 쉽습니다.

아두이노 우노 명령줄 인터페이스 (CLI): 빠르고 안정적인 시리얼 제어

여러분이 바로 활용할 수 있는 단순하고 빠르며 메모리 효율이 매우 좋은 명령줄 인터페이스를 소개합니다. 몇 분 만에 복사해 여러분 코드에 붙여넣고 바로 실행할 수 있습니다. 명령은 이름과 여러 인수로 구성되며, 타이핑 중 백스페이스도 지원합니다.

빠른 개요

모든 스케치엔 loop() 함수가 있습니다. 여러분의 루프는 아래처럼 단순할 수 있습니다. CommandLine.h라는 별도 탭에 있는 두 루틴을 호출합니다. 이 두 파일을 설명해 드릴 테니 금방 적용하실 수 있을 겁니다.

아두이노 우노 명령줄 인터페이스 (CLI): 빠르고 안정적인 시리얼 제어

Loop 함수

동작 원리: loop()가 실행될 때마다 시리얼 포트에서 명령이 왔는지 확인하기 위해 getCommandLineFromSerialPort()를 호출합니다. 변수 CommandLine은 CommandLine.h에 선언되어 있지만, 스타일상 메인 Loop 탭으로 옮기는 게 좋습니다. 시리얼 포트로 전체 명령이 도착해 명령 버퍼에 복사되면:

char commandLine[COMMAND_BUFFER_LENGTH + 1];

loop()는 DoMyCommand()를 호출해 적절한 명령을 실행합니다.

이제 CommandLine.h 안을 살펴보죠. 모든 코드를 CommandLine.h에 넣은 이유는, 이 코드를 아두이노 IDE의 새 탭에 복사해 넣기만 하면 되기 때문입니다(탭 이름이 .h로 끝나게). 그런 다음 메인 파일에 포함시키면 됩니다:

#include "CommandLine.h"

이렇게 하면 모든 명령줄 코드를 한 탭에 넣으면서도 프로그램 다른 곳에서 루틴을 참조할 수 있습니다.

CommandLine.h 들여다보기

CommandLine.h 파일은 이 글 끝에 있습니다. 이 파일 내에서 수정해야 할 각 줄엔 //Modify here 주석이 달려 있습니다. 파일엔 add와 sub 두 샘플 명령이 포함되어 있고, 이들이 DoMyCommand 내에서 어떻게 호출되는지 보여줍니다.

여러분 대부분은 이 정도만 보면 됩니다. CommandLine.h를 훑어보세요. 더 자세히 알고 싶다면 계속 읽으세요.

더 깊이 보기

CommandLine.h 내에서 먼저 <string.h>를 포함합니다. string.h는 C 표준 라이브러리입니다. C 라이브러리를 처음 접한다면 "The C Programming Language"를 검색해보세요. C 언어의 바이블은 브라이언 커니핸과 데니스 리치가 썼고 계속 업데이트되어 왔습니다. 대부분 한 권씩 가지고 있지만 온라인에서도 무료로 볼 수 있습니다.

<string.h>에서 strtok() 루틴(문자열을 토큰으로 분할)만 사용합니다. 이 루틴은 특정 문자(두 번째 매개변수)로 구분된 토큰, 즉 단어를 읽습니다. 작동 방식은 이렇습니다:

<stdlib.h>도 포함하는데, 여기서 atoi()(ASCII를 정수로 변환)만 사용합니다. 컴파일러가 이 루틴만 포함하므로 라이브러리 전체가 들어가지 않지만, 이 라이브러리의 다른 루틴들도 유용하니 살펴보세요.

다음은 제가 쓴 작은 매크로 print2입니다:

#define print2(x,y) (Serial.print(x), Serial.println(y))

레이블과 값을 같이 출력하고 싶을 때 유용합니다. 이렇게 쓰세요:

print2("myVar = ", myVar);

myVar가 25라면 시리얼 창에 이렇게 출력됩니다:

myVar = 25

CommandLine.h엔 getCommandLineFromSerialPort()가 있어 시리얼 포트에서 명령줄을 조립합니다. 호출될 때마다 시리얼 포트에서 읽어 전역 입력 버퍼 CommandLine[]에 저장합니다. return 문자(명령 끝)를 만나면 true를 반환합니다.

완성된 버퍼는 DoMyCommand()로 넘어가 사용자가 요청한 기능이 뭔지 파악하고 호출합니다. 명령이 많으면 if-else 문이 꽤 길어질 수 있습니다.

명령이 정말 엄청나게 많다면 해싱 함수 도입이나, 명령을 한 글자로 만들어 switch-case로 처리하는 등 속도 향상 방법이 많습니다. 하지만 그런 경우는 드뭅니다. 단어 하나가 한 글자보다 기억하기 쉽고, 아두이노 칩 용량 한계상 명령이 그렇게 많을 수도 없습니다.

Add와 Sub 명령

각 명령 함수는 자기 인수를 직접 파싱합니다. 이게 단순하고 수정하기 쉽습니다. 대안으론 DoMyCommand에서 모든 인수를 바로 읽어 전역 문자열 배열 argv[]에 넣는 방식이 있습니다.

이 예제에선 add와 sub 두 명령을 정의했습니다. 둘 다 숫자 인수를 쓰지만, 텍스트 단어를 반환하는 readWord(readStringToken로 불러도 무방)도 포함했습니다. 이 readStringToken을 수정해 "this is a string" 같은 문자열도 허용하게 할 수 있습니다. 독자 연습 문제로 남겨둡니다.

가장 빠르게 실행하는 법

위와 같은 메인 루프 파일이 있다면, 아두이노 IDE 창 오른쪽 아래 삼각형을 눌러 새 탭을 만들고 CommandLine.h(아래 전체 코드)를 붙여넣으세요. 그러면 add와 sub 명령을 바로 입력해 테스트할 수 있습니다.

이제 여러분 몫입니다!

코드

아두이노용 심플 명령줄 인터프리터 (C/C++)

이 글이나 코드의 주석을 보고 아두이노 스케치에 심플 명령줄을 추가하세요.

이런 명령 추가 가능:

add 5, 10
subtract 10, 5

그 외 필요한 건 뭐든 가능

/*****************************************************************************
CommandLine 사용법:
1. 스케치 생성. 아래 샘플 setup과 메인 루프 코드 복사해 새 스케치에 붙여넣기
2. 새 탭 생성 (아두이노 에디터 맨 오른쪽 드롭다운 메뉴(작은 삼각형) 사용)
3. 탭 이름: CommandLine.h
4. 이 파일 내용 붙여넣기
테스트: 방금 만든 스케치를 아두이노에 평소처럼 업로드하고 시리얼 창 열기.
다음 명령 입력 후 엔터:
add 5, 10
subtract 10, 5
포함된 add, subtract 명령 보고 본인 명령 작성 시작!
******************************************************************************

내부 동작 원리
******************************************************************************

심플하고 명확한 명령줄 인터프리터
이 파일을 사용하면 시리얼 창에 아래 같은 명령 입력 가능:
add 23,599
blink 5
playSong Yesterday
아두이노에서 실행 중인 스케치가 이를 실행함.

구현 노트:
String 객체 대신 C 문자열 사용. 명령줄 인터프리터가 필요하면 공간도 부족할 가능성이 크고 String 객체는 공간 비효율적이기 때문.

1) 심플 프로토콜
명령은 단어와 숫자, 공백이나 콤마로 구분
첫 단어가 명령, 이후 각 단어가 인수
"\n"으로 각 명령 종료

2) C 라이브러리 루틴 strtok 사용:
명령은 공백이나 콤마로 구분된 단어. 특정 문자(공백, 콤마)로 구분된 단어를 토큰이라 함.
토큰을 하나씩 얻기 위해 C 라이브러리 루틴 strtok 사용 (C stdlib.h 일부, 아래 포함 방법 참고).
C 언어 라이브러리 <string.h>의 일부로 온라인에서 찾아볼 수 있음.
기본 동작:
1) 문자열(과 구분자, 즉 공백과 콤마) 넘기면 문자열에서 첫 토큰 반환
2) 이후 호출 시 NULL(문자열 ptr 대신) 넘기면 초기 문자열에서 멈춘 곳부터 계속.

기본 헬퍼 루틴 두 개 작성:
readNumber: strtok와 atoi(atoi: ASCII to int, 역시 C stdlib.h 일부) 사용해 정수 반환.
atoi는 int 반환하므로 uint8_t 같은 1바이트 정수 쓰면 lowByte()로 하위 바이트만 가져와야 함.
readWord: 텍스트 단어 포인터 반환

4) DoMyCommand:
각 명령별 if-then-else 리스트. 모든 명령이 한 글자면 switch-case로도 가능.
단어가 더 읽기 쉬움. 이 예제엔:
Add, Subtract, nullCommand

******************샘플 메인 루프 코드 ************************************
#include "CommandLine.h"
void setup() {
  Serial.begin(115200);
}
void loop() {
  bool received = getCommandLineFromSerialPort(CommandLine); //전역 CommandLine은 CommandLine.h에 정의
  if (received) DoMyCommand(CommandLine);
}
**********************************************************************************/

//탭 이름: CommandLine.h
#include <string.h>
#include <stdlib.h>

//디버깅용 매크로, 예: print2("myVar= ", myVar);
#define print2(x,y) (Serial.print(x), Serial.println(y))

#define CR '\r'
#define LF '\n'
#define BS '\b'
#define NULLCHAR '\0'
#define SPACE ' '
#define COMMAND_BUFFER_LENGTH 25 //들어오는 명령용 시리얼 버퍼 길이

char CommandLine[COMMAND_BUFFER_LENGTH + 1]; //시리얼에서 명령 읽어 저장할 버퍼. 종료 문자용 +1
const char *delimiters = ",\n"; //명령 구분자: 엔터, 공백, 콤마

/*************************************************************************************************************
명령 이름들
*************************************************************************************************************/
const char *addCommandToken = "add"; //Modify here
const char *subtractCommandToken = "sub"; //Modify here

/*************************************************************************************************************
getCommandLineFromSerialPort()
다음 명령 문자열 반환. 명령은 엔터로 구분.
백스페이스 문자 처리
모든 문자 소문자로 변환
*************************************************************************************************************/
bool getCommandLineFromSerialPort(char * commandLine) {
  static uint8_t charsRead = 0; //참고: COMAND_BUFFER_LENGTH는 255 미만이어야 함
  //풀 명령 입력까지 비동기 읽기
  while (Serial.available()) {
    char c = Serial.read();
    switch (c) {
      case CR: //버퍼에 풀 명령이 들어옴, 명령은 CR과/or LF로 종료
      case LF:
        commandLine[charsRead] = NULLCHAR; //명령 문자 배열 널 종료
        if (charsRead > 0) {
          charsRead = 0; //charsRead는 static이므로 리셋
          Serial.println(commandLine);
          return true;
        }
        break;
      case BS: //입력 중 백스페이스 처리: 마지막 문자에 공백 넣기
        if (charsRead > 0) {
          //commandLine과 charsRead 조정
          commandLine[--charsRead] = NULLCHAR;
          Serial << byte(BS) << byte(SPACE) << byte(BS); //어떻게 동작하는진 모름, 인터넷에서 찾음
        }
        break;
      default:
        //c = tolower(c);
        if (charsRead < COMMAND_BUFFER_LENGTH) {
          commandLine[charsRead++] = c;
        }
        commandLine[charsRead] = NULLCHAR; //혹시 모르니
        break;
    }
  }
  return false;
}

/**************************** 
readNumber: 명령줄에서 16비트(아두이노 우노 기준) 부호 있는 정수 반환
readWord: 명령줄에서 텍스트 단어 가져오기
*/
int readNumber() {
  char * numTextPtr = strtok(NULL, delimiters); //K&R string.h p.250
  return atoi(numTextPtr); //K&R string.h p.251
}

char * readWord() {
  char * word = strtok(NULL, delimiters); //K&R string.h p.250
  return word;
}

void nullCommand(char * ptrToCommandName) {
  print2("Command not found: ", ptrToCommandName); //위 매크로 참조
}

/****************************************************
여기에 명령 추가
*/
int addCommand() { //Modify here
  int firstOperand = readNumber();
  int secondOperand = readNumber();
  return firstOperand + secondOperand;
}

int subtractCommand() { //Modify here
  int firstOperand = readNumber();
  int secondOperand = readNumber();
  return firstOperand - secondOperand;
}

/****************************************************
DoMyCommand
*/
bool DoMyCommand(char * commandLine) {
  // print2("\nCommand: ", commandLine);
  int result;
  char * ptrToCommandName = strtok(commandLine, delimiters); 
  // print2("commandName= ", ptrToCommandName);
  if (strcmp(ptrToCommandName, addCommandToken) == 0) { //Modify here
    result = addCommand();
    print2("> The sum is = ", result);
  } else {
    if (strcmp(ptrToCommandName, subtractCommandToken) == 0) { //Modify here
      result = subtractCommand();
      //K&R string.h p.251
      print2("> The difference is = ", result);
    } else {
      nullCommand(ptrToCommandName);
    }
  }
  return true;
}

제조공정

  1. C# 주석 마스터하기: 종류, 모범 사례, XML 문서화
  2. C# 인터페이스 마스터하기: 정의, 구현, 실전 활용 사례
  3. Java 인터페이스 마스터: 개념, 구현, 모범 사례
  4. 긴 진입로를 위한 신뢰성 높은 무선 진입로 센서 – 라즈베리 파이 간편 연동
  5. 디지털 핀만으로 라즈베리 파이용 저가 적외선 근접 센서 만들기
  6. ANDY - 다목적 휴머노이드 로봇
  7. 심플 파이 로봇 – 라즈베리파이로 2WD 자율 로버 만들기
  8. C 언어 명령행 인자 마스터하기: 실전 가이드
  9. C# 인터페이스 마스터하기: 견고한 객체 지향 설계를 위한 계약
  10. 스틸 코일 슬리팅 라인 이해: 코일을 정밀 스트랩으로 가공하기