Bluetooth는 근거리 무선 통신 기술 중 하나로, 주로 짧은 거리에서 기기 간 데이터를 주고받는 데 사용됩니다.

주요 특징은 다음과 같습니다.
Bluetooth는 사물 인터넷 애플리케이션(IoT)과 상호 연결된 장치의 Personal Area Network(PAN)를 만드는 데 사용됩니다.
블루투스의 역사는 1990년대 초반, 스웨덴의 통신 장비 회사인 에릭슨(Ericsson)의 사내 프로젝트에서 시작되었습니다.
1994년: 에릭슨의 연구 시작
에릭슨은 유선 케이블 없이도 휴대전화와 헤드셋 같은 주변기기들을 연결할 수 있는 저전력 무선 기술을 개발하기 시작했습니다. 이들은 2.4GHz 주파수 대역을 활용하여 데이터와 음성을 동시에 전송하는 기술을 연구했습니다.
1998년: 블루투스 SIG 결성
에릭슨은 이 기술의 잠재력을 인식하고, 이를 산업 표준으로 확립하기 위해 여러 기업들과 협력하기 시작했습니다. 1998년 5월, 에릭슨을 포함해 IBM, 인텔, 노키아, 도시바 등 5개의 주요 IT 기업이 모여 블루투스 특별 이익 단체(Bluetooth Special Interest Group, SIG)를 설립했습니다. 이 단체는 기술 규격을 표준화하고 상용화를 추진하는 역할을 맡았습니다.
1999년: 블루투스 1.0 발표
블루투스 SIG는 1999년 7월, 최초의 공식 사양인 블루투스 1.0을 발표했습니다. 같은 해, 첫 번째 블루투스 기기인 핸즈프리 모바일 헤드셋이 출시되어 '컴덱스(COMDEX)'에서 '최고의 기술상'을 수상하며 주목을 받기 시작했습니다. 하지만 초기 버전은 제조사 간의 호환성 문제나 보안 취약점 등 여러 단점을 가지고 있었습니다.
참고. 이름의 탄생 비화
'블루투스'라는 이름은 10세기경 스칸디나비아를 통일한 덴마크의 왕 '하랄 블라톤 고름손(Harald Bluetooth Gormsson)' 에서 유래했습니다. 여러 부족을 하나로 통일했던 그의 업적처럼, 서로 다른 통신 기기들을 하나의 무선 통신 기술로 연결한다는 의미를 담고 있습니다.
Bluetooth는 크게 Bluetooth Classic과 Bluetooth Low Energy(BLE)로 나눌 수 있습니다. 이 두 기술은 같은 2.4GHz ISM 주파수 대역을 사용하지만, 설계 목표와 사용 용도가 명확하게 다릅니다.
Bluetooth Classic
블루투스 클래식은 우리가 흔히 아는 기존 블루투스 기술입니다. Bluetooth BR/EDR 이라고도 합니다. 이는 Basic Rate/Enhanced Data Rate 를 위한 것입니다.
주로, 무선 헤드폰, 스피커, 자동차 핸즈프리 시스템, 파일 전송 등에 사용됩니다.
Bluetooth Low Energy (BLE)
BLE는 블루투스 4.0 버전에서 처음 도입된 기술로, 저전력 소비에 초점을 맞춰 개발되었습니다. Bluetooth Smart 이라고도 합니다. 블루투스 클래식과 호환되지 않습니다.
주로, 스마트워치, 피트니스 트래커, 스마트 홈 센서, 무선 키보드, 비콘(Beacon) 등 IoT 기기에서 사용됩니다.
참고: https://en.wikipedia.org/wiki/Bluetooth#Specifications_and_features

