11.11. L2CAP 채널

GATT는 키/값 모델입니다. 제공하는 작업(read, write, notify, indicate)은 한 번에 하나의 짧은 값을 전달하며, 단일 페이로드로 운반할 수 있는 최대 크기는 협상된 MTU가 허용하는 것, 잘해야 수백 바이트입니다. 이는 센서 측정값, 명령 레지스터, 상태 플래그에는 잘 동작합니다. 그러나 킬로바이트나 메가바이트 단위에서는 무너집니다. 긴 블롭을 수백 개의 작은 write로 분할하면 라디오가 훨씬 빠르게 처리할 수 있는 것보다 더 많은 왕복 비용이 듭니다.

대용량 데이터 전송 – 카메라가 휴대폰으로 스트리밍하는 캐프처된 프레임, 무선 업데이트 이미지, 일괄 처리된 측정값 내보내기 – 의 경우 BLE는 대안 경로를 제공합니다. 바로 Logical Link Control and Adaptation Protocol, 즉 L2CAP입니다. L2CAP은 링크 계층과 GATT 사이에 위치하며, 애플리케이션이 동일한 무선 링크 위에 자체적인 연결 지향 채널(connection-oriented channel)을 확보할 수 있게 해줍니다. 이 채널은 크레딧 흐름 제어 방식의 바이트 경로로, 패킷당 MTU가 훨씬 크고 중간에 GATT 프레이밍이 없습니다.

11.11.1. L2CAP을 사용할 때

L2CAP 채널이 적합한 도구인 경우:

  • 전송이 수백 바이트를 초과할 때.

  • 양쪽 끝 모두 L2CAP 채널이 사용될 것임을 알고 있을 때(광고 페이로드에 노출되지 않으므로, 클라이언트는 채널의 프로토콜/서비스 멀티플렉서, 즉 PSM 번호를 대역 외에서 알아야 합니다).

  • 애플리케이션이 GATT의 편의 기능을 포기할 의향이 있을 때: UUID에 의한 내장 주소 지정 없음, 표준 앱을 통한 클라이언트 검색 없음, 알림 없음.

aioble 기반 애플리케이션에서 가장 흔한 경우는 PSM 규약을 모두 알고 있는 두 소프트웨어 사이에서 바이너리 블롭을 옮기는 것입니다. 예를 들어 사용자 정의 카메라-휴대폰 프로토콜, 서로 통신하는 openmv 카메라 한 쌍, peripheral의 GATT 서비스 아래 내부 펌웨어 업데이트 경로 등이 있습니다.

그 외 모든 경우에는 GATT를 사용하세요. 짧은 상태, 제어 레지스터, 센서 측정값은 모두 특성에 속합니다.

11.11.2. 채널 설정하기

L2CAP은 기존 aioble.DeviceConnection 위에서 실행되므로, GAP 측 검색 / 광고 / 연결 흐름은 GATT와 완전히 동일합니다. 양쪽이 연결을 보유하면, 한쪽은 PSM에서 리스닝하고 다른 쪽은 그것에 연결합니다.

PSM은 작은 정수일 뿐입니다. Bluetooth SIG는 표준화된 용도를 위해 범위의 하단(0x0001-0x007F)을 예약합니다. 애플리케이션별 채널에는 동적 범위의 번호를 사용하세요(고정 PSM의 경우 0x0080-0x00FF, 일반적으로 0x0040 이상이 사용자 정의 용도로 자유롭게 사용 가능). 양쪽 모두 사전에 값에 합의해야 합니다.

L2CAP 채널의 MTU는 어느 쪽이든 단일 send() 에서 전달할 가장 큰 단일 SDU(Service Data Unit)이며, BLE 링크 MTU가 아닙니다. Aioble은 더 큰 페이로드를 자동으로 단편화합니다. 카메라의 BLE 호스트는 L2CAP MTU를 1017바이트로 제한합니다. 512 는 RAM을 많이 소비하지 않으면서 양쪽에 여유를 남기는 합리적인 기본값입니다.

리스너 측에서(예: peripheral로서의 카메라):

