Generic Attribute Profile. BLE 로 데이터를 주고받는 표준 규칙이다.
메시지가 아니라 변수다
TCP 소켓이나 CAN 을 먼저 배운 사람이 가장 헷갈리는 지점이다. BLE 에는 "패킷을 보낸다"는 개념이 없다.
CAN 0x210 프레임을 버스에 던진다 ← 메시지 지향
BLE "온도" 라는 변수를 읽는다 / 쓴다 / 바뀌면 알림받는다 ← 속성(attribute) 지향
양쪽이 미리 변수 목록에 합의하고, 그 변수를 조작하는 것으로 통신한다. 그 변수 하나를 캐릭터리스틱(characteristic), 관련된 것들을 묶은 상자를 서비스(service)라 한다.
Service: Vehicle Control ← 상자
├ Characteristic: Request ← 변수 (쓰기 전용)
├ Characteristic: ReqResult ← 변수 (알림 전용)
└ Characteristic: State ← 변수 (읽기 + 알림)
서버와 클라이언트가 뒤집혀 있다
변수를 들고 있는 쪽이 서버다. 차량(ESP32)이 GATT 서버, 폰이 GATT 클라이언트다. 폰이 더 강력한 기기인데 클라이언트라는 게 직관에 어긋나지만, "데이터의 소유자가 서버" 라고 보면 맞는다.
조작 방법은 넷뿐이다
| 연산 | 방향 | 쓰임 |
|---|---|---|
| Read | 클라이언트 → 서버 요청 | 현재 상태를 한 번 가져온다 |
| Write | 클라이언트 → 서버 | 제어 요청을 보낸다 |
| Notify | 서버 → 클라이언트 (응답 없음) | 상태가 바뀌면 밀어 준다 |
| Indicate | 서버 → 클라이언트 (응답 필요) | 놓치면 안 되는 것 |
Notify 가 핵심이다. 이게 없으면 폰이 계속 Read 로 물어봐야 하고(폴링), 배터리가 녹는다. 차량 상태처럼 언제 바뀔지 모르는 것은 반드시 Notify 로 받는다.
Notify 와 Indicate 의 차이는 확인 응답이다. Notify 는 던지고 잊고, Indicate 는 클라이언트의 확인을 기다린다. 그래서 Indicate 가 느리다 — 확인이 올 때까지 다음 걸 못 보낸다.
UUID — 128비트를 쓰는 이유
각 서비스·캐릭터리스틱은 UUID 로 구분한다. 표준으로 정해진 것(심박수·배터리 등)은 16비트 축약형을 쓰지만, 우리가 만든 것은 128비트 커스텀 UUID 를 써야 한다. 16비트 공간은 Bluetooth SIG 가 관리하므로 마음대로 쓰면 충돌한다.
가장 흔한 함정 — CCCD
Notify 를 받으려면 두 곳을 켜야 한다.
① 폰 안쪽 설정 setCharacteristicNotification(char, true)
② 차량에게도 알린다 CCCD 디스크립터에 0x0001 쓰기
CCCD(Client Characteristic Configuration Descriptor)는 "나 이 변수의 알림 구독할게" 를 서버에 등록하는 자리다. ①만 하면 폰은 받을 준비가 됐는데 차량이 보내지를 않는다. 아무 에러도 안 나고 그냥 조용하다 — BLE 입문자가 가장 많이 걸린다.
이 시스템에서의 설계 판단
CAN 에서 올라온 신호를 ESP32 가 예쁘게 재포장하지 않고 Trusted<T> 구조 그대로 흘린다. 중간에서 *_trust 를 떼면 앱이 stale 표시 금지 를 지킬 방법이 사라진다.