11.10. Merkez olarak davranmak

Konuşmanın diğer tarafı merkez‘dir – reklam yapan çevre birimlerini tarayan, konuşmak için birini seçen, bir bağlantı açan, uzak GATT veritabanını gezen ve üzerindeki karakteristikleri okuyan veya bunlara abone olan cihaz. Giyilebilir bir sensörden okumalar toplayan, bir beacon dinleyen veya bir eşlik eden mikrodenetleyiciyle konuşan bir kamera bir merkezdir.

aioble içindeki merkez deseni dört aşamadan geçer: tarama, bağlanma, keşif, işleme.

11.10.1. Tarama

aioble.scan(), keşfedilen cihazlar üzerinde bir async yineleyici işlevi de gören bir async bağlam yöneticisi döndürür. Tipik kullanım, ilgilenilen bir cihaz görünene kadar taramak, ardından yinelemeden çıkmaktır:

import aioble
import asyncio
import bluetooth

HR_SERVICE = bluetooth.UUID(0x180D)

async def find_heart_rate():
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if HR_SERVICE in result.services():
                return result.device
    return None

duration_ms=5000, taramanın ne kadar süre çalışacağını sınırlar; duration_ms=0 sonsuza kadar tarar (bağlam yöneticisi çıkana kadar). active=True, her iki taraftan da küçük bir ek iletim pahasına cihaz başına yük boyutunu iki katına çıkaran tarama yanıtlarını ister. Geri kalan interval_us / window_us anahtar sözcük argümanları, tarayıcının kendi radyo görev döngüsünü ayarlar ve varsayılanlardan nadiren değiştirilir.

Her aioble.ScanResult, cihaz adresini, son RSSI değerini, ham reklam ve tarama yanıtı baytlarını ve standart alanları ayrıştıran yardımcıları sunar:

  • result.device – üzerinde connect() çağrılmaya hazır bir aioble.Device.

  • result.rssi – dBm cinsinden alınan sinyal gücü göstergesi, “en yakını seç” mantığı için kullanışlıdır.

  • result.name() – yerel ad dizesi veya reklam yapılmıyorsa None.

  • result.services() – cihazın reklamını yaptığı her hizmet için bir bluetooth.UUID üreteci.

  • result.manufacturer() – üreticiye özgü alanlar için bir (company_id, data) demetleri üreteci.

  • result.connectable – en son reklamın bağlanılabilir olup olmadığı.

Aynı cihaz için yeni reklam verileri geldikçe aynı ScanResult yeniden verilir, dolayısıyla yalnızca cihazları süresiz olarak izlemek isteyen pasif bir dinleyici, async yineleyiciyi sonsuza kadar çalıştırabilir ve her olayda dağıtım yapabilir.

11.10.2. Bağlanma

Bir hedef cihaz tanımlandıktan sonra, bir bağlantı açmak tek bir await‘tir:

async def talk_to(device):
    connection = await device.connect()           # 10 s timeout
    async with connection:
        # ... do GATT work ...
        pass

aioble.Device.connect(), timeout_ms (bağlantının kurulmasını ne kadar bekleyeceği; varsayılan 10 sn) ve min_conn_interval_us / max_conn_interval_us (Bağlantılar belgesindeki istenen bağlantı aralığı) alır.

11.10.2.1. Tarama yapmadan bilinen bir eşe yeniden bağlanma

Bir eşle bir bağ bir kez var olduğunda, adres zaten bilinmektedir ve başka bir tara-ve-seç turu boşa harcanan radyo zamanıdır. Kaydedilmiş adresle doğrudan bir aioble.Device oluşturun ve doğrudan connect() adımına atlayın:

import aioble

KITCHEN_CAM = aioble.Device(aioble.ADDR_PUBLIC,
                            "aa:bb:cc:dd:ee:ff")

async def talk_to_kitchen():
    async with await KITCHEN_CAM.connect() as connection:
        # ... GATT work ...
        pass

