Chapter 18

사례로 배우는 디버깅: HCI 로그에서 원인까지

현장의 블루투스 문제는 대개 “가끔 끊겨요” 한 줄로 온다. 그때 손에 쥔 것은 호스트와 컨트롤러 사이를 지나간 HCI 패킷 몇 줄이고, 그 안의 오류 코드 한 바이트가 가장 큰 단서다. 이 장은 흔한 여섯 사례를 로그 바이트부터 읽어, 오류 코드가 무엇을 뜻하고 사양의 어느 규칙에서 나왔는지, 그래서 무엇을 고쳐야 하는지까지 따라간다. 앞 장들에서 배운 규칙이 실제로 어떻게 드러나는지 모아 보는 장이다.

로그 읽는 법

UART 같은 전송에서 HCI 패킷은 앞에 1바이트 종류 표시가 붙는다(H4). 0x01 명령(호스트 → 컨트롤러), 0x02 ACL 데이터(양방향), 0x04 이벤트(컨트롤러 → 호스트). 숫자 필드는 모두 리틀 엔디언이다.

종류모양예
명령 0x01옵코드 2(OGF 6비트 · OCF 10비트) · 길이 1 · 파라미터01 0a 20 01 01 = LE Set Advertising Enable(켬)
이벤트 0x04이벤트 코드 1 · 길이 1 · 파라미터. LE 이벤트는 0x3E 안의 하위 코드04 05 04 00 40 00 08 = 연결 0x0040 끊김, 이유 0x08
ACL 0x02핸들 12비트 + PB·BC 4비트 · 길이 2 · L2CAP(길이 2 · CID 2 · 내용)CID 0x0004 = ATT

명령에는 두 가지 답이 있다. 바로 끝나는 명령은 Command Complete(0x0E, 반환 값 첫 바이트가 Status), 오래 걸리는 명령은 Command Status(0x0F)로 “받았다”만 알리고 결과는 나중 이벤트로 온다. 그래서 Command Status의 0x00은 “성공”이 아니라 “시작했다”일 뿐이다.

블랙박스 기록

HCI 로그는 비행기의 블랙박스와 같다. 조종석(호스트)이 내린 지시와 기체(컨트롤러)가 보낸 보고가 시간순으로 남는다. 다만 기체 바깥, 즉 공중에서 상대 기기와 주고받은 링크 계층 PDU는 적혀 있지 않다. 그래서 한쪽 기록만 보면 “상대가 갑자기 사라졌다”(0x08)처럼 보이는 일이 상대 기록에서는 “내가 끊었다, MIC가 틀려서”(0x3D)로 적혀 있곤 하다. 가능하면 두 기기의 로그를, 필요하면 공중 캡처까지 함께 본다.

H4 패킷 표시: [Vol 4] Part A §2 · HCI 패킷 형식: [Vol 4] Part E §5.4 · Command Complete·Status: §7.7.14, §7.7.15 · 오류 코드 표: [Vol 1] Part F §1.3

연결 한 번의 로그 지도

사례를 보기 전에, 문제없는 연결 하나가 HCI 로그에 어떤 순서로 남는지부터 보자. 아래는 본딩한 두 기기가 다시 연결해 암호화하고, 데이터를 주고받다 끊기까지다. 왼쪽은 Central, 오른쪽은 Peripheral의 HCI 로그이고, 가운데는 로그에 없는 공중의 링크 계층 PDU다. 동그라미 숫자는 아래 해석기의 사례가 나타나는 자리다.

Central HCI 로그 공중 (로그에 없음) Peripheral HCI 로그 → LE Set Adv. Parameters 0x2006 → LE Set Adv. Enable 0x200A → LE Extended Create Connection 0x2043(v1)·0x2085(v2) ← Command Status 0x0F ← 광고 · CONNECT_IND → ← LE Enh. Connection Complete 0x3E/0x0A·0x29 (핸들) ← LE Enh. Connection Complete 0x3E/0x0A·0x29 (핸들) → LE Read Remote Features 0x2016 ← 0x3E/0x04 (상대 기능) LL_FEATURE_REQ / RSP ← 0x3E/0x07 데이터 길이 · 0x3E/0x0C PHY ← 0x3E/0x07 · 0x3E/0x0C LL_LENGTH · LL_PHY → LE Enable Encryption 0x2019 LL_ENC_REQ → ← LE LTK Request 0x3E/0x05 → LTK Request Reply 0x201A (없으면 Negative Reply 0x201B) ← Encryption Change 0x08·0x59 ← Encryption Change 0x08·0x59 LL_START_ENC ⇄ ACL: L2CAP 0x0004 = ATT ← Number Of Completed Packets 0x13 데이터 PDU (MIC 포함) ⇄ ACL: ATT LL_CONNECTION_UPDATE_IND(instant) ← Disconnection Complete 0x05 (Reason) ← Disconnection Complete 0x05 (Reason) ⑤ ③ ② ⑥ ④ ①
그림 18-1. LE 연결 한 번의 HCI 로그 순서. →는 호스트가 보낸 명령, ←는 컨트롤러의 이벤트다. LE 이벤트는 0x3E 뒤의 하위 코드로 구별하고, 같은 이벤트에 v1·v2가 있으면 둘 다 적었다(0x0A·0x29, 0x08·0x59). 동그라미는 해석기의 사례가 드러나는 자리다. ⑤ 광고 설정 거부, ③ LTK 없음, ② MIC 실패, ⑥ ATT_MTU에 걸린 알림, ④ instant가 지난 연결 갱신, ① 감독 타임아웃.

