11.4. การโฆษณาและการสแกน¶
อุปกรณ์ BLE สองตัวที่ไม่เคยพบกันมาก่อนต้องค้นหากันก่อน การเชื่อมต่อเครือข่ายแก้ปัญหานี้โดยกำหนด IP ให้ทุกอุปกรณ์จากกลุ่มที่ใช้ร่วมกัน และให้ทั้งสองฝั่งเข้าหากันผ่านเราเตอร์ BLE ไม่มีเราเตอร์ ไม่มีกลุ่ม IP ร่วม และระหว่างอุปกรณ์ส่วนใหญ่ก็ไม่มีความสัมพันธ์ก่อนหน้าเลย Generic Access Profile (GAP) แก้ปัญหาการค้นพบด้วยรูปแบบออกอากาศและฟังแทน ฝั่งหนึ่ง โฆษณา นั่นคือส่งแพ็กเก็ตสั้นๆ บนช่องโฆษณาสามช่องในช่วงเวลาปกติ เพื่ออธิบายว่าตัวเองคือใคร ส่วนอีกฝั่ง สแกน นั่นคือกวาดดูสามช่องเดียวกันเพื่อฟังแพ็กเก็ตเหล่านั้น
GAP กำหนดบทบาทสี่แบบรอบรูปแบบนั้น แต่ละบทบาลเป็นการผสมผสานเฉพาะของการโฆษณาและการฟัง
11.4.1. บทบาท GAP สี่แบบ¶
บทบาท GAP สี่แบบ แกนแนวตั้งคือว่าอุปกรณ์โฆษณาหรือไม่ แกนแนวนอนคือว่ารับ (หรือเริ่ม) การเชื่อมต่อหรือไม่¶
Peripheral โฆษณาแพ็กเก็ตที่บอกว่า "ฉันอยู่ที่นี่และคุณเชื่อมต่อหาฉันได้" เมื่ออุปกรณ์อื่นเปิดการเชื่อมต่อ peripheral จะหยุดโฆษณาและเริ่มให้บริการคำขอ GATT สายคาดอกวัดอัตราการเต้นของหัวใจ เทอร์โมมิเตอร์ และกล้องส่วนใหญ่ที่ทำหน้าที่เป็นเซนเซอร์จะทำงานเป็น peripheral
Central สแกนหา peripheral เลือกอันหนึ่ง และเริ่มการเชื่อมต่อ หลังจากเชื่อมต่อแล้วจะทำหน้าที่เป็น GATT client โทรศัพท์ แล็ปท็อป และกล้องที่ทำหน้าที่เป็นตัวเก็บข้อมูลจะทำงานเป็น central
Broadcaster โฆษณาแต่ไม่รับการเชื่อมต่อใดๆ payload การโฆษณา คือ ข้อมูล ไม่มีสิ่งใดให้เชื่อมต่อ iBeacon และบีคอนตรวจจับการอยู่ในร้านส่วนใหญ่เป็น broadcaster
Observer สแกนหาโฆษณาเหล่านั้นและอ่าน payload โดยไม่เชื่อมต่อเลย กล้องที่ฟังบีคอนใกล้เคียงและดำเนินการตามข้อมูลที่ได้รับคือ observer
อุปกรณ์เดียวสามารถทำได้หลายบทบาทในเวลาเดียวกัน กล้องสามารถเป็นทั้ง peripheral ที่เผยแพร่สถานะของตัวเอง และ central ที่เชื่อมต่อกับเซนเซอร์ใกล้เคียง วิทยุจะแบ่งงานเหล่านี้
11.4.2. สิ่งที่อยู่ในแพ็กเก็ตโฆษณา¶
แพ็กเก็ตโฆษณามีขนาดเล็ก: payload 31 ไบต์ หรือ 62 ไบต์หาก advertiser ยังเผยแพร่ scan response ที่ scanner สามารถร้องขอได้ทันที payload คือรายการฟิลด์ประเภทสั้นๆ:
Flags เชื่อมต่อได้หรือไม่ ค้นพบได้แบบ general/limited
ชื่อท้องถิ่น สตริงสั้นที่มนุษย์อ่านได้ นั่นคือชื่อที่ระบบปฏิบัติการบนโทรศัพท์หรือแล็ปท็อปแสดงในเมนู Bluetooth
Service UUIDs รายการตัวระบุบริการ GATT ที่อุปกรณ์เป็นเจ้าของ เพื่อให้ scanner รู้จัก peripheral ที่มีความสามารถโดยไม่ต้องเชื่อมต่อก่อน สายคาดอกวัดอัตราการเต้นของหัวใจโฆษณา
0x180Dซึ่งเป็น UUID ของบริการ Heart-Rate มาตรฐาน และแอปวัดอัตราการเต้นของหัวใจบนโทรศัพท์รู้จากข้อมูลนั้นเพียงอย่างเดียวว่าอุปกรณ์นี้คุ้มค่าที่จะเชื่อมต่อAppearance ค่า 16 บิตจากรายการหมายเลขที่กำหนดโดย Bluetooth (เซนเซอร์ สื่อทั่วไป นาฬิกาทั่วไป...) เป็นคำแนะนำสำหรับ central เกี่ยวกับสิ่งที่ควรแสดง
ข้อมูลเฉพาะผู้ผลิต ไบต์รูปแบบอิสระที่นำหน้าด้วย company ID iBeacon ใช้ฟิลด์นี้เพื่อบรรจุ UUID, major และ minor ของตัวเอง แอปพลิเคชันที่กำหนดเองสามารถใส่อะไรก็ได้ที่ต้องการ
payload การโฆษณามีพื้นที่จำกัด ขีดจำกัด 31 ไบต์ทำให้การเลือกสิ่งที่จะรวมไว้เป็นการตัดสินใจออกแบบจริงๆ ชื่อที่มนุษย์อ่านได้ยาวๆ อาจทำให้ไม่เหลือที่ว่างสำหรับ service UUID aioble.advertise() API รับแต่ละรายการเหล่านี้เป็น keyword argument และประกอบไบต์ให้คุณ โดยอัตโนมัติล้นไปยัง scan response หากแพ็กเก็ตหลักเต็ม
11.4.3. การสแกนแบบ Active และ Passive¶
Scanner สามารถทำงานแบบ passive ซึ่งฟังแพ็กเก็ตโฆษณาและแยกวิเคราะห์สิ่งที่มาถึง หรือแบบ active ซึ่งยังส่ง scan request ไปยัง advertiser แต่ละตัวและแยกวิเคราะห์ scan response ที่ได้รับกลับมา
การสแกนแบบ passive เห็นเฉพาะแพ็กเก็ตโฆษณาเริ่มต้น (สูงสุด 31 ไบต์) การสแกนแบบ active เพิ่มเป็นสองเท่า scan response คืออีก 31 ไบต์ที่ peripheral สามารถใช้สำหรับฟิลด์ที่ไม่พอดี การสแกนแบบ active ยังใช้พลังงานจากทั้งสองฝั่งด้วย เนื่องจาก scanner ส่งสัญญาณและ advertiser ส่งแพ็กเก็ตพิเศษ จึงเป็นทางเลือก ไม่ใช่ค่าเริ่มต้น
ใน aioble API, active=True บน aioble.scan() จะสลับโหมด และแต่ละ ScanResult จะเปิดเผย adv_data รวมกับ resp_data ตลอดจน helper เช่น result.name() และ result.services() ที่ซ่อนการแยกวิเคราะห์ระดับไบต์
Note
แอตทริบิวต์ adv_data และ resp_data คือ payload การโฆษณาและ scan-response ดิบ (bytes) helper ได้แก่ name(), services(), manufacturer() ครอบคลุมฟิลด์มาตรฐานทั่วไปและเป็นตัวเลือกที่ถูกต้อง 99% ของเวลา เข้าถึงไบต์ดิบเฉพาะเมื่อคุณต้องการฟิลด์ vendor ที่ helper ไม่แยกวิเคราะห์ (Eddystone URL, iBeacon UUID/major/minor, ประเภทโฆษณาที่กำหนดเอง) รูปแบบไบต์คือ TLV มาตรฐาน: แต่ละฟิลด์คือ length, type, value...
11.4.4. ช่วงเวลาการโฆษณา¶
ความถี่ที่ peripheral ออกอากาศเป็นการแลกเปลี่ยนระหว่างพลังงานกับเวลาแฝงในการค้นพบ โฆษณาที่ออกทุก 20 ms จะถูกตรวจพบเกือบทันทีโดย scanner แต่ทำให้วิทยุยุ่งและแบตเตอรีหมดเร็ว โฆษณาทุกวินาทีใช้พลังงานแทบไม่มี แต่ทำให้ scanner ใช้เวลานานขึ้นในการสังเกตเห็นอุปกรณ์
interval_us บน aioble.advertise() กำหนดช่วงเวลาเป็นไมโครวินาที:
20,000 ถึง 100,000 us (20 ms - 100 ms) -- การจับคู่เร็ว แอปคาดหวังการตอบสนองรวดเร็ว อุปกรณ์ที่เสียบปลั๊ก
250,000 ถึง 1,000,000 us (250 ms - 1 s) -- ค่าเริ่มต้นที่เหมาะสมสำหรับ peripheral ที่ใช้แบตเตอรีซึ่งต้องการให้ค้นพบได้โดยไม่เปลืองประจุ
สูงกว่า 1,000,000 us -- การออกอากาศพื้นหลังที่ช้า บีคอนที่ส่งข้อมูลตำแหน่งทุกไม่กี่วินาที
ฝั่ง scanner มีปุ่มปรับของตัวเอง aioble.scan() รับ interval_us และ window_us (ความถี่ที่ scanner ตื่นวิทยุและระยะเวลาที่ฟังแต่ละครั้ง) ค่าเริ่มต้นใช้ได้ดี การเปลี่ยนแปลงทั่วไปเพียงอย่างเดียวคือตั้งค่าทั้งสองให้เท่ากันสำหรับการสแกนต่อเนื่องเมื่อไม่เป็นปัญหาเรื่องแบตเตอรี
11.4.5. รูปแบบ Connectionless -- broadcaster และ observer¶
หน้าต่างๆ บน การทำหน้าที่เป็น peripheral และ การทำงานในฐานะ central จะอธิบายรูปแบบ connectable ของ API ซึ่ง peripheral รับการเชื่อมต่อและทั้งสองฝั่งแลกเปลี่ยนข้อมูลผ่าน GATT อีกรูปแบบหนึ่งคือ connectionless: broadcaster ส่ง payload เป็นโฆษณา และ observer ใดก็ตามในระยะสามารถอ่านได้โดยไม่ต้องเชื่อมต่อ บีคอน เซนเซอร์ตรวจจับการมีอยู่ และการส่งข้อมูลทางเดียวล้วนอยู่ที่นี่
broadcaster คือ aioble.advertise() ที่มี connectable=False ข้อมูลเฉพาะผู้ผลิตบรรจุ payload:
import aioble
import asyncio
import struct
_COMPANY_ID = const(0xFFFF) # 0xFFFF is "no specific vendor"
async def beacon():
seq = 0
while True:
seq = (seq + 1) & 0xFFFF
payload = struct.pack("<H", seq)
await aioble.advertise(
interval_us=500000,
connectable=False,
name="openmv-beacon",
manufacturer=(_COMPANY_ID, payload),
timeout_ms=1000, # one cycle, then loop
)
asyncio.run(beacon())
คีย์เวิร์ด timeout_ms สิ้นสุดการเรียก advertise หลังหนึ่งวินาที ลูปด้านนอกออกคำสั่งใหม่พร้อมหมายเลขลำดับถัดไปเพื่อให้ผู้ฟังเห็นข้อมูลใหม่ แฟล็ก connectable=False คือสิ่งที่ทำให้การโฆษณาเป็นสไตล์ broadcaster กล้องจะไม่ตอบสนองต่อคำขอเชื่อมต่อแม้จะมาถึง
observer คือ scanner แบบอ่านอย่างเดียวที่ตรงกัน มันรัน aioble.scan() ตลอดไป แยกวิเคราะห์โฆษณาที่เข้ามา และไม่เรียก connect() เลย:
import aioble
import asyncio
_COMPANY_ID = const(0xFFFF)
async def watch():
async with aioble.scan(duration_ms=0, active=False) as scanner:
async for result in scanner:
for company, data in result.manufacturer(filter=_COMPANY_ID):
print(result.device.addr_hex(),
"rssi", result.rssi, "data", data)
asyncio.run(watch())
duration_ms=0 สแกนจนกว่า context manager จะออก active=False ทำให้วิทยุของ observer เงียบ (ไม่มีคำขอ scan-response) เพื่อการใช้พลังงานต่ำสุด อาร์กิวเมนต์ filter= บน manufacturer() ทิ้งโฆษณาทุกตัวที่ไม่ตรงกับ company ID ดังนั้นลูปจะทำงานเฉพาะสำหรับ broadcaster เท่านั้น
11.4.6. จากการค้นพบสู่การเชื่อมต่อ¶
เมื่อ central เลือก peripheral ที่จะสื่อสารด้วย มันจะหยุดฟัง ส่ง connect request บนช่องโฆษณาที่ peripheral ใช้ล่าสุด และทั้งสองฝั่งก็เข้าสู่ช่องข้อมูลแบบ hopping ของ link layer โดยทั่วไป peripheral จะหยุดโฆษณาในจุดนี้ สิ่งที่เกิดขึ้นต่อจากนั้น ได้แก่ พารามิเตอร์การเชื่อมต่อ การค้นพบ GATT อายุการใช้งานของลิงก์ อยู่ใน การเชื่อมต่อ