İlk argüman aioble.ADDR_PUBLIC (bir denetleyicinin fabrika adresi) veya aioble.ADDR_RANDOM (oluşturulmuş statik veya çözülebilir özel bir adres) değerlerinden biridir; ikincisi ya altı baytlık bir bytes değeri ya da iki nokta üst üste ile ayrılmış onaltılık bir dizedir. Herhangi bir Device cihazının (örneğin daha önce bir ScanResult üzerinden elde edilen biri) addr_type ve addr öznitelikleri kalıcı hale getirilip buraya geri beslenebilir.

Döndürülen aioble.DeviceConnection, merkezin geri kalan işinin bağlı olduğu şeydir. async with, blok çıktığında – başarıda, iptalde veya eşin ayrılmasından kaynaklanan aioble.DeviceDisconnectedError dahil herhangi bir istisnada – bağlantının kapatılmasını sağlar.

Merkezin, varsayılan 23 baytlık MTU’nun izin verdiğinden daha büyük bir karakteristik değerine ihtiyacı varsa, bunu pazarlık etmenin yeri burasıdır:

await connection.exchange_mtu(512)

(exchange_mtu(), gerçekte pazarlık edilen MTU’yu döndürür; bu, istenen değerin ve eşin desteklediğinin en küçüğüdür.)

11.10.3. Keşif

Keşif, hizmetleri ve karakteristikleri UUID’lerine göre bulmak için uzak GATT veritabanını gezer. İki türü vardır: hedefli (UUID’yi biliyorsunuz ve belirli bir şey istiyorsunuz) ve kapsamlı (her şeyi istiyorsunuz).

Hedefli – yaygın durum:

service = await connection.service(HR_SERVICE)
if service is None:
    return                                        # no such service

char = await service.characteristic(HR_MEASUREMENT)
if char is None:
    return                                        # no such characteristic

aioble.DeviceConnection.service() ve aioble.ClientService.characteristic(), her biri bir bluetooth.UUID alır ve eşleşen nesneyi (veya None) döndürür. Her ikisinin de varsayılanı 2 sn olan keşif başına bir timeout_ms anahtar sözcüğü vardır.

Kapsamlı:

async for service in connection.services():
    print("service:", service.uuid)
    async for char in service.characteristics():
        print("  characteristic:", char.uuid, "properties:", hex(char.properties))

Genel Bluetooth gezgini uygulamalarının yaptığı budur – geliştirme için kullanışlıdır, hangi UUID’leri beklediğini bilen üretim kodu için ise daha azdır.

11.10.3.1. Bir karakteristiğin neyi desteklediğini inceleme

Keşif, her karakteristik için eşin reklamını yaptığı GATT özellik bit maskesini properties olarak döndürür. Bitler GATT tarafından tanımlanmış olanlardır – okuma (0x02), yanıtsız yazma (0x04), yazma (0x08), bildirim (0x10), gösterge (0x20) ve benzerleri. Bir işlem yayınlamadan önce bit maskesini incelemek, genel bir istemcinin yeteneklerini önceden bilmediği karakteristiklere uyum sağlamasına olanak tanır:

_PROP_READ = const(0x02)
_PROP_NOTIFY = const(0x10)

char = await service.characteristic(STATUS_UUID)
if char.properties & _PROP_NOTIFY:
    await char.subscribe(notify=True)
    value = await char.notified()
elif char.properties & _PROP_READ:
    value = await char.read()
else:
    value = None                                  # nothing the client can do

Eşin GATT profilini zaten bilen üretim kodunun genellikle buna ihtiyacı yoktur – UUID’ler baştan belgelenmiştir. Genel / keşif amaçlı istemciler (bilinmeyen bir cihazı gezen bir ayarlar sayfası, bir eklenti ana bilgisayarı) buna dayanır.

11.10.4. İşleme

