11.9. Bertindak sebagai periferal¶
Pola BLE yang paling umum di sisi kamera adalah bertindak sebagai peripheral -- mempublikasikan database GATT kecil, mengiklankan keberadaannya, menerima koneksi dari ponsel atau perangkat pendamping, dan mengalirkan nilai ke siapa pun yang terhubung.
11.9.1. Membangun database GATT¶
Hal pertama yang dilakukan periferal saat startup -- bahkan sebelum menghidupkan radio -- adalah membangun database yang akan diekspos, membuat objek untuk setiap layanan dan karakteristik, lalu mendaftarkan semuanya:
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)
Setiap aioble.Characteristic terlampir pada layanannya hanya dengan mengonstruksinya menggunakan layanan sebagai argumen pertama. Argumen kata kunci boolean (read, write, write_no_response, notify, indicate) memilih operasi GATT mana yang boleh dilakukan klien; meneruskan False (default) berarti bit properti tidak disetel.
aioble.register_services() mengkomit pohon yang telah dirakit ke server GATT. Fungsi ini harus dipanggil sekali, sebelum aioble.advertise() dimulai; memanggilnya lagi akan menggantikan database sebelumnya.
11.9.2. Advertising¶
Setelah database siap, advertising adalah satu pemanggilan coroutine yang menunggu koneksi:
async def serve_one():
connection = await aioble.advertise(
interval_us=250000,
name="openmv-env",
services=[ENV_SERVICE],
appearance=0x0540, # Generic Sensor
)
Argumen kata kunci dipetakan langsung ke kolom payload advertising. name adalah kolom local-name; services adalah daftar UUID layanan yang dihosting perangkat (pemindai di sisi ponsel dapat memfilter berdasarkan ini); appearance adalah petunjuk dari nilai appearance 16-bit standar yang memungkinkan central menampilkan ikon yang sesuai. Data khusus produsen dimasukkan melalui manufacturer=(company_id, data_bytes).
Beberapa kata kunci yang jarang digunakan mencakup sisa ruang flag advertising:
connectable=False-- mode siaran saja (tidak ada koneksi yang pernah diterima). Pilihan yang tepat untuk payload gaya beacon.limited_disc=True-- menggunakan flag limited discoverable alih-alih general discoverable; beberapa sistem operasi memperlakukan keduanya secara berbeda dalam UI penyandingan mereka.adv_data/resp_data-- byte mentah jika aplikasi membutuhkan kontrol penuh atas tata letak.timeout_ms-- berhenti setelah waktu tertentu. Defaultnya adalah beriklan selamanya.
Ketika central terhubung, aioble.advertise() mengembalikan aioble.DeviceConnection yang dihasilkan. Periferal berhenti beriklan pada titik ini.
11.9.3. Melayani satu klien¶
Loop utama periferal biasanya terlihat seperti ini:
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 membuat pembersihan pemutusan otomatis. disconnected() adalah coroutine yang menangguhkan eksekusi hingga salah satu pihak mengakhiri koneksi -- cara bersih untuk menjaga periferal tetap melayani hingga central pergi, kemudian kembali beriklan di putaran berikutnya.
11.9.4. Memperbarui karakteristik¶
Periferal memperbarui database GATT lokal dengan aioble.Characteristic.write()
temp_char.write(b"\\x9a\\x09") # 24.58 deg C as sint16, 0.01 units
Hal tersebut mengubah nilai yang akan dikembalikan oleh read berikutnya dari klien mana pun. Dengan sendirinya, ini tidak mendorong nilai baru -- klien yang berlangganan tidak akan melihat apa pun hingga klien melakukan polling atau periferal mengirimkan notifikasi eksplisit.
Sisi push adalah satu kata kunci pada panggilan yang sama:
temp_char.write(temp_bytes, send_update=True)
send_update=True memberi tahu (atau mengindikasikan) setiap klien yang telah berlangganan karakteristik ini. Sebagian besar kode bergaya sensor berada dalam tugas per-koneksi yang berulang membaca sensor dan menulis nilai dengan send_update=True setiap detik atau lebih:
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()
Jika Anda lebih suka mengarahkan notifikasi ke satu klien tertentu daripada seluruh set berlangganan (misalnya respons privat koneksi terhadap perintah klien tersebut), aioble.Characteristic.notify() dan indicate() menerima argumen DeviceConnection dan payload opsional.
11.9.5. Menerima penulisan¶
Arah lainnya -- klien menulis ke karakteristik -- tersedia ketika karakteristik dibuat dengan write=True atau write_no_response=True. Periferal menunggu penulisan berikutnya dengan aioble.Characteristic.written()
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)
Tanpa capture=True, written() hanya mengembalikan koneksi yang menulis; nilai baru berada di buffer pendukung karakteristik dan aplikasi mengambilnya dengan read(). Jika penulisan kedua tiba sebelum aplikasi membaca yang pertama, nilai kedua menimpa nilai pertama di buffer dan nilai aslinya hilang -- written() masih membangunkan aplikasi, tetapi hanya sekali per "ada sesuatu yang baru", bukan sekali per penulisan.
Kata kunci capture=True memperbaiki hal tersebut. Setiap penulisan yang masuk ditambahkan ke antrean seluruh modul, dan written() mengembalikan tuple (connection, data) untuk setiap penulisan individu -- loop aplikasi melihat masing-masing tepat satu kali, dalam urutan kedatangan. Dua konsekuensi praktis:
Antrean dibatasi dan dibagikan di seluruh karakteristik yang mengaktifkan capture pada perangkat. Ledakan singkat penulisan berturut-turut dapat ditoleransi; overrun berkelanjutan (penulisan tiba lebih cepat dari drainase aplikasi) akan secara diam-diam menjatuhkan entri tertua dalam antrean, dan lalu lintas bursty pada satu karakteristik dapat mengusir entri yang tertunda dari karakteristik lain.
Pilih
capture=Trueuntuk penulisan bergaya perintah di mana setiap nilai penting. Biarkan nonaktif untuk karakteristik bergaya status di mana hanya nilai terbaru yang relevan.
Jika pembacaan dari klien harus dijawab oleh kode yang berjalan sesuai permintaan daripada nilai statis, timpa on_read(). Metode ini dipanggil secara sinkron ketika pembacaan masuk; kembalikan 0 untuk mengizinkan pembacaan (nilai saat ini dari write() akan dikirim), atau kode kesalahan ATT non-nol untuk menolaknya:
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)
Callback mengambil sampel sensor dan memperbarui nilai karakteristik tepat sebelum stack GATT melayani pembacaan, sehingga klien selalu melihat data terbaru. Batas laju mencegah klien membebani sensor lebih cepat dari yang bisa diambil sampelnya -- pembacaan apa pun dalam jeda satu detik akan dikembalikan sebagai kesalahan ATT Read Not Permitted daripada nilai basi.
11.9.5.1. Buffer pendukung yang lebih besar -- BufferedCharacteristic¶
Buffer pendukung untuk Characteristic biasa lebarnya 20 byte -- batas praktis pada MTU default 23-byte. Klien yang menulis lebih dari itu ke karakteristik biasa akan mendapatkan nilainya terpotong. Untuk nilai masuk yang lebih besar atau untuk mengantrekan penulisan berturut-turut yang akan dikejar oleh loop aplikasi nantinya, deklarasikan karakteristik sebagai BufferedCharacteristic dan pilih ukuran buffer di awal:
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)
Dua kenop membedakannya dari Characteristic biasa:
max_lenadalah ukuran buffer pendukung dalam byte. Pilih untuk mencocokkan penulisan tunggal terbesar yang diharapkan dilakukan klien (setelah negosiasi MTU).append=Truemembuat penulisan berurutan ditambahkan ke buffer alih-alih menimpa -- berguna untuk menerima nilai yang tiba melalui beberapa penulisan (potongan pembaruan firmware, baris log). Denganappend=Falsebuffer berperilaku seperti karakteristik normal, hanya lebih lebar.
Semua flag konstruktor lainnya (read, write, notify, indicate, capture, initial) diteruskan tidak berubah ke karakteristik yang mendasarinya.
11.9.6. Layanan standar dan UUID yang ditetapkan SIG¶
Menggunakan UUID assigned-numbers (0x180F untuk Battery Service, 0x181A untuk Environmental Sensing, 0x180D untuk Heart Rate, dan sebagainya) berarti menu Bluetooth generik ponsel atau aplikasi pemindai pihak ketiga mana pun dapat mengidentifikasi tujuan perangkat tanpa kode klien kustom apa pun. Tata letak byte di dalam setiap karakteristik standar juga ditetapkan oleh spesifikasi -- Battery Level (0x2A19) adalah satu byte 0..100; Temperature (0x2A6E) adalah sint16 little-endian dalam satuan 0,01 derajat-C. Untuk aplikasi yang tidak sesuai dengan layanan standar, buat UUID 128-bit sekali dan gunakan di seluruh layanan dan karakteristik perangkat.
Periferal yang hanya mempublikasikan UUID kustom tetap baik-baik saja -- hanya perlu aplikasi klien kustom yang mengetahui UUID tersebut.
Catatan
Nilai BLE bersifat little-endian di mana-mana -- spesifikasi GATT, setiap karakteristik standar, setiap kolom advertising. Integer multi-byte dikirim melalui jaringan dengan byte rendah terlebih dahulu. Awalan < dalam string format struct adalah yang Anda inginkan untuk pengodean/dekoding ("<h", "<H", "<I", ...); menggunakan urutan byte native default pada MCU little-endian kebetulan berfungsi untuk saat ini, tetapi menuliskan < secara eksplisit adalah kebiasaan yang aman.
11.9.7. Radio di balik semuanya¶
Radio menyala pada saat coroutine aioble pertama menyentuhnya. Hingga central terhubung, periferal menghabiskan waktunya beralih antara ledakan advertising singkat dan mode tidur; setelah koneksi, ia mengikuti interval koneksi yang dinegosiasikan. Periferal membayar biaya daya kecil per advertisement, sehingga pilihan interval_us pada aioble.advertise() adalah kenop paling langsung yang dimiliki periferal untuk menyeimbangkan latensi penemuan dengan daya tahan baterai.