11.9. Çevre birimi olarak davranma

En yaygın kamera tarafı BLE deseni bir çevre birimi gibi davranmaktır – küçük bir GATT veritabanı yayımlamak, varlığını ilan etmek (advertise), bir telefondan veya yardımcı bir cihazdan gelen bağlantıyı kabul etmek ve karşı uçtaki kim olursa olsun ona değerler aktarmak.

11.9.1. GATT veritabanını oluşturma

Bir çevre biriminin başlangıçta yaptığı ilk şey – radyoyu açmadan bile önce – açığa çıkarmayı planladığı veritabanını oluşturmak, her hizmet (service) ve karakteristik için nesneler kurmak, ardından tümünü kaydetmektir:

import aioble
import bluetooth

ENV_SERVICE = bluetooth.UUID(0x181A)              # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)                # Temperature
HUMID_UUID = bluetooth.UUID(0x2A6F)               # Humidity

env = aioble.Service(ENV_SERVICE)
temp_char = aioble.Characteristic(
    env, TEMP_UUID,
    read=True, notify=True, initial=b"\\x00\\x00",
)
humid_char = aioble.Characteristic(
    env, HUMID_UUID,
    read=True, notify=True, initial=b"\\x00\\x00",
)

aioble.register_services(env)

Her aioble.Characteristic, yalnızca hizmeti ilk argüman olarak vererek kurularak hizmetine bağlanır. Boolean anahtar sözcük argümanları (read, write, write_no_response, notify, indicate) istemcinin hangi GATT işlemlerini gerçekleştirmesine izin verileceğini seçer; False (varsayılan) geçilmesi ilgili özellik bitinin ayarlanmadığı anlamına gelir.

aioble.register_services() bir araya getirilen ağacı GATT sunucusuna işler. Herhangi bir aioble.advertise() başlamadan önce bir kez çağrılmalıdır; tekrar çağrılması önceki veritabanını değiştirir.

11.9.2. Yayımlama (Advertising)

Veritabanı yerine oturduğunda, yayımlama bir bağlantıyı bekleyen tek bir coroutine çağrısıdır:

async def serve_one():
    connection = await aioble.advertise(
        interval_us=250000,
        name="openmv-env",
        services=[ENV_SERVICE],
        appearance=0x0540,           # Generic Sensor
    )

Anahtar sözcük argümanları doğrudan yayımlama yükü (payload) alanlarına eşlenir. name yerel ad alanıdır; services cihazın barındırdığı hizmet UUID’lerinin listesidir (telefon tarafındaki bir tarayıcı bunlara göre filtreleyebilir); appearance standart 16-bit görünüm değerlerinden bir ipucudur ve merkezin (central) makul bir simge göstermesini sağlar. Üreticiye özgü veriler manufacturer=(company_id, data_bytes) aracılığıyla girer.

Daha az yaygın birkaç anahtar sözcük, geri kalan yayımlama bayrağı alanını kapsar:

  • connectable=False – yalnızca yayın modu (hiçbir zaman bağlantı kabul edilmez). Işaret (beacon) tarzı yükler için doğru seçim.

  • limited_disc=Truegeneral discoverable yerine limited discoverable bayrağını kullan; bazı işletim sistemleri ikisini eşleştirme (pairing) arayüzlerinde farklı şekilde ele alır.

  • adv_data / resp_data – uygulamanın yerleşim üzerinde tam denetime ihtiyacı varsa ham baytlar.

  • timeout_ms – sabit bir süre sonra vazgeç. Varsayılan, sonsuza dek yayımlamaktır.

Bir merkez bağlandığında, aioble.advertise() ortaya çıkan aioble.DeviceConnection nesnesini döndürür. Çevre birimi bu noktada yayımlamayı durdurur.

11.9.3. Tek bir istemciye hizmet verme

Bir çevre biriminin ana döngüsü genellikle şöyle görünür:

async def serve():
    while True:
        connection = await aioble.advertise(
            interval_us=250000,
            name="openmv-env",
            services=[ENV_SERVICE],
        )
        print("connected:", connection.device.addr_hex())
        async with connection:
            await connection.disconnected()
        print("disconnected; advertising again")

asyncio.run(serve())

async with connection bağlantı kesme temizliğini otomatik hale getirir. disconnected(), taraflardan biri bağlantıyı sonlandırana kadar askıya alan bir coroutine’dir – çevre birimini merkez gidene kadar hizmet verir durumda tutmanın, ardından bir sonraki tur için yayımlamaya geri dönmenin temiz bir yoludur.

11.9.4. Bir karakteristiği güncelleme

