11.12. 동시 역할과 다중 연결¶
peripheral 및 central 페이지에서는 각각 한 번에 단일 연결을 처리하는 단일 역할을 보여줍니다. 실제 애플리케이션은 그렇게 단순한 경우가 드뭅니다. 카메라는 심박 측정 스트랩에서 값을 읽으면서 동시에 휴대폰에 센서 서비스를 게시할 수도 있고, 동시에 페어링된 두 대의 휴대폰으로부터 연결을 수락할 수도 있습니다. aioble API는 이 두 가지 패턴을 모두 지원하는데, 라디오가 내부적으로 멀티플렉싱하고 모든 작업이 이미 코루틴이기 때문입니다. 코루틴을 더 실행하면 하나의 이벤트 루프에서 작업이 병렬로 처리됩니다.
이 페이지에서는 자주 등장하는 패턴들을 모았습니다.
11.12.1. 여러 클라이언트가 하나의 peripheral에 연결하기¶
주변장치로 동작하기 의 단순한 peripheral 루프는 한 번에 연결된 하나의 central만 처리합니다:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
둘 이상의 클라이언트를 수락하도록 하는 패턴은, 연결별 작업을 시작하고 즉시 aioble.advertise() 로 다시 루프를 돌아 다음 클라이언트도 연결할 수 있게 하는 것입니다:
async def handle_client(connection):
async with connection:
# ... per-client work: subscribe their CCCDs,
# push notifications, await writes ...
await connection.disconnected()
async def serve():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-env",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
각 연결은 자체 작업에서 실행됩니다. GATT 데이터베이스는 공유되어 모든 클라이언트가 동일한 서비스와 특성을 보지만, 연결별 상태는 작업 내부에 존재합니다. write() 를 send_update=True 로 호출하면 알림이 구독한 모든 클라이언트로 전송됩니다. 한 클라이언트에게만 도달해야 하는 지정된 푸시는 특정 DeviceConnection 인자와 함께 notify() / indicate() 를 사용합니다.
팬아웃은 작게 유지하세요. 유지되는 각 연결은 라디오 시간, RAM, 그리고 컨트롤러 연결 테이블의 슬롯을 소비하며, 카메라는 수십 개 클라이언트를 위한 허브로 설계되지 않았습니다. central 두세 개(휴대폰, 태블릿, 동반 마이크로컨트롤러)는 충분히 가능한 범위이며, 그 이상이 필요한 설계는 카메라가 아니라 제대로 된 BLE 게이트웨이에 적합합니다.
11.12.2. 동시에 peripheral과 central 역할 수행하기¶
카메라는 휴대폰에 자체 서비스를 광고하면서 동시에 웨어러블에 대한 central 역할을 할 수 있습니다. aioble 에는 “모드” 전환이 없습니다. 광고 루프와 스캔-연결 루프는 그저 독립적인 코루틴일 뿐입니다:
async def be_peripheral():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
async def be_central():
while True:
sensor = await find_sensor()
if sensor is None:
await asyncio.sleep(5)
continue
try:
async with await sensor.connect() as conn:
await stream_from_sensor(conn)
except aioble.DeviceDisconnectedError:
pass
async def main():
await asyncio.gather(be_peripheral(), be_central())
asyncio.run(main())
라디오는 두 역할 사이에서 시간을 분할합니다. 여기서는 스캔 윈도우, 저기서는 광고 버스트, 양쪽 연결 중 하나가 활성화되면 연결 이벤트가 발생합니다. 라디오가 말 그대로 두 가지를 동시에 할 수는 없기 때문에 둘 다 활성화되면 각 역할의 처리량이 떨어지지만, BLE가 설계된 저대역폭 통신에서는 보통 그 비용이 눈에 띄지 않습니다.
염두에 두어야 할 두 가지 실질적인 사항:
두 역할 모두 자체 코루틴에 있어야 합니다. 연결된 central을 처리하는 클라이언트별 작업 내부에서
aioble.scan()을 호출하는 것은 동작하지만, 스캔이 끝날 때까지 해당 클라이언트의 알림을 차단합니다. 대신 스캔은 자체 작업에서 실행하세요.스캔은 한 번에 하나만 실행됩니다. 두 곳에서 스캔해야 한다면 스캔 이터레이터를 공유하거나 접근을 조율하세요. 두 개의
aioble.scan()컨텍스트 매니저를 병렬로 진입하지 마세요.
11.12.3. 하나의 작업에서 여러 연결 조율하기¶
여러 연결을 하나의 논리적 작업으로 결합해야 할 때(예: 카메라가 두 센서와 동시에 통신하고 둘 다 응답한 후에만 결과를 보고하는 경우), 표준 asyncio 기본 요소가 그대로 적용됩니다. asyncio.gather() 는 연결별 코루틴을 동시에 실행하고 모두 완료되면 반환합니다. asyncio.wait_for() 는 마감 시한을 추가합니다.
async def read_pair():
async with await sensor_a.connect() as a:
async with await sensor_b.connect() as b:
value_a, value_b = await asyncio.gather(
read_value(a, A_SERVICE, A_CHAR),
read_value(b, B_SERVICE, B_CHAR),
)
return value_a, value_b
asyncio 장(Asyncio)이 네트워킹에 사용하는 것과 동일한 패턴입니다. BLE 코루틴은 TCP 코루틴과 동일한 방식으로 gather / wait_for / Event / Lock 에 연결됩니다.
11.12.4. 한 역할은 주기마다 끝나고 다른 역할은 그렇지 않을 때¶
배터리 구동 카메라의 한 주기는 다음과 같이 보일 수 있습니다:
깨어납니다.
central로서 페어링된 센서 스트랩에서 새로운 값을 읽습니다.
peripheral로서 휴대폰이 그날의 측정값을 다운로드하도록 광고합니다.
둘 다 유휴 상태일 때
aioble.stop()을 호출하고 절전 모드로 들어갑니다.
두 개의 작업과 asyncio.Event 를 사용하면 이 순서 제어가 간단합니다:
phone_done = asyncio.Event()
async def serve_phone():
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
async with connection:
await stream_measurements(connection)
phone_done.set()
async def read_strap():
async with await strap.connect() as conn:
await pull_fresh_values(conn)
async def cycle():
await asyncio.gather(read_strap(), serve_phone())
aioble.stop() # radio off until next wake