지도를 손에 쥐면 로그 읽기가 “어디서 멈췄나” 찾기가 된다. 연결 완료 이벤트가 오지 않았으면 광고·연결 단계, Encryption Change가 오지 않았거나 Status가 0이 아니면 키 단계, 데이터가 오가다 끊겼으면 Disconnection Complete의 Reason을 본다. 그리고 그 이유가 한쪽 로그만의 이야기인지 상대 로그로 확인한다.

LE Extended Create Connection(v1 0x0043, v2 0x0085): [Vol 4] Part E §7.8.66 · LE Enhanced Connection Complete(v1 0x0A, v2 0x29): §7.7.65.10 · LE Read Remote Features Page 0: §7.8.21·§7.7.65.4 · 데이터 길이 변경 0x07: §7.7.65.7 · PHY 갱신 완료 0x0C: §7.7.65.12 · LE Enable Encryption: §7.8.24 · LTK 요청 0x05와 응답(0x001A·0x001B): §7.7.65.5·§7.8.25·§7.8.26 · Encryption Change(v1 0x08, v2 0x59): §7.7.8 · Number Of Completed Packets: §7.7.19 · Disconnection Complete: §7.7.5

여섯 가지 사례

SIMULATOR

HCI 로그 해석기

사례
이 사례의 근거—
C는 Central, P는 Peripheral 쪽 로그다. ←는 컨트롤러 → 호스트, →는 호스트 → 컨트롤러. 줄을 하나씩 넘기며 다음에 무엇이 올지, 원인이 무엇일지 먼저 짐작해 보자. 마지막 줄까지 읽으면 진단이 열린다. 바이트는 해석기가 직접 풀어서 보여 준다(검증 스크립트가 같은 함수로 확인).
예측해 보기

② 사례에서 Central 로그는 0x3D(MIC 실패)로 끊겼다고 한다. 같은 순간 Peripheral 로그에는 무엇이 적혀 있을 가능성이 가장 클까?

MIC 실패를 알아챈 쪽은 곧바로 연결 상태에서 빠져나간다. 상대는 그걸 어떻게 알까?

[Vol 6] Part E §1: 인증 실패를 감지한 쪽은 연결을 잃은 것으로 보고 호스트에 0x3D를 알린다. “상대 링크 계층은 감독 타임아웃으로 연결 끊김을 알게 된다.” 그래서 한쪽은 0x3D, 다른 쪽은 0x08이 된다.

사례 뒤의 규칙들

LOOKUP

오류 코드 표 ([Vol 1] Part F Table 1.1)

이름—
이 책에서—
“Reserved for future use”와 “Previously used”는 뺐다. 사양은 호스트가 모르는 코드를 받으면 Unspecified Error(0x1F)와 같게 다루라고 한다. 같은 코드가 BR/EDR(LMP)과 LE(LL)에서 이름을 나눠 쓰는 경우가 있다(예: 0x22 LMP/LL Response Timeout).

감독 타임아웃: [Vol 6] Part B §4.5.2 · instant와 6이벤트 권장: §5.5, §5.5.1 · 연결 갱신: §5.1.1 · 암호화 시작과 키 없음: §5.1.3.1 · MIC 실패: [Vol 6] Part E §1 · 절차 충돌(0x23·0x2A): [Vol 6] Part B §5.3 · Set Advertising Parameters의 오류: [Vol 4] Part E §7.8.5 · ATT_MTU 교환: [Vol 3] Part F §3.2.8, §3.4.2.1, §3.4.7.1 · LE 기본 ATT_MTU 23: [Vol 3] Part G §5.2.1 · 오류 코드: [Vol 1] Part F §1.3, §2

로그 얻기와 열기

해석은 로그가 있어야 시작된다. HCI 로그는 운영체제가 호스트와 컨트롤러 사이에서 받아 적는다. 아래 방법은 Core 사양이 아니라 각 운영체제와 도구의 기능이라 판마다 메뉴 이름이 조금씩 다르다(2026년 기준).

