1회성 명령 처리는 널리 알려진 방법으로 간단하게 코딩하면 되지만
데이터 로깅, 그래프 표현을 목적으로 데이터를 수신할 때는 통신 도중에 데이터가 사라지면 안 된다.
수신 버퍼는 제한적이고 수신 데이터는 연속으로 들어오는데,
정상 데이터의 일부만 가지고 처리하면 이어서 수신되는 데이터는 놓치게 된다.
(예를 들어 수신 버퍼가 100Byte이고 정상 데이터가 30Byte라면, 3번의 정상 데이터가 버퍼에 들어 오고, 4번째 데이터는 10B만 들어오게 된다. 그 다음 수신이벤트가 동작하고 버퍼가 비워지면 새 버퍼에 나머지 20B가 들어온다. 이미 이전의 10B를 잘 보관해 놓았고, 이후의 20B와 합치면 30Byte라는 정상 데이터가 된다)
즉, 버퍼관리만 잘 하면 끊김없이 줄줄이 수신할 수 있다.
소스
/*
시리얼 통신
simulz.kr
*/
#include <SoftwareSerial.h>
SoftwareSerial BT (3, 2);
class COM {
public:
//Serial
String RcvBuffer; // 수신 데이터
//*CDS:123;CDSNOW?;XYZ:-987$*CDS:123;CDSNOW?;XYZ:-987$*CDS:123;CDSNOW?;XYZ:-987$
void HandleInput(String InBuff) {
if (!InBuff.length()) return;
String SumBuffer = "";
SumBuffer = RcvBuffer + InBuff;
// 시작 종료 문자 포함 검사
while (SumBuffer.indexOf('*') != -1 && SumBuffer.indexOf('$') != -1) {
uint8_t startCMD = SumBuffer.indexOf('*');
uint8_t endCMD = SumBuffer.indexOf('$');
String strSplitCMD; // 분할한 명령어 임시 저장
uint8_t idx;
String tmpString = SumBuffer.substring(startCMD + 1, endCMD - startCMD); // 시작-끝 문자 사이 추출
tmpString.trim();
while (tmpString.length() > 0) {
idx = tmpString.indexOf(";");
if (idx == -1) {
strSplitCMD = tmpString;
tmpString = "";
} else {
strSplitCMD = tmpString.substring(0, idx); // 문자열 처음 명령 잘라서 배열에 저장
tmpString = tmpString.substring(idx + 1); // 나머지 문자열을 다시 임시 변수에 저장
tmpString.trim(); // 양쪽 빈 공기 빼기 꺼억~
}
strSplitCMD.trim();
CALC(strSplitCMD); // 명령어 처리 호출
} //while
RcvBuffer = "";
RcvBuffer = SumBuffer.substring(endCMD + 1, SumBuffer.length() - 0); // 2022.07 추가
SumBuffer = RcvBuffer; // 2022.07 추가
}
} // void HandleInput()
private:
// 명령어 처리
void CALC(String CMDVAL) {
Serial.println(CMDVAL);
String strCMD; // 명령어
String strVAL; // 값
uint8_t idx;
idx = CMDVAL.indexOf(":");
if (idx != -1) {
strCMD = CMDVAL.substring(0, idx); // 문자열 처음 명령 잘라서 배열에 저장
strVAL = CMDVAL.substring(idx + 1); // 나머지 문자열을 다시 임시 변수에 저장
int iVal = strVAL.toInt();
if (strCMD.equalsIgnoreCase("CDSMIN")) { // 대소문자 무시
// 값 확인 후 처리
if ((iVal >= 0) && (iVal <= 1023)) {
Serial.println("CDSMIN OK");
//MOTION.iMinCDS = iVal; //★
//EEPROMWrite(0, iVal); //★
}
} else if (strCMD.equalsIgnoreCase("RELAYTIME")) {
if ((iVal >= 0) && (iVal <= 1023)) {
Serial.println("RELAYTIME OK");
//MOTION.iOnTime = iVal; //★
//EEPROMWrite(1, iVal); //★
}
}
}
idx = CMDVAL.indexOf("?");
if (idx != -1) {
strCMD = CMDVAL.substring(0, idx); // 문자열 처음 명령 잘라서 배열에 저장
if (strCMD.equalsIgnoreCase("CDSNOW")) { // 대소문자 무시
Serial.println("CDSNOW? OK");
//Serial.println(MOTION.orgCdsValue); //★
} else if (strCMD.equalsIgnoreCase("CDSMIN")) {
Serial.println("CDSMIN? OK");
//Serial.println(MOTION.iMinCDS); //★
}
}
// Clear
strCMD = "";
strVAL = "";
} // void CALC()
};
COM COM1;
void setup() {
Serial.begin(9600);
Serial.println("Serial Rcv Test");
BT.begin(9600);
}
void loop() {
// 데이터 수신
COM1.HandleInput(Serial.readStringUntil('\r'));
}
예제 설명
문자열을 한번에 수신 받아서 처리
SoftwareSerial.h 선언 후 Bluetooth에 사용 가능
아스키 통신용
조건
- 맨앞에 시작문자 * 문자를 붙인다.
- 맨뒤에 종료문자 $ 문자를 붙인다.
- 명령그룹 구분은 ; 문자를 붙인다.
- 응답이 필요한 명령은 ? 문자를 붙인다.
- 일방 값 전달은 명령과 값 사이에 : 문자를 붙인다.
예) *CDS:123;CDSNOW?;XYZ:-987$*CDS:123;CDSNOW?;XYZ:-987$*CDS:123;CDSNOW?;XYZ:-987$
설명
COM 클래스를 만들고 COM1로 객체 생성
마지막 줄바꿈 문자 수신시 수신된 문자열 전체를 COM1.HandleInput으로 넘김
(\r\n은 PC 통신에서 0x0D 0x0A를 뒤에 붙인 것과 같음)
기존 RcvBuff 내용과 새로 전달 받은 InBuff를 SumBuffer에 저장
SumBuffer에 *, $ 포함되있으면 그 사이에 있는 문자열을 tmpString에 저장
명령그룹 구분 ; 문자가 있으면 첫번째 명령그룹을 strSplitCMD 변수에 저장하고 나머지는 tmpString에 다시 저장한다.
첫번째 명령 그룹을 CALC()로 전달하여 처리한다.
명령그룹 구분 ; 문자가 없을 때까지 반복한다.
CALC() 에서는 문자열을 : 문자로 나눠 앞부분은 명령어, 뒷부분은 값으로 처리한다.
추가
여러 명령어 그룹을 반복하여 분석하도록 수정
*, $ 문자를 붙인 이유는, STX, ETX 역할이며, 다중 통신 환경에서 사용하던 기법이었다.
(RS485/422, Ethernet TCP/IP)
RS232C와 같은 1:1 통신에서도 데이터 끊김으로 인한 오동작을 방지하기 위해 시작/종료 문자를 사용하는 것이 좋다.
실무에 사용할 때에는 콜백 함수 등을 사용하여 사용자 정의 코드를 클래스 내에서 작업할 필요가 없도록 해야 한다.