Bluetooth 1.0 (1999): 블루투스 기술의 첫 상용화 버전입니다. 기본적인 무선 데이터 전송만 가능하고 오디오 스트리밍은 지원하지 않는 초기 버전입니다. 낮은 전송 속도와 불안정한 연결, 그리고 장치 간의 호환성 문제가 많았습니다. 고정된 PIN을 사용해 보안이 취약했습니다.
Bluetooth 1.1 (2001): 헤드셋을 위한 오디오 기능을 도입했습니다. Bluetooth Headset Profile(HSP), Hands-Free Profile(HFP)을 추가하여 음성 통화가 가능해집니다.
Bluetooth 1.2 (2003): 스테레오 오디오 지원과 Advanced Audio Distribution Profile(A2DP)을 도입하여 음악 스트리밍이 가능해집니다.
Bluetooth 2.0 (2004): 이 버전부터 EDR(Enhanced Data Rate) 기술이 도입되었습니다. 전송 속도가 최대 3Mbps까지 향상되었습니다. Audio/Video Remote Control Profile(AVRCP)가 추가되었습니다. 전력 소모도 줄었고, Secure Simple Pairing(SSP)의 기반이 마련되어 페어링 과정이 더 쉬워졌습니다.
Bluetooth 2.1 (2007): Secure Simple Pairing(SSP) 이 공식적으로 도입된 버전입니다. PIN 입력 없이 페어링이 가능해져 사용자 편의성이 크게 개선되었습니다. 보안도 대폭 강화되어 중간자 공격(Man-in-the-Middle)을 방지할 수 있게 되었습니다.
Bluetooth 3.0 (2009): HS(High Speed) 기능이 추가되었습니다. Wi-Fi 기술을 활용하여 고속 데이터 전송을 지원합니다. 블루투스로 연결을 수립한 후, 실제 데이터 전송은 최대 24Mbps의 Wi-Fi를 통해 이루어졌습니다. 하지만 이로 인해 전력 소모가 컸고, 상용화된 기기가 많지는 않았습니다.
Bluetooth 4.0 (2010): 블루투스 역사에서 가장 중요한 전환점인 BLE(Bluetooth Low Energy) 가 도입되었습니다. 낮은 전력 소모가 가장 큰 특징입니다. 짧고 간헐적인 통신에 최적화되어, 동전 크기 배터리로 수년 간 작동하는 IoT 기기(센서, 스마트워치)에 혁명을 가져왔습니다. 클래식과 BLE를 모두 지원하는 듀얼 모드가 가능해졌습니다.
Bluetooth 5 (2016): BLE 기술이 대폭 강화된 버전입니다. BLE의 데이터 전송 속도가 2배 (2Mbps)로 향상되었고, 통신 거리도 4배까지 확장되었습니다. 오디오 품질이 개선되었습니다. 또한, 브로드캐스트 데이터 용량이 8배 늘어나, 더 풍부한 광고 패킷을 전송할 수 있게 되어 비콘과 같은 위치 기반 서비스에 유리해졌습니다.
Bluetooth 5.2 (2020): 저전력 오디오(LE Audio) 와 LC3 코텍이 도입되었습니다. BLE를 통해 고음질 오디오를 전송할 수 있게 되었습니다. 이는 기존 클래식 블루투스 오디오를 대체할 수 있는 기술로, 저전력으로 무선 이어폰을 사용할 수 있게 해줍니다. 보청기 지원이 강화되었습니다.
Bluetooth 5.4 (2023): 새로운 저전력 논리 전송 방식과 Periodic Advertising with Responses(PAwR) 기능이 추가되었습니다.
Bluetooth Classic과 BLE를 세부적인 특성 별로 차이를 알아보도록 하겠습니다.
블루투스 클래식과 BLE는 모두 2.4GHz ISM(Industrial, Scientific, Medical) 대역의 동일한 주파수를 사용하고, 주파수 호핑(Frequency Hopping)이라는 기술을 사용해 여러 채널을 빠르게 이동하며 통신합니다.
이는 Wi-Fi와 같은 다른 2.4GHz 대역 무선 기기와의 전파 간섭을 최소화하고 안정적인 연결을 유지하기 위함입니다. 그러나 이 호핑 기술의 방식에 차이가 있습니다.

블루투스 클래식은 79개의 채널(2402MHz ~ 2480MHz, 1MHz 간격)을 사용합니다. 마스터 기기와 슬레이브 기기는 정해진 순서에 따라 초당 1,600회까지 채널을 이동하며 데이터를 주고받습니다. 이처럼 많은 채널을 사용하고 빠르게 호핑하기 때문에 높은 데이터 전송 속도를 확보할 수 있습니다.

BLE는 40개의 채널(2402MHz ~ 2480MHz, 2MHz 간격)을 사용합니다. 이 중 3개의 채널은 광고 채널(Advertising Channel)로 사용되며, 나머지 37개의 채널은 데이터 채널(Data Channel)로 사용됩니다.
BLE는 필요할 때만 짧은 시간 동안 데이터를 전송하고 다시 수면 모드로 돌아가기 때문에, 클래식에 비해 데이터 채널의 수가 적어도 효율적으로 작동합니다. 이 방식을 통해 극도로 낮은 전력 소비를 달성할 수 있습니다.

