11.4. 광고와 스캔

이전에 만난 적이 없는 두 BLE 기기는 먼저 서로를 찾아야 합니다. 네트워킹은 모든 기기에 공유 풀에서 주소를 할당하고 양쪽이 라우터를 통해 서로 도달할 수 있게 하여 이를 해결합니다. BLE에는 라우터도, 공유 풀도 없으며, 대부분의 기기 쌍 사이에는 사전 관계가 전혀 없습니다. Generic Access Profile(GAP)은 대신 브로드캐스트-앤-리슨 패턴으로 발견 문제를 해결합니다. 한쪽이 광고합니다. 즉, 자신이 누구인지 설명하는 짧은 패킷을 세 개의 광고 채널에 일정한 간격으로 전송합니다. 다른 쪽은 스캔합니다. 즉, 그 패킷을 들으며 동일한 세 채널을 훑습니다.

GAP는 그 패턴을 중심으로 네 가지 역할을 정의하며, 각각은 광고와 청취의 특정 조합입니다.

11.4.1. 네 가지 GAP 역할

2x2 행렬입니다. 행에는 "광고함"과 "광고하지 않음"이 표시되어 있습니다. 열에는 "연결을 수락함"과 "연결을 수락하지 않음"이 표시되어 있습니다. 네 칸에는 역할 이름이 들어 있습니다: Peripheral, Broadcaster, Central, Observer.

네 가지 GAP 역할. 세로축은 기기가 광고하는지 여부이고, 가로축은 기기가 연결을 수락(또는 시작)하는지 여부입니다.

  • peripheral은 “나 여기 있고, 나에게 연결할 수 있다”고 말하는 패킷을 광고합니다. 다른 기기가 연결을 열면 peripheral은 광고를 멈추고 GATT 요청 처리를 시작합니다. 심박 측정 밴드, 온도계, 그리고 센서 역할을 하는 대부분의 카메라가 peripheral로 동작합니다.

  • central은 peripheral을 스캔하고, 하나를 골라 연결을 시작합니다. 연결 후에는 클라이언트로서 GATT로 통신합니다. 휴대폰, 노트북, 그리고 데이터 수집기 역할을 하는 카메라가 central입니다.

  • broadcaster는 광고하지만 연결을 절대 수락하지 않습니다. 그 광고 페이로드 자체가 데이터입니다. 연결할 대상이 없습니다. iBeacon과 대부분의 매장 내 존재 감지 비콘이 broadcaster입니다.

  • observer는 그러한 광고를 스캔하고 페이로드를 읽으며, 역시 연결은 전혀 하지 않습니다. 근처 비콘을 청취하고 들은 내용에 따라 동작하는 카메라가 observer입니다.

단일 기기는 동시에 둘 이상의 역할을 수행할 수 있습니다. 카메라는 자신의 상태를 게시하는 peripheral인 동시에 근처 센서에 연결하는 central일 수 있습니다. 라디오가 그 작업을 다중화합니다.

11.4.2. 광고 패킷에 담기는 내용

광고 패킷은 작습니다. 페이로드 31바이트이며, 광고자가 스캐너가 즉석에서 요청할 수 있는 스캔 응답도 함께 게시하면 62바이트입니다. 페이로드는 짧은 타입 지정 필드의 목록입니다:

  • Flags. 연결 가능 여부, 일반/제한 검색 가능 여부.

  • Local name. 짧고 사람이 읽기 쉬운 문자열로, 휴대폰이나 노트북의 운영 체제가 Bluetooth 메뉴에 표시하는 이름입니다.

  • Service UUID. 기기가 호스팅하는 GATT 서비스 식별자 목록으로, 스캐너가 먼저 연결하지 않고도 적합한 peripheral을 인식할 수 있게 합니다. 심박 측정 밴드는 표준 Heart-Rate 서비스 UUID인 0x180D를 광고하며, 휴대폰의 심박 측정 앱은 그것만으로도 해당 기기가 연결할 가치가 있음을 알 수 있습니다.

  • Appearance. Bluetooth 할당 번호 목록에서 가져온 16비트 값(센서, 일반 미디어, 일반 시계 등)으로, central에게 무엇을 표시할지에 대한 힌트입니다.

  • 제조사별 데이터. 회사 ID가 앞에 붙은 자유 형식 바이트입니다. iBeacon은 이 필드를 사용하여 자신의 UUID, major, minor를 담고, 사용자 정의 애플리케이션은 여기에 원하는 무엇이든 넣을 수 있습니다.

광고 페이로드는 빡빡합니다. 31바이트 제한은 무엇을 포함할지 결정하는 것을 실질적인 설계 결정으로 만듭니다. 사람이 읽기 쉬운 긴 이름은 금세 서비스 UUID를 위한 공간을 남기지 않을 수 있습니다. aioble.advertise() API는 이들 각각을 키워드 인수로 받아 바이트를 조립해 주며, 메인 패킷이 가득 차면 자동으로 스캔 응답으로 넘쳐 들어가게 합니다.

11.4.3. 능동 스캔과 수동 스캔

스캐너는 수동으로 실행되어 광고 패킷을 청취하고 도착한 것을 파싱할 수도 있고, 능동으로 실행되어 각 광고자에게 스캔 요청도 보내고 돌아오는 스캔 응답을 파싱할 수도 있습니다.

