11.8. โมดูล aioble

ข้อกำหนด Bluetooth Core มีคำศัพท์ที่แมปกับโมดูล MicroPython สองโมดูล

  • bluetooth -- การเชื่อมโยง ระดับต่ำ กับ BLE controller ทำงานแบบซิงโครนัส ขับเคลื่อนด้วยอีเวนต์ผ่านคอลแบ็กสไตล์ IRQ และมีโครงสร้างรอบบัฟเฟอร์ไบต์, handle, และ GATT primitives พื้นฐาน มันเปิดเผยโปรโตคอลตามที่เป็น ไม่ใช่ตามที่แอปพลิเคชัน Python ต้องการใช้

  • aioble -- wrapper ระดับสูงกว่า เขียนด้วย Python บน bluetooth ที่แปลงทุกการดำเนินการระยะไกลให้เป็น coroutine ของ asyncio และทุก BLE object (services, characteristics, connections, scan results, L2CAP channels) ให้เป็น Python class ที่ใช้งานง่าย การสแกนกลายเป็น async iterators, การเชื่อมต่อกลายเป็น async context managers, การแจ้งเตือนกลายเป็น awaitable

11.8.1. เมื่อใดควรใช้โมดูลระดับต่ำ

bluetooth ยังคงเป็นคำตอบที่ถูกต้องสำหรับสองกรณีที่แคบ:

  • คุณกำลังเขียนโค้ดประเภทที่ aioble ถูกสร้างขึ้นมา ซึ่งเป็นรูปแบบใหม่ที่ต้องการการควบคุมระดับ IRQ เหนือโปรโตคอล

  • คุณกำลังทำงานบน hardware target ที่ไม่มีแพ็กเกจ aioble และ shim บางๆ รอบ controller เป็นตัวเลือกเดียว

สำหรับแอปพลิเคชัน camera ทุกตัว aioble คือคำตอบที่ถูกต้อง

11.8.2. ส่วนประกอบของโปรแกรม aioble

แอปพลิเคชันที่ใช้ aioble ทุกตัวมีชิ้นส่วนที่เคลื่อนไหวชุดเล็กๆ โดยไม่คำนึงถึงบทบาทที่เล่น

  • event loop ของ asyncio ที่ทำงานต่อเนื่อง ทุกอย่างใน aioble เป็น coroutine ดังนั้นแอปพลิเคชันจึงมีโครงสร้างเป็น task หนึ่งตัวหรือมากกว่าบน event loop เดียว ดู Asyncio สำหรับรายละเอียดเกี่ยวกับ loop, tasks, และ exceptions

  • วิทยุที่เปิดอยู่ aioble จะเปิดใช้งาน BLE radio โดยอัตโนมัติเมื่อใช้งานครั้งแรก แต่ยังสามารถควบคุมได้อย่างชัดเจนด้วย aioble.config() (ซึ่งส่งต่อไปยัง bluetooth.BLE.config() หลังจากตรวจสอบว่าวิทยุทำงานอยู่) และปิดด้วย aioble.stop()

  • บทบาท หนึ่งตัวหรือมากกว่าที่กำลังทำงานพร้อมกัน ฝั่ง peripheral: ชุด GATT services ที่ลงทะเบียนแล้ว (ดู aioble.register_services()) และ coroutine aioble.advertise() ที่กำลังทำงาน ฝั่ง central: iterator aioble.scan() ที่กำลังทำงานหรือ aioble.Device.connect() ที่รอดำเนินการ วิทยุจะมัลติเพล็กซ์งาน โดยแอปพลิเคชันมองเห็นแต่ละบทบาทเป็น task อิสระ

11.8.3. peripheral ขั้นต่ำ

โปรแกรม aioble ที่เล็กที่สุดที่มีประโยชน์ ซึ่งเป็น peripheral ที่โฆษณา characteristic แบบอ่านอย่างเดียวหนึ่งตัว มีความสั้นกระชับ:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)            # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)               # Temperature

service = aioble.Service(SERVICE_UUID)
temp = aioble.Characteristic(service, TEMP_UUID, read=True)
aioble.register_services(service)

async def main():
    while True:
        conn = await aioble.advertise(
            interval_us=250000,
            name="openmv-temp",
            services=[SERVICE_UUID],
        )
        async with conn:
            await conn.disconnected()

asyncio.run(main())

central ที่ทำเพียงแค่เชื่อมต่อและอ่านครั้งเดียวก็สั้นในลักษณะเดียวกัน:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)
TEMP_UUID = bluetooth.UUID(0x2A6E)

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

    async with await device.connect() as conn:
        service = await conn.service(SERVICE_UUID)
        char = await service.characteristic(TEMP_UUID)
        print(await char.read())

asyncio.run(main())

ทั้งสองโปรแกรมมีประมาณสิบห้าบรรทัดและครอบคลุมขั้นตอนทั้งหมดตั้งแต่ "วิทยุปิดอยู่" ถึง "งานที่มีประโยชน์เสร็จสิ้น"

11.8.4. การปิดวิทยุ

บน camera ที่ใช้แบตเตอรี่ BLE radio คือสิ่งที่ใช้พลังงานมากที่สุดในงบประมาณที่เลือกได้ มีปุ่มควบคุมสองตัวที่สำคัญ