🎯 BLE에서 광고 채널(Advertising Channel) 은 기기가 자신의 존재를 외부에 알리고, 다른 기기들이 이 정보를 탐색(scanning)할 수 있도록 하는 역할을 합니다. BLE 통신의 첫 단계이자 가장 중요한 부분이죠.
왜 광고 채널이 필요한가? - BLE 기기는 전력을 절약하기 위해 평소에는 '수면 모드'에 있습니다. 그러다 잠에서 깨어나 다른 기기와 연결하거나 데이터를 보낼 필요가 생기면, 광고 채널을 통해 자신의 정보를 '방송'합니다.
결론적으로, 광고 채널은 BLE가 낮은 전력으로 기기 발견, 초기 연결, 그리고 간단한 데이터 교환을 수행할 수 있도록 해주는 '게시판'과 같은 역할을 합니다.
🎯 BLE는 적응형 주파수 호핑(Adaptive Frequency Hopping, AFH) 기술을 통해 간섭을 효과적으로 회피합니다. 블루투스 클래식은 단순히 79개의 채널을 순서대로 빠르게 이동하며 간섭을 회피합니다. 반면, BLE의 AFH는 주변 환경을 능동적으로 감지하고 피하는 방식입니다.
BLE 기기는 통신 중 특정 채널에서 패킷 손실률(Packet Error Rate, PER)이나 수신 신호 세기(Received Signal Strength Indicator, RSSI)를 지속적으로 측정합니다. 특정 채널에서 간섭으로 인해 데이터 오류가 자주 발생하거나 신호가 약해지면, 해당 채널을 '불량 채널'로 등록합니다.
마스터 기기(예: 스마트폰)는 불량 채널 정보를 슬레이브 기기(예: 센서)에게 전달하고, 두 기기는 앞으로의 통신에서 해당 채널을 사용하지 않기로 약속합니다.
블루투스 클래식과 BLE는 장치를 발견하는 방식이 근본적으로 다릅니다. 이 차이는 각 기술의 설계 목표인 고속 데이터 전송과 초저전력에 따라 결정됩니다.
클래식 블루투스의 발견 과정은 탐색(Inquiry)과 페이지(Paging) 라는 두 단계로 이루어집니다. 이 과정은 전력을 많이 소모하며, BLE에 비해 시간이 오래 걸립니다.
이러한 방식은 지속적인 통신을 위해 만들어졌기 때문에, 발견 과정 자체가 비교적 복잡하고 전력 소모가 큽니다.
BLE의 발견 과정은 광고(Advertising)와 스캔(Scanning) 이라는 개념을 사용합니다. 이는 매우 짧고 간헐적인 통신을 목표로 합니다.
BLE는 수시로 광고 패킷을 보내고, 필요한 경우에만 짧게 연결을 맺고 끊는 방식을 사용합니다. 이는 배터리 수명을 극대화하는 데 매우 효과적입니다.
클래식 블루투스의 페어링은 기기 간의 지속적인 관계를 맺는 데 중점을 둡니다. 페어링 과정에서 두 기기는 공통의 링크 키(Link Key) 를 생성하고 저장합니다. 이 키는 이후 연결을 다시 시도할 때 사용되어, 매번 복잡한 보안 절차를 거치지 않아도 됩니다.
초기 페어링(Initial Pairing):
서비스 탐색(Service Discovery): 기기들은 서로의 지원 프로필(A2DP, HFP 등)을 교환합니다.
보안 절차: 사용자는 PIN 코드 또는 Secure Simple Pairing (SSP) 등을 통해 인증을 진행합니다.
링크 키 생성: 인증이 성공하면, 두 기기는 비대칭 알고리즘을 사용하여 유일한 링크 키를 생성하고, 이를 저장합니다.
재연결(Reconnection): 재연결 시에는 저장된 링크 키를 사용하여 신속하게 인증을 완료합니다. 이 키는 두 기기 간에 지속적인 신뢰 관계를 보장합니다.
페어링 과정이 복잡하고 전력 소모가 크지만, 한 번 완료되면 견고한 보안을 기반으로 안정적인 고속 통신이 가능합니다.
📕 참고. PIN와 Secure Simple Pairing (SSP)
BLE의 페어링은 빠르고 간단한 연결을 위해 설계되었습니다. 이는 '수면 모드'를 주로 사용하는 저전력 기기의 특성에 맞춘 것입니다.
광고(Advertising): 연결하려는 BLE 기기는 자신의 존재를 광고 패킷을 통해 외부에 알립니다.
연결 요청: 스캔 중인 기기가 이 광고를 감지하고, 연결 요청(Connection Request)을 보냅니다.
페어링/본딩(Bonding):
Just Works: 보안이 중요하지 않은 경우, 비밀번호 없이 바로 연결됩니다. 가장 편리하지만, 중간자 공격(Man-in-the-Middle attack)에 취약할 수 있어 보안이 중요하지 않은 장치에 적합합니다.
Passkey Entry: 사용자가 6자리 숫자를 입력하여 인증하는 방식입니다.
Numeric Comparison: 두 기기에 동일한 6자리 숫자가 표시되고, 사용자가 확인을 누르는 방식입니다.
OOB(Out-of-Band): NFC와 같은 다른 무선 기술을 통해 페어링 정보를 교환하는 방식입니다. 예를 들어, 스마트폰을 NFC 태그에 갖다 대는 것만으로도 블루투스 페어링이 완료됩니다.
장기 키(LTK) 생성: 페어링이 성공하면 장기 키(Long-Term Key, LTK)를 생성하고 저장합니다. 이 키는 이후 재연결 시 사용됩니다.
📕 참고. 페어링과 본딩 용어 차이
페어링은 블루투스 기기가 처음 연결될 때 거치는 일련의 보안 절차를 의미합니다. 본딩은 페어링 과정에서 생성된 보안 키를 두 기기가 각자의 메모리에 저장하는 행위를 의미합니다. 즉, 페어링이 완료된 후 재연결을 위해 그 보안 정보를 영구적으로 기록하는 것입니다.
페어링은 새로운 사람과 처음 만나 서로를 확인하고 명함을 교환하는 행위와 같습니다. 본딩은 그 명함 정보를 휴대폰 주소록에 저장하는 행위와 같습니다. 본딩된 기기는 저장된 정보를 이용해 신속하게 재연결할 수 있습니다. (와... 비유 좋다)
블루투스 클래식은 직렬 통신(Serial Communication) 방식을 모방한 SPP(Serial Port Profile) 와 같은 프로파일을 주로 사용합니다.
BLE는 GATT(Generic Attribute Profile) 라는 프로파일을 기반으로 데이터를 교환합니다. GATT는 데이터를 '특성(Characteristic)'이라는 작은 단위로 나누어 관리합니다.
특성(Characteristic): BLE 통신에서 데이터는 '특성'이라는 형태로 저장됩니다. 예를 들어, 온도 센서는 '온도'라는 특성을 가지며, 이 특성은 '현재 온도 값'을 저장하는 변수와 같습니다. 다른 기기는 이 특성을 읽거나 쓸 수 있습니다.
ESP32는 Wi-Fi와 함께 듀얼 모드 블루투스를 지원하는 강력한 SoC(System on a Chip)입니다. 이는 하나의 칩에서 블루투스 클래식(Bluetooth Classic)과 BLE 기능을 모두 사용할 수 있다는 의미입니다.
ESP32 블록 다이어그램을 살펴보겠습니다. (참고)