Çevre birimi yerel GATT veritabanını aioble.Characteristic.write() ile günceller:

temp_char.write(b"\\x9a\\x09")              # 24.58 deg C as sint16, 0.01 units

Bu, herhangi bir istemciden gelecek bir sonraki read işleminin döndüreceği değeri değiştirir. Kendi başına, yeni değeri itmez (push) – abone olmuş bir istemci, istemci yoklama yapana veya çevre birimi açık bir bildirim (notification) gönderene kadar hiçbir şey görmeyecektir.

İtme tarafı, aynı çağrıdaki tek bir anahtar sözcüktür:

temp_char.write(temp_bytes, send_update=True)

send_update=True, bu karakteristiğe abone olmuş her istemciyi bildirir (veya gösterir). Çoğu sensör tarzı kod, sensörü okuyup değeri her saniye kadar send_update=True ile yazan bir döngüye sahip, bağlantı başına bir görevde yaşar:

async def stream_temperature(connection):
    while connection.is_connected():
        temp_char.write(encode_temperature(read_sensor()), send_update=True)
        await asyncio.sleep(1)

async def serve():
    while True:
        connection = await aioble.advertise(
            interval_us=250000,
            name="openmv-env",
            services=[ENV_SERVICE],
        )
        async with connection:
            asyncio.create_task(stream_temperature(connection))
            await connection.disconnected()

Bir bildirimi tüm abone kümesi yerine belirli bir istemciye yönlendirmeyi tercih ederseniz (örneğin o istemcinin komutuna bağlantıya özel bir yanıt), aioble.Characteristic.notify() ve indicate() bir DeviceConnection argümanı ve isteğe bağlı bir yük alır.

11.9.5. Yazmaları alma

Diğer yön – bir istemcinin bir karakteristiğe yazması – karakteristik write=True veya write_no_response=True ile kurulduğunda kullanılabilir hale gelir. Çevre birimi bir sonraki yazmayı aioble.Characteristic.written() ile bekler:

cmd_char = aioble.Characteristic(env, CMD_UUID, write=True, capture=True)

async def handle_commands():
    while True:
        connection, data = await cmd_char.written()
        print("command from", connection.device.addr_hex(), "=", data)

capture=True olmadan, written() yalnızca yazan bağlantıyı döndürür; yeni değer karakteristiğin destek arabelleğinde yaşar ve uygulama bunu read() ile getirir. Uygulama ilkini okumadan önce ikinci bir yazma gelirse, ikinci değer arabellekte ilkinin üzerine yazar ve özgün değer kaybolur – written() yine de uygulamayı uyandırır, ancak yazma başına bir kez değil, yalnızca “yeni bir şey var” başına bir kez.

capture=True anahtar sözcüğü bunu düzeltir. Gelen her yazma modül genelinde bir kuyruğa eklenir ve written() her bir tekil yazma için bir (connection, data) demeti döndürür – uygulama döngüsü her birini tam olarak bir kez, varış sırasında görür. İki pratik sonuç:

  • Kuyruk sınırlıdır ve cihazdaki yakalama (capture) etkin her karakteristik arasında paylaşılır. Kısa, art arda gelen yazma patlamalarına tolerans gösterilir; sürekli taşma (uygulamanın boşalttığından daha hızlı gelen yazmalar) sessizce kuyruktaki en eski girdileri düşürür ve bir karakteristikteki patlama tipi trafik, başka bir karakteristiğin bekleyen girdilerini dışarı atabilir.

  • Her değerin önemli olduğu komut tarzı yazmalar için capture=True seçin. Yalnızca en son değerin ilgi konusu olduğu durum tarzı karakteristikler için kapalı bırakın.

İstemciden gelen bir okumanın statik bir değer yerine talep üzerine çalışan kodla yanıtlanması gerekiyorsa, on_read() metodunu geçersiz kılın. Metot, bir okuma geldiğinde eşzamanlı olarak çağrılır; okumaya izin vermek için 0 döndürün (write() ile ayarlanan geçerli değer gönderilecektir) veya reddetmek için sıfır olmayan bir ATT hata kodu döndürün:

import time

_ATT_ERR_READ_NOT_PERMITTED = const(0x02)
_MIN_READ_INTERVAL_MS = const(1000)            # at most once per second

class TempChar(aioble.Characteristic):
    _last_read_ms = 0

    def on_read(self, connection):
        now = time.ticks_ms()
        if time.ticks_diff(now, self._last_read_ms) < _MIN_READ_INTERVAL_MS:
            return _ATT_ERR_READ_NOT_PERMITTED
        self._last_read_ms = now
        self.write(encode_temperature(read_sensor()))
        return 0

