11.10. Bertindak sebagai central

Sisi lain dari percakapan ini adalah central -- perangkat yang memindai periferal yang sedang mengiklan, memilih satu untuk diajak berkomunikasi, membuka koneksi, menelusuri database GATT jarak jauh, serta membaca atau berlangganan karakteristik di dalamnya. Sebuah kamera yang mengumpulkan pembacaan dari sensor yang dapat dipakai, mendengarkan beacon, atau berkomunikasi dengan mikrokontroler pendamping adalah sebuah central.

Pola central dalam aioble berjalan melalui empat tahap: scan, connect, discover, operate.

11.10.1. Pemindaian

aioble.scan() mengembalikan sebuah async context manager yang sekaligus berfungsi sebagai async iterator atas perangkat yang ditemukan. Penggunaan umum adalah memindai hingga perangkat yang diinginkan muncul, lalu keluar dari iterasi:

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 membatasi berapa lama pemindaian berjalan; duration_ms=0 memindai selamanya (hingga context manager keluar). active=True meminta scan response, yang menggandakan ukuran payload per perangkat dengan biaya transmisi tambahan kecil dari kedua sisi. Argumen kata kunci interval_us / window_us yang tersisa menyetel duty cycle radio scanner itu sendiri dan jarang diubah dari nilai defaultnya.

Setiap aioble.ScanResult mengekspos alamat perangkat, RSSI terakhir, byte iklan mentah dan scan response, serta pembantu yang mengurai bidang standar:

  • result.device -- sebuah aioble.Device yang siap untuk dipanggil connect() padanya.

  • result.rssi -- indikator kekuatan sinyal yang diterima dalam dBm, berguna untuk logika "pilih yang terdekat".

  • result.name() -- string nama lokal, atau None jika tidak diiklankan.

  • result.services() -- sebuah generator dari bluetooth.UUID untuk setiap layanan yang diiklankan perangkat.

  • result.manufacturer() -- sebuah generator dari tuple (company_id, data) untuk bidang-bidang spesifik produsen.

  • result.connectable -- apakah iklan terbaru adalah iklan yang dapat dihubungkan.

ScanResult yang sama dihasilkan ulang saat data iklan baru tiba untuk perangkat yang sama, sehingga pendengar pasif yang hanya ingin melacak perangkat tanpa batas dapat menjalankan async iterator selamanya dan menangani setiap peristiwa.

11.10.2. Menghubungkan

Setelah perangkat target diidentifikasi, membuka koneksi hanya membutuhkan satu await

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

aioble.Device.connect() menerima timeout_ms (berapa lama menunggu koneksi terbentuk; default 10 detik), serta min_conn_interval_us / max_conn_interval_us (rentang interval koneksi yang diminta dari Koneksi).

11.10.2.1. Menghubungkan ulang ke peer yang dikenal tanpa pemindaian

Setelah ikatan ada dengan peer, alamat sudah diketahui dan putaran scan-and-pick berikutnya membuang waktu radio. Buat aioble.Device secara langsung dengan alamat yang tersimpan dan langsung lanjutkan ke connect()

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

Argumen pertama adalah salah satu dari aioble.ADDR_PUBLIC (alamat pabrik controller) atau aioble.ADDR_RANDOM (alamat privat statis atau resolvable yang dibuat); argumen kedua adalah nilai bytes enam-byte atau string heks yang dipisahkan titik dua. Atribut addr_type dan addr dari Device mana pun (misalnya yang diperoleh sebelumnya dari ScanResult) dapat disimpan dan dimasukkan kembali di sini.

aioble.DeviceConnection yang dikembalikan adalah tempat bergantungnya sisa pekerjaan central. async with memastikan koneksi ditutup saat blok keluar -- saat sukses, saat dibatalkan, atau pada pengecualian apa pun termasuk aioble.DeviceDisconnectedError dari peer yang pergi.

Jika central membutuhkan nilai karakteristik yang lebih besar dari yang diizinkan MTU default 23-byte, di sinilah tempat untuk menegosiasikannya:

await connection.exchange_mtu(512)

(exchange_mtu() mengembalikan MTU yang benar-benar dinegosiasikan, yaitu nilai minimum dari nilai yang diminta dan yang didukung peer.)

11.10.3. Discovery