async def serve_l2cap(connection, image_bytes):
    channel = await connection.l2cap_accept(psm=0x80, mtu=512)
    async with channel:
        # image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
        # or a compressed JPEG buffer. send() fragments into MTU-sized
        # chunks automatically and awaits flow-control credits between.
        await channel.send(image_bytes)
        await channel.flush()

연결 측에서(예: 휴대폰 또는 central):

async def open_l2cap(connection, total_bytes):
    channel = await connection.l2cap_connect(psm=0x80, mtu=512)
    async with channel:
        image_bytes = bytearray(total_bytes)
        view = memoryview(image_bytes)
        received = 0
        while received < total_bytes:
            n = await channel.recvinto(view[received:])
            if n == 0:
                break
            received += n
        return image_bytes

l2cap_accept() 는 피어가 연결할 때까지(또는 timeout_ms 가 만료될 때까지) 차단합니다. l2cap_connect() 는 리스너가 수락할 때까지(또는 실패할 때까지) 차단합니다. 둘 다 aioble.L2CAPChannel 을 반환하며, 이는 종료 시 채널을 닫는 비동기 컨텍스트 매니저 자체입니다.

11.11.3. 보내기와 받기

채널에서의 두 가지 주요 작업은 send()(피어에게 바이트를 쓴)와 recvinto()(미리 할당된 버퍼로 읽어 들임)입니다. 둘 다 코루틴입니다.

  • send() 는 버퍼를 MTU 크기의 청크로 단편화하고 그 사이에 링크 계층 흐름 제어 크레딧을 기다립니다. 긴 send는 애플리케이션 관점에서 하나의 await 입니다. 내부적으로는 여러 패킷을 큐에 넣고 피어의 수신 크레딧이 소진될 때마다 일시 정지할 수 있습니다.

  • recvinto() 는 사용 가능한 모든 것(채널의 MTU까지)으로 전달된 버퍼를 채우고 바이트 수를 반환합니다. 사용 가능한 것이 없으면 await합니다.

  • available() 은 버퍼링된 데이터가 준비되어 있으면 동기적으로 True 를 반환합니다. 일시 정지 없이 폴링하는 데 유용합니다.

  • flush() 는 미해결 send가 컨트롤러로 완전히 전송될 때까지 await합니다.

L2CAP 채널은 바이트가 순서대로 손실 없이 도착한다는 점에서 스트림과 유사하지만, 단일 send 의 경계는 보존됩니다. 각 SDU는 단일 recvinto 에서 나옵니다. 이는 하나의 send() 경계가 여러 recv() 호출에 걸쳐 뭉개질 수 있는 TCP와는 다릅니다.

11.11.4. 연결 해제 처리

채널은 세 가지 조건에서 사라집니다: 어느 쪽이든 disconnect() 를 호출하거나, 기반 GAP 연결이 끊기거나, L2CAP 수준의 연결 해제가 도착하는 경우입니다. 활성 작업은 aioble.L2CAPDisconnectedError 를 발생시킵니다. GATT 측과 마찬가지로, 이는 대기 중이던 코루틴에서 예외로 표면화되며 async with channel 블록은 깔끔하게 종료됩니다.

GAP 수준의 연결 해제로 채널에 도달할 수 없게 되면, 애플리케이션은 GATT 연결 해제와 동일한 방식으로 광고나 스캔으로 다시 루프를 돕니다.

11.11.5. 메모리 비용

더 큰 MTU와 더 긴 큐는 양쪽에서 더 많은 RAM을 사용합니다. 512바이트 MTU에 채널당 수신 버퍼를 더하면 채널당 약 1 KB이며, 여러 채널이 한 번에 열려 있으면 작은 카메라에서는 결코 공짜가 아닙니다. 연결당 하나의 채널을 고수하고 예상 메시지 크기에 맞는 MTU를 선택하세요. 대부분의 애플리케이션에서는 DeviceConnection 당 하나의 L2CAPChannel 이라는 기본값으로 충분합니다.

L2CAP은 BLE의 안전 밸브입니다. GATT는 거의 모든 애플리케이션이 가장 먼저 찾는 것이며, 이 섹션의 나머지 central / peripheral 예제는 GATT를 고수합니다. 채널 방식 API는 애플리케이션이 키/값 모델을 넘어설 때의 해답입니다.