📕 시분할(Time-sharing) 작동 방식 - 공존 메커니즘
ESP32는 Wi-Fi와 블루투스 간의 통신 우선순위를 조절하여 짧은 시간 단위로 RF 모듈을 전환하며 사용합니다. ESP32의 칩 내부에는 Wi-Fi와 블루투스 트래픽을 관리하는 공존(Coexistence) 메커니즘이 내장되어 있습니다. 이 메커니즘은 두 무선 프로토콜이 충돌하지 않고 효율적으로 RF 자원을 공유하도록 스케줄링을 관리합니다.
예를 들어, ESP32가 Wi-Fi 패킷을 전송하는 동안에는 블루투스 통신이 잠시 중단되고, Wi-Fi 전송이 끝나면 블루투스 패킷을 전송하는 식으로 작동합니다. 이러한 전환은 매우 빠르게 일어나기 때문에 사용자는 두 통신이 동시에 작동하는 것처럼 느끼게 됩니다.
ESP32 칩을 선택할 때 블루투스 버전 지원은 중요한 고려 사항입니다. ESP32 칩셋은 모델에 따라 지원하는 블루투스 버전이 다릅니다.
ESP32 보드에는 내부 안테나와 외부 안테나를 연결할 수 있는 옵션이 모두 있습니다. 제조사나 보드 모델에 따라 한 가지만 제공하거나, 둘 다 제공하는 경우가 많습니다.
내부 안테나 (On-board Antenna)
대부분의 ESP32 개발 보드는 PCB(인쇄 회로 기판)에 직접 설계된 내부 안테나를 기본으로 제공합니다. 이 안테나는 보통 PCB 트레이스 안테나라고 불립니다.
별도의 외부 안테나를 구매하거나 연결할 필요가 없어서 사용이 간편합니다. 다만 물리적인 크기와 위치 때문에 외부 안테나보다 신호 강도나 수신 거리가 약할 수 있습니다.
외부 안테나 (External Antenna)
일부 ESP32 보드에는 IPEX(U.FL) 커넥터가 장착되어 있어, 개발자가 외부 안테나를 연결할 수 있습니다. 이는 더 나은 성능을 원할 때 사용됩니다.

외부 안테나를 사용하여 신호 강도를 높이고 통신 거리를 확장할 수 있습니다. 다만 외부 안테나와 연결 케이블을 별도로 구매해야 합니다. 안테나 설치와 배선에 대한 고려가 필요합니다.
Espressif에서 제공하는 공식 개발 프레임워크인 ESP-IDF는 블루투스 기능을 효율적으로 사용할 수 있도록 풍부한 API와 예제 코드를 제공합니다. 개발자는 이 프레임워크를 사용하여 복잡한 블루투스 스택 구현 대신 애플리케이션 개발에 집중할 수 있습니다.
Serial Bluetooth Terminal 앱은 블루투스 직렬 통신(SPP)을 지원하는 안드로이드 애플리케이션으로, ESP32의 Bluetooth Classic 기능을 테스트하는 데 매우 유용합니다.