Discovery menelusuri database GATT jarak jauh untuk menemukan layanan dan karakteristik berdasarkan UUID-nya. Ada dua varian: bertarget (Anda mengetahui UUID dan menginginkan satu hal tertentu) dan menyeluruh (Anda menginginkan semuanya).

Bertarget -- kasus umum:

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() dan aioble.ClientService.characteristic() masing-masing menerima bluetooth.UUID dan mengembalikan objek yang cocok (atau None). Keduanya memiliki kata kunci timeout_ms per-discovery yang defaultnya 2 detik.

Menyeluruh:

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

Inilah yang dilakukan aplikasi penjelajah Bluetooth generik -- berguna untuk pengembangan, kurang berguna untuk kode produksi yang sudah mengetahui UUID yang diharapkan.

11.10.3.1. Memeriksa apa yang didukung sebuah karakteristik

Discovery mengembalikan bitmask properti GATT yang diiklankan peer untuk setiap karakteristik sebagai properties. Bit-nya adalah yang didefinisikan oleh GATT -- read (0x02), write-without-response (0x04), write (0x08), notify (0x10), indicate (0x20), dan lainnya. Memeriksa bitmask sebelum mengeluarkan operasi memungkinkan klien generik beradaptasi dengan karakteristik yang kemampuannya tidak diketahui sebelumnya:

_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

Kode produksi yang sudah mengetahui profil GATT peer biasanya tidak memerlukan ini -- UUID sudah didokumentasikan sejak awal. Klien generik/eksploratif (halaman pengaturan yang menelusuri perangkat yang tidak dikenal, host plugin) mengandalkannya.

11.10.4. Mengoperasikan

Setelah central memegang ClientCharacteristic, setiap operasi GATT adalah satu panggilan coroutine:

  • Read. Kirimkan GATT read dan dapatkan kembali nilainya:

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

    Long read (nilai lebih besar dari MTU) ditangani secara transparan.

  • Write. Kirim nilai baru ke server:

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

    response=True menunggu write-response dan melempar aioble.GattError jika server menolak penulisan. response=False adalah write-without-response: kirim dan lupakan. response=None (default) memilih secara otomatis berdasarkan apa yang diiklankan peer.

  • Subscribe. Aktifkan notifikasi atau indikasi dengan menulis ke CCCD karakteristik:

    await char.subscribe(notify=True)
    

    Setelah ini kembali, central dapat menunggu push masuk.

  • Notified / indicated. Tunggu push berikutnya dari server:

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

    timeout_ms=None (default) menunggu selamanya; masukkan integer dalam milidetik untuk menyerah setelah beberapa saat.

Menggabungkan keempat operasi memberikan program central "connect, subscribe, stream" yang kanonik:

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())

Keseluruhannya hanya sekitar selusin baris dan mencakup alur dari "tidak ada Bluetooth berjalan" hingga "streaming data langsung". Iterator scan cocok dengan pola broadcaster/observer, connect membuka koneksi GAP, service / characteristic menelusuri pohon GATT, subscribe menulis CCCD, dan notified menunggu push.

11.10.5. Pemutusan koneksi dan reconnection

Apapun yang terjadi pada tautan radio akan muncul di coroutine yang sedang menunggu padanya. aioble.DeviceDisconnectedError adalah sinyal bahwa peer pergi atau supervision timeout terpicu; pengecualian tersebut mengakhiri panggilan read(), write(), atau notified() yang sedang berlangsung, dan blok async with connection mana pun keluar dengan bersih.

Central yang harus melakukan reconnect saat putus membungkus pekerjaan dalam loop luar sendiri:

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. Membingkai urutan dengan timeout()

Ketika beberapa operasi GATT berturut-turut seharusnya selesai dalam satu anggaran -- bukan masing-masing secara individual dengan timeout_ms-nya sendiri -- gunakan aioble.DeviceConnection.timeout() untuk membungkusnya. Context manager yang dikembalikan membatalkan isinya jika anggaran habis (melempar asyncio.TimeoutError) atau jika peer terputus (melempar aioble.DeviceDisconnectedError):

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")

Ini adalah alternatif yang lebih bersih daripada membungkus setiap panggilan secara individual dalam asyncio.wait_for() dan menghindari keberhasilan palsu di mana setiap panggilan memenuhi deadline-nya sendiri tetapi urutan secara keseluruhan melebihi batas. Mengoper timeout_ms=None ke timeout() menonaktifkan deadline dan hanya membiarkan disconnect guard aktif.