11.7. GATT 작업¶
characteristic은 GATT 데이터베이스에 명명된 값으로 그저 존재할 뿐입니다. 이를 유용하게 만드는 것은 클라이언트가 그것에 실행할 수 있는 작고 잘 정의된 작업 집합입니다. 각 characteristic은 지원하는 작업을 property bitmask로 선언합니다 – 노출할 것이 없는 서버는 읽기 전용 값을 게시할 수 있고, 제어 레지스터는 쓰기 전용일 수 있으며, 업데이트를 스트리밍하는 센서는 notify 비트를 설정합니다. 클라이언트는 discovery 중에 이 비트마스크를 발견하고 이를 준수합니다.
다섯 가지 작업은 read, write, write without response, notify, indicate입니다. 이들은 두 그룹으로 나뉩니다 – pull(클라이언트가 요청)과 push(서버가 전송).
11.7.1. Pull: read와 write¶
이 둘은 가장 단순하며 함수 호출과 정확히 같아 보입니다.
Read. 클라이언트가 현재 값을 요청하면 서버가 이를 돌려보냅니다. 한 번의 왕복으로 클라이언트는 서버가 해당 characteristic에 설정한 바이트를 얻고, 서버는 누가 읽었는지에 대해 아무것도 얻지 못합니다.
Write. 클라이언트가 새 바이트를 보내면 서버가 이를 저장합니다(그리고 선택적으로 새 값에 대해 애플리케이션 로직을 실행합니다). 두 가지 방식이 존재합니다:
Write with response – 서버가 확인 응답하며, 0이 아닌 상태에 대해 애플리케이션 오류를 발생시킵니다. 신뢰성이 있고 한 번의 왕복입니다.
Write without response – 서버가 바이트를 조용히 저장하며, 클라이언트는 어떠한 확인 응답도 받지 못합니다. (ack를 기다리는 왕복이 없어) 더 빠르고 스트리밍에 유용하지만, 그 대가로 오류는 사이드 채널 읽기를 통해서만 알 수 있습니다.
aioble 에서 클라이언트 측 API는 response 키워드(True / False / None 으로 피어가 광고하는 내용에 따라 자동 선택)를 가진 단일 aioble.ClientCharacteristic.write() 메서드 뒤로 선택을 숨깁니다.
11.7.2. Push: notify와 indicate¶
pull 모델은 센서 데이터에는 적합하지 않습니다. 휴대폰이 매초 폴링해야 하는 심박수 스트랩은 수백 개의 불필요한 라디오 이벤트로 배터리를 소모할 것입니다. 새 측정값이 있을 때만 값을 push하는 스트랩이야말로 애초에 BLE의 핵심입니다.
GATT는 이를 서버 주도 작업으로 해결합니다. 클라이언트가 characteristic을 구독합니다. 그 시점부터 서버가 값을 업데이트할 때마다 새 값이 링크를 통해 클라이언트로 push됩니다. 두 가지 방식이 있습니다:
Notify. Fire-and-forget입니다. 서버가 알림을 큐에 넣으면 링크 계층이 다음 connection event 동안 이를 전송하고, 클라이언트가 이를 수신합니다. GATT 수준에서는 확인 응답이 없습니다. 링크 계층의 일반적인 재전송이 라디오 측의 손실을 처리하지만, 애플리케이션은 값이 처리되었다는 확인을 받지 못합니다.
Indicate. 서버가 알림을 보내고 그리고 다음 알림을 보내기 전에 클라이언트의 GATT 수준 확인을 기다립니다. 한 번에 하나의 indication입니다. 서버가 클라이언트가 실제로 값을 보았는지 알아야 할 때 사용됩니다 – 중요 경보 characteristic, 구성 확인 응답 등.
Pull(read) 대 push(notify). 알림을 사용하면 클라이언트는 한 번 구독하고 서버는 값이 바뀔 때마다 새 값을 push합니다.¶
구독은 특성에 연결된 디스크립터, 즉 Client Characteristic Configuration Descriptor (CCCD, 0x2902)에 값을 기록함으로써 이루어집니다. 0x0001을 기록하면 notification이 활성화되고, 0x0002를 기록하면 indication이 활성화되며, 0x0000을 기록하면 둘 다 비활성화됩니다. aioble.ClientCharacteristic.subscribe() 메서드는 notify=True 및 indicate=True 키워드 플래그를 사용하여 이 기록 작업을 대신 수행합니다.
구독이 완료되면 클라이언트는 notified() 와 indicated() 로 들어오는 push를 기다립니다 – 둘 다 다음 push가 도착할 때까지 일시 중단되는 async 코루틴입니다.
11.7.3. MTU가 페이로드 크기를 좌우합니다¶
모든 작업은 링크 수립 시점에 연결이 정한 협상된 MTU에 의해 제약됩니다. 기본 MTU는 23바이트이며, GATT 헤더 이후 characteristic 값 바이트로 20바이트가 남습니다. 그보다 큰 것은 더 큰 MTU(aioble.DeviceConnection.exchange_mtu() 를 통해 카메라에서 최대 512바이트까지 상향 협상)에 맞추거나, 여러 characteristic이나 여러 알림으로 분할되어야 합니다.
MTU보다 큰 값에 대한 클라이언트 주도 read 및 write는 GATT의 long 절차(Read Long / Prepare-Write + Execute-Write)에 의해 내부적으로 처리됩니다. aioble 은 이를 투명하게 실행하므로, 크기가 큰 값으로 read() / write() 를 호출하면 단지 더 많은 왕복이 들 뿐입니다. 서버 주도 알림과 indication은 단편화되지 않습니다 – 하나의 push는 MTU에 의해 제한되며, 애플리케이션은 그보다 큰 것을 여러 알림으로 분할하거나 GATT를 완전히 벗어납니다.
정말로 큰 전송 – 캡처된 프레임, 측정값 묶음, 펌웨어 블롭 – 의 경우 올바른 답은 보통 GATT를 완전히 벗어나 대신 L2CAP 채널을 사용하는 것입니다(L2CAP 채널 참조).
11.7.4. 한눈에 보는 양측¶
다섯 가지 작업은 연결의 각 측에서 다르게 노출됩니다:
서버 측에서(일반적인 구성에서는 peripheral):
aioble.Characteristic.read()– GATT 데이터베이스에서 현재 로컬 값을 읽습니다(“클라이언트가 보게 될 것”의 서버 측).aioble.Characteristic.write()– 로컬 값을 업데이트하며, 선택적으로 구독한 모든 클라이언트에게 업데이트를 push합니다.aioble.Characteristic.notify()/indicate()– 특정한 한 클라이언트에게 push를 보냅니다.aioble.Characteristic.written()– 모든 클라이언트로부터 들어오는 다음 write를 기다립니다.aioble.Characteristic.on_read()– 클라이언트가 읽을 때 동기적으로 호출되는 콜백으로, 필요할 때 값을 계산하는 데 유용합니다.
클라이언트 측에서(일반적인 구성에서는 central):
aioble.ClientCharacteristic.read()– 서버에게 현재 값을 요청합니다.aioble.ClientCharacteristic.write()– 응답 유무와 함께 새 값을 보냅니다.aioble.ClientCharacteristic.subscribe()– 알림과 indication을 활성화/비활성화합니다.aioble.ClientCharacteristic.notified()/indicated()– 다음 push를 기다립니다.