이 앱은 ESP32의 Bluetooth Classic과 연결하여 양방향 직렬 통신을 수행할 수 있습니다.
테스트 방법은 다음과 같습니다.
Serial Bluetooth Terminal 앱은 Bluetooth Classic에 특화되어 있기 때문에, BLE를 테스트하려면 BLE 전용 앱을 사용해야 합니다. nRF Connect: Nordic Semiconductor에서 제공하는 앱으로, BLE 장치 스캔, GATT 서비스 및 특성(Characteristic) 탐색, 데이터 읽기/쓰기/알림 설정 등 BLE 통신의 모든 기능을 테스트할 수 있는 가장 강력하고 범용적인 도구입니다.
참고로, iOS에서는 Serial Bluetooth Terminal 앱이 없을뿐더러... 블루투스 클래식(SPP)을 직접 테스트하는 것이 안드로이드만큼 쉽지 않습니다. 이는 애플의 보안 정책 때문입니다. (킹갓 애플) - 레딧 참고, Arduino 포럼 글 참고
애플은 아이폰과의 외부 장치 통신에 대해 엄격한 통제 정책을 적용하고 있습니다. 블루투스 클래식 기반의 직렬 통신(SPP) 프로파일은 일반적인 iOS 앱에서 직접 접근할 수 없으며, MFi(Made For iPhone/iPad/iPod) 프로그램을 통해 승인된 장치에만 제한적으로 허용됩니다.
iOS에서 ESP32와 통신을 테스트하는 가장 일반적인 방법은 BLE를 사용하는 것입니다. BLE는 MFi 프로그램의 제약이 덜하며, iOS에서 기본적으로 지원합니다. nRF Connect 또는 LightBlue와 같은 BLE 전용 앱을 사용하면 ESP32의 BLE 서버/클라이언트 기능을 쉽게 테스트할 수 있습니다.
따라서 저는 이전에 사용했던 죽어있던 안드로이드 폰을 소생시켜서 해당 앱을 설치해서 테스트를 진행해보겠습니다. 뒤적뒤적...
Bluetooth를 제대로 작동시키기 위해 먼저 이해해야 할 것은 블루투스 스택, 그리고 다양한 프로필과 프로토콜입니다.
블루투스 스택은 물리적인 무선 신호부터 애플리케이션까지의 계층적 소프트웨어 구조입니다. 하드웨어 제어 → 링크 관리 → 프로토콜 처리 → 프로필 지원 → API 제공의 단계로 구성되어, 개발자가 복잡한 블루투스 통신을 쉽게 사용할 수 있게 해줍니다.
애플리케이션 (사용자 코드)
↓
API/SDK (ESP32 Bluetooth API)
↓
프로필 레이어 (A2DP, HFP, GATT 등)
↓
프로토콜 레이어 (L2CAP, SDP, ATT 등)
↓
HCI (Host Controller Interface)
↓
하드웨어 (RF, 베이스밴드)
이미지 출처: http://intsain.com/w/?p=322

블루투스 스택은 보통 두 부분으로 나눌 수 있습니다.
하드웨어 제어 계층 (Controller, 하위 계층)
호스트 계층 (Host, 상위 계층)