수동 스캔은 초기 광고 패킷(최대 31바이트)만 봅니다. 능동 스캔은 그것을 두 배로 늘립니다. 스캔 응답은 peripheral이 미처 담지 못한 필드에 사용할 수 있는 또 다른 31바이트입니다. 능동 스캔은 또한 양쪽 모두에서 전력을 소모하는데, 스캐너가 송신하고 광고자가 추가 패킷을 송신하기 때문입니다. 따라서 이는 기본값이 아니라 선택 사항입니다.

aioble API에서 aioble.scan()active=True는 모드를 전환하며, 각 ScanResult는 결합된 adv_dataresp_data뿐 아니라 바이트 수준 파싱을 숨겨 주는 result.name()result.services() 같은 헬퍼도 제공합니다.

참고

adv_dataresp_data 속성은 원시 광고 및 스캔 응답 페이로드(bytes)입니다. 헬퍼인 name(), services(), manufacturer()는 흔한 표준 필드를 다루며 99%의 경우에 올바른 선택입니다. 헬퍼가 파싱하지 않는 벤더 필드(Eddystone URL, iBeacon UUID/major/minor, 사용자 정의 광고 유형)가 필요할 때만 원시 바이트를 사용하십시오. 바이트 레이아웃은 표준 TLV 형식입니다. 각 필드는 length, type, value...입니다.

11.4.4. 광고 간격

peripheral이 얼마나 자주 브로드캐스트하는지는 전력과 발견 지연 시간 사이의 절충입니다. 20 ms마다 나가는 광고는 스캐너에 거의 즉시 포착되지만 라디오를 바쁘게 유지하여 배터리를 소모합니다. 1초마다 나가는 광고는 전력을 거의 쓰지 않지만 스캐너가 기기를 인지하는 데 더 오래 걸리게 만듭니다.

aioble.advertise()interval_us는 간격을 마이크로초 단위로 설정합니다:

  • 20,000~100,000 us (20 ms - 100 ms) – 빠른 페어링, 앱이 빠른 응답을 기대하는 경우, 전원에 연결된 기기.

  • 250,000~1,000,000 us (250 ms - 1 s) – 충전을 소모하지 않으면서 검색 가능하기를 원하는 배터리 구동 peripheral에 합리적인 기본값.

  • 1,000,000 us 초과 – 느린 백그라운드 브로드캐스트, 몇 초마다 위치 업데이트를 보내는 비콘.

스캐너 쪽에도 자체 조정 항목이 있습니다 – aioble.scan()interval_uswindow_us (스캐너가 얼마나 자주 무선을 깨우는지, 그리고 매번 얼마나 오래 수신 대기하는지)를 인자로 받습니다. 기본값으로도 충분하며, 흔히 변경하는 경우는 배터리가 문제되지 않을 때 연속 스캔을 위해 두 값을 동일하게 설정하는 것뿐입니다.

11.4.5. 비연결 패턴 – broadcaster와 observer

주변장치로 동작하기센트럴로 동작하기 페이지는 API의 연결형 형태를 다룹니다. 여기서는 peripheral이 연결을 수락하고 양쪽이 GATT를 통해 데이터를 주고받습니다. 다른 형태는 비연결형입니다. broadcaster가 페이로드를 광고로 전송하면, 범위 안의 모든 observer가 연결 없이 그것을 읽을 수 있습니다. 비콘, 존재 감지 센서, 단방향 텔레메트리가 모두 여기에 해당합니다.

broadcaster는 connectable=False를 지정한 aioble.advertise()입니다. 제조사별 데이터가 페이로드를 담습니다:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

timeout_ms 키워드는 1초 후 advertise 호출을 종료합니다. 바깥쪽 루프가 다음 시퀀스 번호로 이를 다시 발행하여 청취자가 새로운 데이터를 보도록 합니다. connectable=False 플래그는 광고를 broadcaster 방식으로 만드는 요소입니다. 카메라는 연결 요청이 도착하더라도 응답하지 않습니다.

observer는 이에 대응하는 읽기 전용 스캐너입니다. aioble.scan()을 무한히 실행하고, 들어오는 광고를 파싱하며, connect()를 절대 호출하지 않습니다:

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0은 컨텍스트 관리자가 종료될 때까지 스캔합니다. active=False는 가장 낮은 전력 소모를 위해 observer 자신의 라디오를 조용히 유지합니다(스캔 응답 요청 없음). manufacturer()filter= 인수는 회사 ID와 일치하지 않는 모든 광고를 버리므로, 루프는 broadcaster의 트래픽에 대해서만 실행됩니다.

11.4.6. 발견에서 연결로

central이 통신할 peripheral을 선택하고 나면, 청취를 멈추고 peripheral이 마지막으로 사용한 광고 채널에서 연결 요청을 보내며, 양쪽은 링크 계층의 홉핑 데이터 채널로 진입합니다. 이 시점에서 peripheral은 일반적으로 광고를 멈춥니다. 그다음에 일어나는 일(연결 파라미터, GATT 발견, 링크의 수명)은 연결에서 다룹니다.