통신 실험실 #2: ESP32 Bluetooth 실습

기운찬곰·2025년 9월 25일

통신 실험실

목록 보기
2/3
post-thumbnail

실습 1. Bluetooth로 LED 제어하기

ESP32에서 Bluetooth Classic SPP로 문자열 메시지를 받아서 "LED ON"는 LED 켜기, "LED OFF"는 LED 끄기를 수행하는 코드를 작성해보도록 하겠습니다.

Bluetooth Classic SPP 기본 설정은 이전 내용을 참고하면 되겠고요. 여기서는 추가적으로 작성이 필요한 코드에 대해 설명하고 실습 결과를 적어볼까 합니다.

GPIO 초기화

LED 핀을 출력 모드로 설정합니다. 여기서는 5번 핀을 LED 핀으로 사용했습니다.

#include "driver/gpio.h"

#define LED_PIN GPIO_NUM_5

/** GPIO 초기화 */
static void gpio_init(void)
{
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
    // 초기 상태는 LED OFF (LOW)
    gpio_set_level(LED_PIN, 0);
    ESP_LOGI(TAG, "GPIO Init: LED on pin %d initialized", LED_PIN);
}

데이터 처리 로직

ESP_SPP_DATA_IND_EVT 이벤트 핸들러 내에 수신된 문자열을 확인해서 "LED ON" 메시지를 받으면 LED를 켜고, "LED OFF" 메시지를 받으면 LED를 끄는 로직을 추가합니다.

// 수신된 데이터를 NULL 종료된 문자열로 만듭니다. 
char *recv_data = (char *)malloc(param->data_ind.len + 1);

SPP 이벤트 구조체 param->data_ind 안에는 data(수신된 바이트 배열)과 len(길이)가 들어있습니다. 하지만 이 데이터는 NULL 종료(\0)가 없습니다. 그래서 길이(len)보다 1바이트 더 크게 동적 메모리 할당(malloc) 해서 마지막에 \0을 넣으려는 겁니다.

if (recv_data == NULL) {
    ESP_LOGE(TAG, "Failed to allocate memory for received data");
    break;
}

메모리 할당 실패했는지 체크합니다. ESP32는 RAM이 작으니까 항상 확인하는 게 좋습니다.

memcpy(recv_data, param->data_ind.data, param->data_ind.len);

수신된 원시 데이터를 recv_data로 복사합니다.

recv_data[param->data_ind.len] = '\0';
ESP_LOGI(TAG, "Received data: %s", recv_data);

마지막 바이트에 NULL 종료 문자 \0 를 넣어줍니다. 이제 recv_data는 안전하게 C 문자열처럼 쓸 수 있습니다.

문자열 비교

이제 recv_data 문자열 데이터와 "LED ON" 혹은 "LED OFF" 문자열이 일치하는지 확인해서 LED를 ON/OFF 시켜보도록 합니다.

if (strcmp(recv_data, "LED ON" == 0) {
    gpio_set_level(LED_PIN, 1);
} else if (strcmp(recv_data, "LED OFF") == 0) {
    gpio_set_level(LED_PIN, 0);
} else {
    ESP_LOGE(TAG, "Received invalid data: %s", recv_data);
    // 수신된 데이터를 에코하여 확인 가능하도록 함
    esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
}

free(recv_data);

만약 위와 같이 strcmp(const char *s1, const char *s2)를 사용하게 되면, 두 문자열 s1, s2를 끝까지(널 문자 \0 만날 때까지) 비교합니다.

recv_data 가 정말로 "LED ON" 이라면 동작하겠지만, Serial Bluetooth Terminal 앱에서 줄바꿈 문자(\r\n)가 포함될 수 있습니다. 정말로 그런지는 밑에서 확인해보도록 합시다.

결과 확인

Serial Bluetooth Terminal 앱에서 블루투스 연결을 한 다음에, "LED ON"이라고 보내봤습니다. 그랬더니 제대로 동작하지 않았습니다. 로그를 한번 살펴봅시다.

I (938834) BT_SPP_DEMO: ESP_SPP_SRV_OPEN_EVT
I (939964) BT_SPP_DEMO: SPP_DATA_IND_EVT len=8 handle=129
I (939964) BT_SPP_DEMO: Received data: LED ON

E (939964) BT_SPP_DEMO: Received invalid data: LED ON

데이터는 "LED ON" 이라고 되어있지만 strcmp 에서 제대로 비교하지 못하고 else 문으로 빠져버렸군요.

"LED ON" 이라고 보냈는데 recv_data len=8 이라는 것을 봤을때, L(1) E(2) D(3) ' '(4) O(5) N(6)는 6바이트 입니다. 하지만 터미널 앱이 전송할 때 개행 문자(\r(CR), \n(LF))를 자동으로 붙였기 때문에 총 8 바이트라는 것을 짐작해볼 수 있습니다.


해당 문제를 해결하기 위한 방법으로는 여러 가지가 있는데 하나씩 소개해보겠습니다.

1. strncmp 함수

strncmp(const char *s1, const char *s2, size_t n): 문자열을 비교할 때, 최대 n개의 문자까지만 비교합니다. 그 외의 동작은 strcmp와 동일합니다.

if (strncmp(recv_data, "LED ON" == 0, 6) {
    gpio_set_level(LED_PIN, 1);
} else if (strncmp(recv_data, "LED OFF") == 0, 7) {
    gpio_set_level(LED_PIN, 0);
} else {
    ESP_LOGE(TAG, "Received invalid data: %s", recv_data);
    // 수신된 데이터를 에코하여 확인 가능하도록 함
    esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
}

위와 같이 작성을 한다면, 문자열 앞 부분 정해진 길이까지만 비교하므로 제대로 동작할 것입니다.

2. strstr 함수

char *strstr(const char *haystack, const char *needle);: "어떤 문자열 안에 다른 문자열이 포함되어 있는지"를 찾을 때 사용합니다. haystack(건초더미)는 검색할 대상 문자열이고요, needle(바늘)은 찾고 싶은 문자열을 의미합니다.

haystack 안에서 needle을 처음 찾은 위치를 가리키는 포인터를 반환합니다. 찾지 못하면 NULL을 반환합니다. 간단하네요.

if (strstr(recv_buf, "LED ON")) {
    gpio_set_level(LED_GPIO, 1);
} else if (strstr(recv_data, "LED OFF")) {
    gpio_set_level(LED_PIN, 0);
} else {
    ESP_LOGE(TAG, "Received invalid data: %s", recv_data);
    // 수신된 데이터를 에코하여 확인 가능하도록 함
    esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
}

3. 개행 문자 제거하기

문자열 안에서 개행 문자가 나오면 바로 \0으로 바꿉니다. 이렇게 하면 strcmp를 사용할 수 있겠죠?

for (int i = 0; i < len; i++) {
    if (recv_buf[i] == '\r' || recv_buf[i] == '\n') {
        recv_buf[i] = '\0';   // 개행 문자 제거
        break;
    }
}

if (strcmp(recv_data, "LED ON" == 0) {
    gpio_set_level(LED_PIN, 1);
} else if (strcmp(recv_data, "LED OFF") == 0) {
    gpio_set_level(LED_PIN, 0);
} else {
    ESP_LOGE(TAG, "Received invalid data: %s", recv_data);
    // 수신된 데이터를 에코하여 확인 가능하도록 함
    esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
}

최종 결과. 간단하지만 블루투스로 직접 뭔가를 제어하는 걸 만들면 재미있는 거 같습니다.


실습 2. ???

profile
행동하는 바보가 돼라. 생각을 즉시 행동으로 옮기는 사람이 되어라

0개의 댓글