위 그림은 ESP32는 상위에 Bluedroid 또는 Apache NimBLE 스택이 있고, 하위에 Bluetooth Controller가 있으며, 이들 사이는 VHCI(Virtual Host Controller Interface)로 연결되어 블루투스 통신을 처리한다는 것을 보여줍니다.
프로파일 vs 프로토콜 차이
프로토콜이란 데이터를 주고받는 "방법"과 "규칙"을 의미합니다. 블루투스의 프로토콜 대표적인 예는 GAP, L2CAP, A2DP, RECOMM 등이 있습니다.
프로파일은 특정 용도(서비스)에 맞게 여러 프로토콜을 조합해 "기기 간 호환성"을 보장하는 상위 규약입니다. 즉, "이런 용도의 기기는 이런 프로토콜을 이렇게 조합해서 써라" 라고 정해놓은 "표준 사용 설명서" 같은 것입니다.
예를 들어, A2DP 프로파일은 오디오 스트리밍을 위해 L2CAP, AVDTP, SBC 등 프로토콜을 조합해서 쓰고요. SPP 프로파일은 시리얼 통신을 위해 RFCOMM, L2CAP, SDP 등 프로토콜을 조합해서 씁니다.
정리해보면 프로토콜은 "기본 규칙"이고요. 프로파일은 "용도별 표준 조합"입니다. 여러 프로토콜이 쌓여서(스택) 하나의 프로파일을 구성합니다.
Bluedroid vs NimBLE 차이
Bluedroid와 NimBLE은 블루투스 스택의 상위 레이어(Host 부분) 구현체가 다릅니다. Bluedroid는 Android 기반의 무겁고 완전한 구현체이고, NimBLE은 임베디드 시스템을 위한 경량화된 구현체입니다.
Bluedroid는 모든 계층을 포함한 거대한 스택(Classic BT + BLE 프로필 다수 지원)이지만, NimBLE은 BLE 전용으로 최적화된 미니멀 스택으로 불필요한 레이어와 기능을 제거하여 메모리와 전력을 절약합니다.
같은 ESP32 하드웨어를 사용해도 어떤 스택을 선택하느냐에 따라 사용 가능한 메모리, 지원 기능, 전력 소모, 부팅 시간이 크게 달라집니다. 마치 같은 컴퓨터에 Windows vs Linux를 설치하는 것과 비슷한 차이입니다.
핵심: Bluedroid(풀스택) vs NimBLE(경량스택)!
SPP는 직렬 포트 프로파일(Serial Port Profile)의 약자로, 블루투스 기기 간에 가상 직렬 통신 포트를 생성하여 데이터를 주고받을 수 있게 해주는 프로파일입니다. 주로 블루투스 클래식(Bluetooth Classic)에서 사용됩니다.
데이터를 연속적인 스트림 형태로 주고받습니다. 한 번 연결이 수립되면 데이터를 실시간으로, 양방향으로 보낼 수 있습니다. 데이터 형식에 특별한 제한이 없어, 텍스트, 바이너리 데이터 등 다양한 종류의 정보를 전송할 수 있습니다.
SPP 서버 (Server): 연결을 수신하고 기다리는 쪽입니다. 수신자 역할을 합니다. 주변 기기가 자신을 찾아서 연결 요청을 보내올 때까지 대기합니다. 연결이 수립되면 데이터를 주고받을 수 있습니다.
SPP 클라이언트 (Client): 연결을 시작하고 요청하는 쪽입니다. 송신자 역할을 합니다. 주변의 SPP 서버 장치를 검색하고, 발견한 장치에 연결을 요청하여 통신을 시작합니다.
클라이언트(마스터)는 주변 장치를 검색하기 위해 주기적으로 탐색 신호를 방송합니다. 서버(슬레이브)는 탐색 가능(Discoverable) 모드인 경우, 이 신호를 듣고 자신의 정보(주소, 이름, 서비스 등)로 응답합니다.
따라서 스마트폰에서 ESP32가 발견되려면 ESP32는 서버여야 합니다. 스마트폰의 블루투스를 켜면, 스마트폰은 주변의 블루투스 장치를 탐색(scanning)하는 클라이언트 역할을 합니다. 반면, ESP32는 자신의 존재를 주변에 알리는 광고(advertising)를 하는 서버 역할을 해야 합니다.
examples/bluetooth/bluedroid/classic_bt 폴더에는 Bluetooth Classic(클래식) 관련 다양한 예제가 들어있습니다. 그 중에서 SPP 관련 예제가 있습니다.
1. 프로젝트 생성 및 menuconfig 설정
ESP-IDF 템플릿을 사용하여 새로운 프로젝트를 만듭니다. menuconfig을 실행하여 Bluetooth를 활성화해줘야 합니다.
만약 이걸 하지 않았을 때, 블루투스 관련 헤더(ex. esp_bt.h)를 찾을 수 없다는 에러가 발생합니다.
fatal error: esp_bt.h: No such file or directory 6 | #include "esp_bt.h"
ESP-IDF에서는 블루투스 관련 헤더 파일들이 Bluetooth 컴포넌트 안에 들어있습니다. 이 파일이 안 보이는 경우는 메뉴 설정(menuconfig)에서 Bluetooth 기능이 꺼져 있기 때문입니다. 기본적으로 일부 ESP-IDF 프로젝트는 CONFIG_BT_ENABLED가 n 으로 되어 있어서 BT 기능이 비활성화되어 있습니다.