temp_char = TempChar(env, TEMP_UUID, read=True)

Geri çağırma (callback), GATT yığını okumaya hizmet etmeden hemen önce sensörü örnekler ve karakteristiğin değerini günceller, böylece istemci her zaman taze veri görür. Hız sınırı, bir istemcinin sensörü örneklenebileceğinden daha hızlı zorlamasını engeller – bir saniyelik bekleme süresi içindeki herhangi bir okuma, eski bir değer yerine Read Not Permitted ATT hatası olarak geri çevrilir.

11.9.5.1. Daha büyük destek arabellekleri – BufferedCharacteristic

Sıradan bir Characteristic için destek arabelleği 20 bayt genişliğindedir – varsayılan 23 baytlık MTU’daki pratik sınır. Sıradan bir karakteristiğe bundan fazlasını yazan bir istemcinin değeri kesilir. Daha büyük gelen değerler için veya uygulama döngüsünün daha sonra yetişeceği art arda yazmaları kuyruğa almak için, karakteristiği BufferedCharacteristic olarak bildirin ve arabellek boyutunu önceden seçin:

blob = aioble.BufferedCharacteristic(
    service, BLOB_UUID,
    max_len=512, append=True,
    write=True, capture=True,
)

async def receive_blob():
    while True:
        connection, chunk = await blob.written()
        handle_chunk(connection, chunk)

İki ayar onu sade bir Characteristic‘ten ayırır:

  • max_len destek arabelleğinin bayt cinsinden boyutudur. İstemcinin yapması beklenen en büyük tek yazmayla (MTU müzakeresinden sonra) eşleşecek şekilde seçin.

  • append=True, ardışık yazmaların üzerine yazmak yerine arabelleğe eklenmesini sağlar – birkaç yazma boyunca gelen bir değeri (aygıt yazılımı güncelleme parçaları, log satırları) almak için yararlıdır. append=False ile arabellek normal bir karakteristik gibi davranır, yalnızca daha geniştir.

Diğer tüm kurucu bayrakları (read, write, notify, indicate, capture, initial) değişmeden altta yatan karakteristiğe iletilir.

11.9.6. Standart hizmetler ve SIG tarafından atanan UUID’ler

Atanmış numaralar UUID’lerine bağlı kalmak (Battery Service için 0x180F, Environmental Sensing için 0x181A, Heart Rate için 0x180D ve benzeri), bir telefonun genel Bluetooth menüsünün veya herhangi bir üçüncü taraf tarayıcı uygulamasının cihazın amacını herhangi bir özel istemci kodu olmadan tanıyabilmesi anlamına gelir. Her standart karakteristik içindeki bayt yerleşimi de spesifikasyon tarafından sabitlenir – Battery Level (0x2A19) 0..100 aralığında tek bir bayttır; Temperature (0x2A6E) 0.01 derece-C birimlerinde küçük-endian (little-endian) sint16’dır. Standart bir hizmete uymayan uygulamalar için, bir kez 128-bit UUID üretin ve onu cihazın hizmetleri ile karakteristikleri arasında kullanın.

Yalnızca özel UUID’ler yayımlayan bir çevre birimi yine de sorunsuzdur – yalnızca bu UUID’leri bilen özel bir istemci uygulaması gerektirir.

Not

BLE değerleri her yerde küçük-endian’dır (little-endian) – GATT spesifikasyonu, her standart karakteristik, her yayımlama alanı. Çok baytlı tam sayılar telde önce düşük bayt gelecek şekilde gider. struct biçim dizelerindeki < öneki, kodlama/kod çözme için istediğiniz şeydir ("<h", "<H", "<I", …); küçük-endian bir MCU’da varsayılan yerel bayt sırasını kullanmak şimdilik işe yarıyor, ancak < ifadesini açıkça yazmak güvenli alışkanlıktır.

11.9.7. Tüm bunların arkasındaki radyo

Radyo, ilk aioble coroutine’i ona dokunduğu anda açıktır. Bir merkez bağlanana kadar çevre birimi zamanını kısa yayımlama patlamaları ile uyku arasında geçiş yaparak geçirir; bir bağlantıdan sonra müzakere edilen bağlantı aralığını izler. Çevre birimi her yayımlama başına küçük bir güç bedeli öder, dolayısıyla aioble.advertise() üzerindeki interval_us seçimi, bir çevre biriminin keşif gecikmesini pil ömrüyle takas etmek için sahip olduğu en doğrudan ayardır.