Merkez bir ClientCharacteristic tuttuğunda, her GATT işlemi tek bir coroutine çağrısıdır:

  • Okuma. Bir GATT okuması yayınlayın ve değeri geri alın:

    value = await char.read()
    print("value:", value)
    

    Uzun okumalar (MTU’dan büyük değerler) şeffaf bir şekilde işlenir.

  • Yazma. Sunucuya yeni bir değer gönderin:

    await char.write(b"\\x01")
    

    response=True bir yazma yanıtı bekler ve sunucu yazmayı reddederse aioble.GattError fırlatır. response=False yanıtsız yazmadır: gönder-ve-unut. response=None (varsayılan), eşin reklamını yaptığına göre otomatik seçim yapar.

  • Abone olma. Karakteristiğin CCCD’sine yazarak bildirimleri veya göstergeleri etkinleştirin:

    await char.subscribe(notify=True)
    

    Bu döndükten sonra, merkez gelen göndermeleri bekleyebilir.

  • Bildirilen / gösterilen. Sunucudan bir sonraki göndermeyi bekleyin:

    while True:
        data = await char.notified()
        print("push:", data)
    

    timeout_ms=None (varsayılan) sonsuza kadar bekler; bir süre sonra vazgeçmek için milisaniye cinsinden bir tam sayı geçirin.

Dördünü bir araya getirmek, kanonik “bağlan, abone ol, akıt” merkez programını verir:

async def stream_heart_rate():
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if HR_SERVICE in result.services():
                device = result.device
                break
        else:
            return

    async with await device.connect() as connection:
        service = await connection.service(HR_SERVICE)
        char = await service.characteristic(HR_MEASUREMENT)
        await char.subscribe(notify=True)
        while connection.is_connected():
            data = await char.notified()
            print("hr push:", data)

asyncio.run(stream_heart_rate())

Bütünü yaklaşık bir düzine satırdır ve “hiç Bluetooth çalışmıyor” durumundan “canlı veri akışı” durumuna kadar olan akışı kapsar. Tarama yineleyicisi yayıncı/gözlemci deseniyle eşleşir, connect GAP bağlantısını açar, service / characteristic GATT ağacını gezer, subscribe CCCD’ye yazar ve notified göndermeleri bekler.

11.10.5. Bağlantı kesilmeleri ve yeniden bağlanma

Radyo bağlantısında olan her şey, onu bekleyen coroutine’de yüzeye çıkar. aioble.DeviceDisconnectedError, eşin ayrıldığının veya denetim zaman aşımının tetiklendiğinin sinyalidir; istisna, devam etmekte olan herhangi bir read(), write() veya notified() çağrısını sonlandırır ve her async with connection bloğu temiz bir şekilde çıkar.

Kayıpta yeniden bağlanması gereken bir merkez, işi kendi dış döngüsüne sarar:

async def keep_streaming():
    while True:
        try:
            await stream_heart_rate()
        except aioble.DeviceDisconnectedError:
            print("disconnected, retrying...")
            await asyncio.sleep(2)

11.10.5.1. Bir diziyi timeout() ile çerçeveleme

Art arda gelen birkaç GATT işleminin – her biri kendi timeout_ms değerinde tek tek değil de – hepsinin tek bir bütçe içinde tamamlanması gerektiğinde, bunları sarmak için aioble.DeviceConnection.timeout() kullanın. Döndürülen bağlam yöneticisi, bütçe dolarsa (asyncio.TimeoutError fırlatarak) veya eş bağlantıyı keserse (aioble.DeviceDisconnectedError fırlatarak) gövdesini iptal eder:

async with await device.connect() as connection:
    try:
        with connection.timeout(2000):                    # 2 s for the whole block
            service = await connection.service(HR_SERVICE)
            char = await service.characteristic(HR_MEASUREMENT)
            await char.subscribe(notify=True)
    except asyncio.TimeoutError:
        print("discovery + subscribe took too long")

Bu, her çağrıyı tek tek asyncio.wait_for() ile sarmaya göre daha temiz bir alternatiftir ve her çağrının kendi son tarihini karşıladığı ancak bir bütün olarak dizinin süreyi aştığı sahte başarıları önler. timeout() işlevine timeout_ms=None geçirmek son tarihi devre dışı bırakır ve yalnızca bağlantı kesme korumasını etkin bırakır.