메뉴 설정 > Bluetooth 설정에 들어가서 아래와 같이 설정해줍니다.
2. 헤더 파일 포함
이제 main.c 에서 코드를 작성해보겠습니다. 먼저 필요한 헤더 파일을 포함시켜줍니다.
// 기본 시스템 헤더
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "esp_log.h"
// 블루투스 관련 헤더
#include "esp_bt.h" // 블루투스 컨트롤러 초기화 및 모드 설정 (Classic / BLE / Dual)
#include "esp_bt_main.h" // Bluedroid 스택 초기화 및 활성화
#include "esp_gap_bt_api.h" // GAP 관련 API (검색/연결 모드, 이벤트 처리)
#include "esp_bt_device.h" // 블루투스 장치 관련 API (이름/주소 관리)
#include "esp_spp_api.h" // SPP(Serial Port Profile) 관련 API
DEVICE_NAME 과 같은 내용을 정의해줍니다.
#define SPP_SERVER_NAME "SPP_SERVER"
#define DEVICE_NAME "esp32test"
static const char *TAG = "BT_SPP_DEMO";
3. SPP 콜백 함수 작성
SPP 연결 상태 변화(초기화, 연결, 데이터 수신 등)가 발생할 때 호출되는 콜백 함수를 정의합니다. 이 함수 내에서 핵심 로직을 처리합니다.
/* SPP 이벤트 콜백 */
static void spp_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
{
switch (event) {
case ESP_SPP_INIT_EVT:
ESP_LOGI(TAG, "ESP_SPP_INIT_EVT");
// 장치 이름 설정
esp_bt_gap_set_device_name(DEVICE_NAME);
// 서비스 검색 허용
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
// SPP 서버 시작
esp_spp_start_srv(ESP_SPP_SEC_AUTHENTICATE, ESP_SPP_ROLE_SLAVE, 0, SPP_SERVER_NAME);
break;
case ESP_SPP_START_EVT:
ESP_LOGI(TAG, "ESP_SPP_START_EVT");
break;
case ESP_SPP_SRV_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT");
break;
case ESP_SPP_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_OPEN_EVT");
break;
case ESP_SPP_CLOSE_EVT:
ESP_LOGI(TAG, "ESP_SPP_CLOSE_EVT");
break;
case ESP_SPP_DATA_IND_EVT:
ESP_LOGI(TAG, "SPP_DATA_IND_EVT len=%d handle=%lu",
param->data_ind.len, param->data_ind.handle);
// 데이터 출력
break;
default:
break;
}
}
가장 중요하고 자주 사용되는 주요 이벤트들은 다음과 같습니다.
4. app_main 함수 로직 작성
프로그램의 진입점인 app_main에서 블루투스 스택을 순서대로 초기화하고 활성화합니다.
📌 NVS 초기화
ESP-IDF에서 Wi-Fi / Bluetooth를 사용하기 전에 반드시 NVS를 초기화해야 합니다.
ESP32의 블루투스 기능은 영구적인 설정 데이터 저장을 위해 NVS(Non-Volatile Storage)를 사용합니다. RF 보정 데이터, 페어링 키, 설정값 등을 NVS 플래시에 저장합니다. nvs_flash_init()을 호출하여 NVS를 초기화하는 코드를 app_main 시작 부분에 추가합니다.
esp_err_t ret;
ret = nvs_flash_init();
// 저장소가 가득 찼거나 버전이 바뀌었으면 지우고 다시 초기화
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
NVS 초기화를 하지 않았을 때, 동작은 되지만 에러 메시지가 출력됩니다.
E (476) phy_init: esp_phy_load_cal_data_from_nvs: NVS has not been initialized.
W (486) phy_init: failed to load RF calibration data (0x1101), falling back to full calibration
E (1056) BT_OSI: config_new: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
W (1056) BT_BTC: btc_config_init unable to load config file; starting unconfigured.
E (1066) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (1076) BT_OSI: config_save, err_code: 0x2
Wi-Fi/BT RF 보정(calibration) 데이터를 NVS에서 불러오려고 했는데, nvs_flash_init()을 호출하지 않아서 못 읽었음. 블루투스 스택에서 설정(config)을 NVS에서 불러오려 했는데 초기화 안 됨. 설정 파일을 못 읽었으니 기본값으로 시작한다는 뜻. 블루투스 설정 저장 시도했는데 실패. 저장 실패 에러 코드.
📌 블루투스 컨트롤러 및 스택 초기화
ESP32 블루투스 컨트롤러에서 BLE(Bluetooth Low Energy)용 메모리를 해제하는 함수를 호출합니다.
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
Bluetooth Classic만 사용할 것이므로 BLE 관련 메모리를 해제해서 RAM을 절약해줍니다.
📌 블루투스 관련 설정
// SPP 동작 모드 지정
// ESP_SPP_MODE_CB: 콜백 기반 모드 (이벤트 콜백으로 데이터 처리)
// ESP_SPP_MODE_VFS: 가상 파일 시스템(VFS) 모드 (표준 파일 I/O 인터페이스처럼 사용 가능)
static const esp_spp_mode_t esp_spp_mode = ESP_SPP_MODE_CB;
// BT 컨트롤러 초기화
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BT 컨트롤러 초기화 실패: %s", esp_err_to_name(ret));
return;
}
// BT 컨트롤러 활성화
ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BT 컨트롤러 활성화 실패: %s", esp_err_to_name(ret));
return;
}
// Bluedroid 스택 초기화
ret = esp_bluedroid_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Bluedroid 스택 초기화 실패: %s", esp_err_to_name(ret));
return;
}
// Bluedroid 스택 활성화
ret = esp_bluedroid_enable();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Bluedroid 스택 활성화 실패: %s", esp_err_to_name(ret));
return;
}
// SPP 콜백 등록
ret = esp_spp_register_callback(spp_callback);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPP 콜백 등록 실패: %s", esp_err_to_name(ret));
return;
}
// SPP 초기화
esp_spp_cfg_t bt_spp_fg = {
.mode = esp_spp_mode, // SPP 동작 모드
.enable_l2cap_ertm = true, // L2CAP ERTM (Enhanced Retransmission Mode) 활성화
.tx_buffer_size = 0, // SPP에서 사용할 송신 버퍼 크기 (0 = 기본값)
};
// 'esp_spp_init' is deprecated: Please use esp_spp_enhanced_init
ret = esp_spp_enhanced_init(&bt_spp_fg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPP 초기화 실패: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Bluetooth SPP 초기화 완료. Device Name: %s", SPP_DEVICE_NAME);
ESP_LOGI(TAG, "Waiting for connection...");
블루투스 컨트롤러 초기화/활성화: esp_bt_controller_init() 및 esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT) 호출하여 컨트롤러(하드웨어) 초기화 및 Classic 모드로 활성화 해줍니다.
Bluedroid 스택 초기화/활성화: esp_bluedroid_init() 및 esp_bluedroid_enable()를 호출하여 블루투스 프로토콜 스택(Bluedroid)을 활성화합니다.
SPP 콜백 등록 및 시작: esp_spp_register_callback(esp_spp_cb)를 통해 콜백 함수를 등록하고, esp_spp_init()을 호출하여 SPP 기능을 시작합니다. SPP 동작 모드, L2CAP ERTM(신뢰성 전송) 등 설정 해줍니다.
이제 해당 코드를 빌드, 플래시, 모니터링을 순차적으로 실행해줍니다.
코드를 실행하고 모니터링에서 먼저 나타나는 로그를 살펴보겠습니다.
I (481) BTDM_INIT: BT controller compile version [dc1cd58]
I (491) BTDM_INIT: Bluetooth MAC: 6c:c8:40:34:97:42
I (491) phy_init: phy_version 4860,6b7a6e5,Feb 6 2025,14:47:07
I (1131) BT_SPP_DEMO: ESP_SPP_INIT_EVT
I (1141) BT_SPP_DEMO: ESP_SPP_START_EVT
I (1141) BT_SPP_DEMO: Bluetooth SPP 초기화 완료. Device Name: esp32test
I (1141) BT_SPP_DEMO: Waiting for connection...
I (1151) main_task: Returned from app_main()
ESP32 블루투스 칩의 MAC 주소는 "6C:C8:40:34:97:42" 라는 것을 알 수 있네요. 블루투스 기기 식별용 주소입니다.
무선 물리 계층(phy) 드라이버 초기화 완료되었고요.
ESP_SPP_INIT_EVT: SPP(Serial Port Profile) 초기화 이벤트가 발생했습니다. 블루투스 SPP 라이브러리가 준비된 상태입니다.
ESP_SPP_START_EVT: 블루투스 SPP 서버가 실행을 시작했음을 의미합니다.
“Bluetooth SPP 초기화가 완료되었고, 기기 이름은 esp32tes" 라는 메시지까지 완벽하게 출력되었습니다.
app_main() 함수 실행은 끝났지만 실제 블루투스 이벤트 루프는 백그라운드에서 계속 동작합니다.
스마트폰에서 블루투스 기능을 활성화한 후, 해당 Mac 주소 디바이스를 연결해주면 esp32test 가 정상적으로 등록됩니다. (만약 안뜨면... 몇 번 시도해보세요)

