Chapter 11

ATT/GATT 심화: EATT, 큐 쓰기, Database Hash

1권 7장은 속성과 서비스·특성, 읽기·쓰기·알림을 다뤘다. 이 장은 현장에서 부딪히는 세부로 내려간다. 요청 하나가 끝나야 다음을 보낼 수 있는 순차 규칙과 30초 타임아웃, 긴 값을 나눠 쓰는 큐 쓰기, 여러 요청을 동시에 흘리는 EATT, 그리고 펌웨어를 올린 뒤 휴대폰이 옛 핸들을 계속 쓰는 문제를 막는 Database Hash와 Robust Caching이다.

ATT PDU와 순차 프로토콜

ATT PDU의 첫 바이트 opcode는 아래 6비트가 방법(Method), 비트 6이 명령 플래그(1이면 응답이 없는 명령), 비트 7은 6.3에서 “이전에 쓰던 값”이 되었다(서명된 쓰기의 인증 서명 플래그. 데이터 서명 삭제와 함께 0xD2 Signed Write Command도 사라졌다). 그래서 ATT_WRITE_CMD는 0x52 = 0x12(쓰기 방법) | 0x40이다. 나머지 바이트는 매개변수이고 ATT_MTU − 1까지 들어간다. 여러 바이트 값은 리틀 엔디언이지만, 속성 값 자체의 바이트 순서는 상위 사양이 정한다.

종류방향흐름 제어예
요청(REQ) → 응답(RSP)클라이언트 → 서버 → 클라이언트베어러마다 한 번에 하나READ 0x0A/0x0B, WRITE 0x12/0x13
명령(CMD)클라이언트 → 서버없음, 처리 못 하면 버림WRITE_CMD 0x52
표시(IND) → 확인(CFM)서버 → 클라이언트 → 서버베어러마다 한 번에 하나0x1D / 0x1E
알림(NTF)서버 → 클라이언트없음0x1B, MULTIPLE 0x23

요청-응답과 표시-확인 한 쌍이 트랜잭션이다. 트랜잭션은 한 베어러에서 끝나야 하고, 30초 안에 끝나지 않으면 실패로 처리하며 그 베어러로는 더 아무것도 보내지 않는다. 다시 쓰려면 베어러를 새로 맺어야 하는데, 고정 채널 0x0004 베어러라면 사실상 연결을 끊고 다시 맺는 것이다. 응답이 늦는 서버 하나가 휴대폰 앱 전체를 멈추게 하는 흔한 원인이다.

창구 하나짜리 민원실

일반 ATT 베어러는 창구가 하나인 민원실이다. 앞사람 민원(요청)이 끝나야 다음 민원을 받는다. 서류 넣기(명령)와 안내 방송(알림)은 창구를 거치지 않지만, 손이 모자라면 버려질 수 있다. EATT는 창구를 여러 개 여는 것이다. 한 창구에서 긴 서류를 처리하는 동안 옆 창구에서 다른 민원을 본다.

PDU 종류(Table 3.1)·opcode 형식(Table 3.2): [Vol 3] Part F §3.3·§3.3.1 · 순차 프로토콜: §3.3.2 · 트랜잭션과 30초: §3.3.3 · opcode 요약과 0xD2: §3.4.8

긴 값: Read Blob과 큐 쓰기

ATT_MTU − 1보다 긴 값은 한 번에 오가지 못한다. 읽을 때는 ATT_READ_REQ로 앞부분을 받고, 이어서 ATT_READ_BLOB_REQ(0x0C)에 오프셋을 주어 나머지를 차례로 받는다(1권 7장의 왕복 계산). 쓸 때는 큐 쓰기를 쓴다. ATT_PREPARE_WRITE_REQ(0x16)로 (핸들, 오프셋, 조각)을 서버의 대기열에 넣고, 서버는 받은 내용을 그대로 되돌려 준다. 마지막에 ATT_EXECUTE_WRITE_REQ(0x18)에 플래그 1을 주면 대기열 전체를 한꺼번에 반영하고, 0을 주면 모두 버린다.

이 되돌려 받기를 이용한 것이 신뢰성 있는 쓰기(Reliable Writes)다. 클라이언트는 서버가 돌려준 조각이 보낸 것과 같은지 확인하고, 하나라도 다르면 실행 대신 취소(플래그 0)를 보낸다. 서버의 대기열이 가득 차면 Prepare Queue Full(0x09)이 온다. 대기열은 여러 특성의 조각을 함께 담을 수 있어 “여러 값을 원자적으로 바꾸기”에도 쓴다.

예측해 보기

ATT_MTU가 23일 때 ATT_PREPARE_WRITE_REQ 하나에 실을 수 있는 값 조각은 최대 몇 바이트일까?

opcode 1 + 핸들 2 + 오프셋 2.

23 − 1 − 2 − 2 = 18바이트. 100바이트 값을 쓰려면 Prepare가 6번, Execute 1번, 응답까지 합쳐 왕복 7번이다. ATT_MTU를 247로 올리면 Prepare 하나로 끝난다(242바이트). 긴 값을 자주 쓰는 제품은 연결 직후 MTU 교환부터 한다.

Read Blob: [Vol 3] Part F §3.4.4.5 · 큐 쓰기: §3.4.6 · 오류 코드(Table 3.4): §3.4.1.1 · GATT의 긴 값 쓰기·신뢰성 있는 쓰기: [Vol 3] Part G §4.9.4·§4.9.5

EATT: 창구 여러 개

EATT(5.2)는 ATT를 10장의 ECFC 채널 여럿 위에 올린다. 순차 규칙은 베어러마다이므로, 채널이 셋이면 요청 셋을 동시에 진행할 수 있다. 덧붙여 바뀌는 것이 있다.