ตัวแรกเป็น แบบอัตโนมัติ: aioble จะเปิดใช้งาน radio เมื่อใช้งานครั้งแรก และ radio จะเข้าสู่โหมดพักระหว่างอีเวนต์ที่กำหนดเวลาไว้ (ชุดโฆษณา, อีเวนต์การเชื่อมต่อ, หน้าต่างสแกน) โดยอัตโนมัติ การเลือกช่วงเวลาที่ยาวขึ้นสำหรับ aioble.advertise() / aioble.scan() และการตกลงช่วงเวลาการเชื่อมต่อที่ยาวขึ้นที่ connect() จะช่วยให้ radio ปิดอยู่นานขึ้นตามสัดส่วน ตารางโฆษณาใน การโฆษณาและการสแกน เป็นแนวทางปฏิบัติที่นี่

ตัวที่สองคือการปิดแบบ ชัดเจน

import aioble

await do_burst_of_ble_work()
aioble.stop()                             # radio deactivated; in-flight tasks unwound
await asyncio.sleep(60)                   # sleep with the radio off
# ... next aioble call brings the radio back up automatically

aioble.stop() จะปิดใช้งาน BLE radio พื้นฐานและยกเลิกทุกสิ่งที่กำลังดำเนินการ การเชื่อมต่อที่เปิดอยู่จะถูกตัด, นักสแกนและผู้โฆษณาจะถูกยกเลิก, ช่องสัญญาณ L2CAP จะถูกปิด Coroutines ที่กำลังรอการดำเนินการเหล่านั้นจะ raise exceptions ตามปกติ (DeviceDisconnectedError และเพื่อนๆ) ซึ่งเป็นกลไกการล้างข้อมูลที่ block async with โดยรอบถูกเขียนขึ้นมาเพื่อจุดประสงค์นี้ การเรียก coroutine aioble ใดๆ หลังจากนั้นจะเปิดใช้งาน radio อีกครั้งจากสถานะเย็น

รูปแบบทั่วไปสำหรับ sensor cam ที่ใช้แบตเตอรี่แบบเป็นระยะๆ คือ:

  • ตื่นขึ้นตามกำหนดเวลา (ตัวจับเวลา, motion sensor, ปุ่ม)

  • ทำงาน BLE ชุดหนึ่ง ได้แก่ โฆษณา, รับการเชื่อมต่อ, ส่งค่า, ตัดการเชื่อมต่อ

  • เรียก aioble.stop() และเข้าสู่โหมดพักจนถึงการตื่นครั้งถัดไป

11.8.5. สิ่งที่ aioble ไม่ทำ

aioble ครอบคลุม GATT, GAP, และ L2CAP โดยเจตนา ซึ่งเป็นชั้นที่แอปพลิเคชันใช้งาน สามส่วนอยู่นอกขอบเขต:

  • ทุกอย่างที่อยู่ต่ำกว่า link layer การเลือกช่อง, การกระโดดความถี่, การรับทราบแพ็กเกต, และการเข้ารหัสระดับ link-layer ทั้งหมดเกิดขึ้นภายใน BLE port และ controller silicon โดย aioble ไม่เปิดเผย hooks ในระดับนั้น

  • Classic Bluetooth aioble รองรับเฉพาะ BLE เท่านั้น ลิงก์เสียง, RFCOMM, A2DP, และคุณสมบัติ classic-profile อื่นๆ ไม่ใช่ส่วนหนึ่งของ API

  • Bluetooth Mesh ชั้น mesh networking ของ Bluetooth SIG (stack แยกต่างหากบน BLE advertising) ไม่ได้ถูก implement บน camera cam สามารถโฆษณาและสังเกตการณ์ได้ แต่ไม่สามารถเข้าร่วมบทบาท relay/friend/proxy ของ mesh network ได้

11.8.6. Exceptions

aioble มี exception สี่ประเภท แต่ละตัวจะเกิดขึ้นจากภายใน coroutine ที่กำลังรอการดำเนินการเมื่อมีข้อผิดพลาด block async with จะคลายออกอย่างสะอาดเมื่อมันแพร่กระจาย

  • aioble.DeviceDisconnectedError -- ลิงก์ BLE ไปยัง peer ขาดหายขณะที่การดำเนินการ GATT (read, write, notified, indicated, subscribe, exchange_mtu, ...) กำลังดำเนินอยู่ จะ raise ภายใน coroutine ที่กำลังรอ เป็น exception ที่พบบ่อยที่สุด ควร catch ในโค้ดที่ควรเชื่อมต่อใหม่เมื่อสูญเสียการเชื่อมต่อ

  • aioble.GattError -- การดำเนินการ GATT ถึง peer แล้วแต่เสร็จสิ้นด้วย ATT status ที่ไม่ใช่ศูนย์ (write-with-response ถูกปฏิเสธ, indicate ไม่ได้รับการรับทราบ, read-not-permitted, ...) รหัสสถานะอยู่ใน attribute _status ของ exception

  • aioble.L2CAPDisconnectedError -- ช่องสัญญาณ L2CAP ขาดหายขณะที่ send(), recvinto(), หรือ flush() กำลังดำเนินอยู่ ฝั่งใดฝั่งหนึ่งอาจปิดช่องสัญญาณ หรือการเชื่อมต่อ GAP พื้นฐานอาจขาดไป

  • aioble.L2CAPConnectionError -- raise โดย l2cap_connect() เมื่อผู้รับฟังปฏิเสธหรือ controller ล้มเหลวในการตั้งค่าช่องสัญญาณ รหัสสถานะ Bluetooth เป็น positional argument แรก

การดำเนินการที่รับ timeout_ms อย่างชัดเจน (การเรียก connect/discovery/read/write/pair รวมถึง timeout() ในฐานะ wrapper) จะ raise asyncio.TimeoutError จาก asyncio เพิ่มเติมเมื่อ deadline หมดเวลาก่อนที่การดำเนินการจะเสร็จสิ้น