모니터링 로그에서는 다음과 같이 출력됩니다.
W (51671) BT_HCI: hcif conn complete: hdl 0x81, st 0x0 W (57771)
BT_HCI: hcif link supv_to changed: hdl 0x81, supv_to 8000 W (62691)
BT_HCI: hci cmd send: disconnect: hdl 0x81, rsn:0x13 W (62791)
BT_HCI: hcif disc complete: hdl 0x81, rsn 0x16
HCI (Host Controller Interface) 쪽에서 찍힌 로그입니다. 블루투스 연결 완료(Connection Complete) 이벤트가 찍혔고요. 연결 유지 시간 기준이 5초로 설정됩니다. 상대방 장치와 일정 시간 동안 데이터 교환이 없으면 상대방 장치(예: 스마트폰 사용자)가 연결을 끊고, ESP32 쪽에서 종료 처리 완료가 되었습니다.
Serial Bluetooth Terminal 앱을 열고 연결 가능한 디바이스 목록을 살펴보니 esp32test가 있습니다.

짠, 연결이 되었습니다. 메시지를 한번 보내봅니다.

모니터링 출력 메시지를 살펴보겠습니다.
W (481551) BT_APPL: new conn_srvc id:26, app_id:255
I (481551) BT_SPP_DEMO: ESP_SPP_SRV_OPEN_EVT
W (488751) BT_HCI: hci cmd send: sniff: hdl 0x81, intv(400 800)
W (488761) BT_HCI: hcif mode change: hdl 0x81, mode 2, intv 800, status 0x0
W (490041) BT_HCI: hcif mode change: hdl 0x81, mode 0, intv 0, status 0x0
I (490041) BT_SPP_DEMO: SPP_DATA_IND_EVT len=8 handle=385
W (497241) BT_HCI: hci cmd send: sniff: hdl 0x81, intv(400 800)
W (500541) BT_HCI: hcif mode change: hdl 0x81, mode 2, intv 800, status 0x0
W (500541) BT_HCI: hcif mode change: hdl 0x81, mode 2, intv 0, status 0x1f
SPP 서버 연결이 열렸고요, 클라이언트(예: 스마트폰)가 ESP32의 SPP 서비스에 접속 성공해서 ESP_SPP_SRV_OPEN_EVT 가 발생했습니다. 이제 양방향 데이터 통신 가능 상태입니다.
sniff는 ESP32가 블루투스 링크를 Sniff Mode (저전력 모드) 로 전환하려고 시도하는 로그입니다. Sniff Mode는 연결을 유지하면서 주기적으로만 깨어나 패킷 확인해서 전력 절약 효과가 있습니다.
그리고 나서 제가 메시지를 보냈더니 Sniff 모드 해제, 그 직후 SPP_DATA_IND_EVT 발생되었고, 길이 8바이트 데이터가 수신되었다는 걸 알 수 있습니다.