환경켜는 법파일
Android개발자 옵션 → 블루투스 HCI 스누프 로그를 “사용”으로 바꾸고 블루투스를 껐다 켠 뒤 재현. “필터링됨”은 데이터 내용을 잘라 낸다adb bugreport의 압축 파일 안 btsnoop_hci.log
iOS·iPadOSApple 개발자 사이트의 Bluetooth 로깅 프로필을 설치하고 재현한 뒤 sysdiagnose를 받는다.pklg (macOS의 PacketLogger로 연다)
Linux (BlueZ)btmon -w trace.log를 켜 둔 채 재현btsnoop 형식
임베디드 칩HCI UART 선을 로직 분석기나 칩 제조사 도구로 받는다H4 바이트열(이 장의 해석기에 그대로 넣을 수 있다)

btsnoop 파일은 Wireshark로 열린다. 필터 몇 개만 알면 수천 줄에서 필요한 곳으로 바로 간다. bthci_evt.code == 0x05는 끊김(그림 18-1의 마지막 줄), bthci_evt.code == 0x3e는 LE 이벤트 전체, btatt는 ATT, btsmp는 페어링이다. 끊김을 찾았으면 그 직전 몇 초를 거꾸로 읽으며 지도의 어느 단계에서 멈췄는지 본다.

HCI 로그가 못 보는 것 HCI 로그는 호스트와 컨트롤러 사이만 적는다. 재전송, 빈 PDU, 상대가 보냈는데 내가 못 받은 패킷처럼 공중에서만 일어난 일은 없다. 그림 18-1의 가운데 칸이 필요하면 nRF Sniffer 같은 공중 캡처 도구로 연결을 따라가야 한다. 다만 암호화된 연결은 키가 있어야 읽힌다. 레거시 페어링은 페어링 과정을 캡처하면 도구가 키를 풀 수 있지만, LE Secure Connections는 엿들어서는 키를 알 수 없게 만든 방식이라(8장) 기기에서 얻은 LTK를 도구에 넣어 주거나 디버그 키로 페어링해야 한다.

핵심 정리

  1. H4: 0x01 명령, 0x02 ACL, 0x04 이벤트. 숫자는 리틀 엔디언, LE 이벤트는 0x3E 안의 하위 코드.
  2. Command Status 0x00은 “시작”일 뿐, 결과는 뒤의 이벤트에서 본다.
  3. 끊김 이유는 양쪽이 다를 수 있다. 알아챈 쪽은 0x3D·0x28, 상대는 0x08로 적힌다.
  4. 감독 타임아웃 하한 = (1 + 지연) × subrate × 간격 × 2. 하한 근처는 합법이지만 위험하다.
  5. 0x06은 키 없음(재페어링), 0x0C는 지금 상태에서 금지된 명령, 알림 20바이트는 ATT_MTU 23의 한계.
  6. 로그는 연결 생성 → 연결 완료 → 기능 교환 → 데이터 길이·PHY → 암호화 → 데이터 → 끊김 순서로 남는다. 어디서 멈췄는지 찾고, 상대 로그로 확인한다.
  7. HCI 로그는 Android 개발자 옵션, iOS 로깅 프로필, Linux btmon으로 얻고 Wireshark로 연다. 공중에서만 일어난 일은 공중 캡처가 필요하다.

확인 퀴즈

04 05 04 00 40 00 13의 뜻은?

이벤트 0x05 Disconnection Complete, Status 0, Handle 0x0040, Reason 0x13 Remote User Terminated Connection.

간격 50 ms, 지연 4, subrate 1일 때 감독 타임아웃으로 쓸 수 있는 가장 작은 값은?

(1 + 4) × 1 × 50 × 2 = 500 ms보다 커야 하고 10 ms 배수이므로 510 ms.

connEventCount가 100인데 받은 LL_CONNECTION_UPDATE_IND의 Instant가 95다. Peripheral은?

(95 − 100) mod 65536 = 65531 ≥ 32767이라 instant가 지났다. 연결 갱신이면 반드시 연결을 잃은 것으로 본다(§5.5.1).

ATT_MTU를 247로 교환한 뒤 알림 하나에 실을 수 있는 값의 최대 길이는?

옵코드 1 + 핸들 2를 빼서 247 − 3 = 244바이트.

Central 로그에서 LE Enable Encryption(0x2019) 뒤로 Encryption Change가 오지 않는다. Peripheral 로그에는 LE Long Term Key Request(0x3E/0x05) 바로 뒤에 0x201B가 있다. 무엇을 먼저 의심할까?

0x201B는 LE Long Term Key Request Negative Reply다. Peripheral의 호스트가 키를 주지 못해 거절했으므로 암호화가 시작되지 않는다(사례 ③, 오류 0x06). 본딩 정보를 지웠거나 잃었으니 다시 페어링한다.