EATT 베어러의 ATT_MTU와 채널 요구: [Vol 3] Part G §5.3 · ATT 베어러: [Vol 3] Part F §3.2.11 · 알림 처리 규칙: §3.3.2 · 새 PDU: §3.4.4.11·§3.4.4.12·§3.4.7.4

Database Hash: 데이터베이스의 지문

휴대폰은 한 번 찾은 핸들을 기억해 두고(1권 7장의 캐싱) 다음 연결에서 바로 쓴다. 그런데 펌웨어를 올려 서비스가 바뀌면 옛 핸들이 엉뚱한 특성을 가리킨다. 이를 알아채는 수단이 GATT 서비스의 Database Hash 특성(UUID 0x2B2A)이다. 서버의 서비스 구조를 요약한 128비트 값이라, 클라이언트는 연결할 때 이것만 읽어 보고 기억해 둔 값과 같으면 다시 찾지 않는다.

계산 규칙: 키가 모두 0인 AES-CMAC에, 핸들 순서대로 다음을 이어 붙인 메시지를 넣는다. 서비스·포함·특성 선언과 확장 속성 디스크립터는 (핸들, 타입, 값), 사용자 설명·CCCD·SCCD·표현 형식·집합 형식 디스크립터는 (핸들, 타입)만, 그 밖(특성 값 등)은 넣지 않는다. 모두 리틀 엔디언이다. 사양의 예: 특성 선언 값 {0x02, 0x0005, 0x2A00}은 02 05 00 00 2A로 붙는다. 그래서 장치 이름 같은 특성 값이 바뀌어도 해시는 그대로이고, 특성을 추가하거나 속성(properties)을 바꾸면 해시가 바뀐다.

WORKBENCH

GATT 데이터베이스와 Database Hash

핸들타입값해시에 넣는 바이트
Database Hash—
처음 상태와 비교—
왼쪽 줄 표시 = 해시 입력에 들어가는 속성. 해볼 것: ① 장치 이름 값을 바꿔도 해시는 그대로다. ② 배터리 서비스를 추가하면 새 핸들이 붙어 해시가 바뀐다. ③ 특성 속성 하나만 바꿔도 바뀐다. 계산은 이 책 7장의 AES-CMAC(RFC 4493 시험 벡터로 검증)이고, 검증 스크립트는 붙이는 규칙을 사양의 예(02 05 00 00 2A)로 확인한다.

Database Hash 특성(0x2B2A)과 읽는 방법: [Vol 3] Part G §7.3 · 계산 규칙과 예: §7.3.1 · 선언 형식: §3.1·§3.3.1

Robust Caching: change-aware와 0x12

해시와 Service Changed를 둘 다 가진 서버는 Robust Caching을 지원해야 한다. 서버는 연결된 클라이언트마다 “변경을 아는가(change-aware)”를 기억한다. 데이터베이스가 바뀌는 순간 모두 change-unaware가 된다. 본딩한 클라이언트는 연결이 끊긴 사이 바뀌었다면 다시 연결할 때 unaware로 시작한다. Robust Caching을 지원한다고 알린 클라이언트가 unaware인 동안,

SIMULATOR

서버가 보는 클라이언트 상태

클라이언트 상태—
클라이언트는 Robust Caching 비트를 켠 상태, ATT 베어러 하나로 연결되어 있다. 해볼 것: ① “데이터베이스 변경” → “읽기 요청”이면 0x12. 같은 요청을 한 번 더 보내면 aware가 되어 정상 응답(클라이언트는 그 사이 캐시를 버리고 다시 찾아야 한다). ② 변경 뒤 “Database Hash 읽기”는 오류 없이 답하고, 그다음 요청에서 aware가 된다. ③ unaware 동안 쓰기 명령은 조용히 버려진다. 앱 입장에서는 “썼는데 반영이 안 된” 버그로 보인다.

Robust Caching과 change-aware 전이(그림 2.5~2.7), 0x12를 보내는 조건, 명령 무시: [Vol 3] Part G §2.5.2.1 · 0x12 Database Out Of Sync: [Vol 3] Part F §3.4.1.1

핵심 정리

  1. opcode = 방법 6비트 + 명령 플래그(비트 6) + 비트 7(6.3에서 이전에 쓰던 값, 0xD2 사라짐). 요청·표시는 베어러마다 하나씩, 30초 넘으면 그 베어러는 끝.
  2. 긴 값: Read Blob으로 읽고 Prepare/Execute로 쓴다. 되돌아온 조각을 비교하면 신뢰성 있는 쓰기.
  3. EATT: ECFC 채널 여럿 = 병렬 트랜잭션, ATT_MTU ≥ 64, 알림을 버리지 않음, Read Multiple Variable·Multiple Handle Value NTF.
  4. Database Hash = AES-CMAC(0, 선언·확장 속성의 핸들·타입·값 + 몇몇 디스크립터의 핸들·타입). 특성 값은 넣지 않는다.
  5. Robust Caching: unaware 클라이언트의 요청엔 0x12(베어러당 한 번), 명령은 무시. 해시 읽기나 0x12 뒤 다음 요청, SC 확인으로 aware.

확인 퀴즈

opcode 0x52의 뜻은?

0x52 = 0x40(명령 플래그) | 0x12(쓰기). 응답이 없다.

ATT 트랜잭션이 30초 안에 끝나지 않으면?

[Vol 3] Part F §3.3.3. 트랜잭션은 베어러를 넘나들 수 없다.

Database Hash 계산에 들어가지 않는 것은?

특성 값은 넣지 않는다. 그래서 이름을 바꿔도 해시는 그대로다.

change-unaware 클라이언트가 쓰기 명령을 보내면?

명령에는 응답이 없으므로 오류도 보낼 수 없다. 그냥 버린다.