其中,構建藍芽BLE廣播報文的函式——advertising_payload是在單獨的模組(.py檔案)中自己實現的。由此,也從另外一個角度反映出,MicroPython只提供了藍芽BLE的“低階”介面:“低階”到廣播報文都需要自己實現,而非系統模組整合。當然,考慮到MicoPython作為一個通用的平臺,又運行於資源有限的微控制器上,其只負責最核心的功能,而將業務場景有關的程式碼實現剝離出來由使用者自己實現,似乎也是無可厚非的了。
兩個藍芽裝置想要建立連線,首先需要外設裝置向外廣播,然後中心裝置才能搜尋到該裝置,再發起連線請求。外設裝置的廣播報文中包含裝置的相關資訊,比如裝置名稱,裝置具有的服務UUID等等,中心裝置可以根據這些資訊決定其是不是自己關心的裝置,以及要不要發起對該裝置的連線請求。
基本的廣播資料包格式如下:
每個廣播資料包都是31位元組,資料包中又分為有效(significant)資料部分和無效(non-significant)資料部分。其中,無效資料部分全為零,僅僅是為了湊夠31位元組而存在。而有效資料部分,又由若干個資料單元組成,每個資料單元的格式為:
1位元組長度+n位元組(通常也是1位元組)型別+n位元組型別特定資料
這裡的型別,用於表明這個資料單元代表什麼,比如標記(Flag),裝置名,或者UUID等等。我們來看例程中是怎麼實現的:
from micropython import constimport structimport bluetooth# Advertising payloads are repeated packets of the following form:# 1 byte data length (N + 1)# 1 byte type (see constants below)# N bytes type-specific data#常量定義_ADV_TYPE_FLAGS = const(0x01)_ADV_TYPE_NAME = const(0x09)_ADV_TYPE_UUID16_COMPLETE = const(0x3)_ADV_TYPE_UUID32_COMPLETE = const(0x5)_ADV_TYPE_UUID128_COMPLETE = const(0x7)_ADV_TYPE_UUID16_MORE = const(0x2)_ADV_TYPE_UUID32_MORE = const(0x4)_ADV_TYPE_UUID128_MORE = const(0x6)_ADV_TYPE_APPEARANCE = const(0x19)#構建廣播資料包def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0): payload = bytearray() #構建廣播資料單元 def _append(adv_type, value): nonlocal payload payload += struct.pack("BB", len(value) + 1, adv_type) + value #標記型別資料單元 _append( _ADV_TYPE_FLAGS, struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)), ) #裝置名型別資料單元 if name: _append(_ADV_TYPE_NAME, name) #服務UUID型別資料單元 if services: for uuid in services: b = bytes(uuid) if len(b) == 2: _append(_ADV_TYPE_UUID16_COMPLETE, b) elif len(b) == 4: _append(_ADV_TYPE_UUID32_COMPLETE, b) elif len(b) == 16: _append(_ADV_TYPE_UUID128_COMPLETE, b) #外觀型別資料單元 # See org.bluetooth.characteristic.gap.appearance.xml if appearance: _append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance)) return payload
Python中struct模組對應C語言中的結構體,其可將結構體中的資料打包成位元組串。比如上述程式碼中,struct.pack函式第一個引數為格式字串,其表示該結構體是如何構建的:"B"表示其中包含一位元組資料,而"BB"則表示其中包含兩位元組資料。上述程式碼中,定義了_append子函式用於構建一個一個的廣播資料單元,其格式正好與前面提到的標準廣播資料單元的格式相符合。
上述廣播資料包中,包含的第一個資料單元為“標記(Flag)”型別資料單元。藍芽BLE可用該型別資料單元表明裝置是有限可發現(LE Limited Discoverable)還是普通可發現(General Discoverable),還可表明裝置是支援雙模(同時支援經典藍芽和藍芽BLE)還是不支援經典藍芽(BR/EDR模式),僅支援藍芽BLE。之後,依據引數傳遞情況決定是否加入裝置名資料單元,服務UUID資料單元,和裝置外觀資料單元。
(注:所謂有限可發現是指該裝置傳送廣播報文時,只是在每個週期中的一段時間之內廣播,其餘時間不廣播。而常規可發現是指該裝置沒有時間限制,一直髮送廣播報文。)
裝置名比較容易理解,上述服務UUID的定義中都有COMPLETE字尾,那麼還有對應INCOMPLETE的型別嗎?確實如此。如果該裝置有兩個服務,只廣播了其中一個,那麼就是INCOMPLETE的,否則,就是COMPLETE的。那外觀(APPEARANCE)又是什麼呢,其實就是告訴接收廣播報文的裝置,本裝置應該用什麼樣的圖示外觀進行顯示,是用一個耳機的圖示,還是用一個HID鍵盤的圖示等等。
如此看來,藍芽規範確實詳實啊,基本所有細節都有考慮!這也是其保證各種裝置能夠廣泛相容的一個措施:自定義功能特性越少,越能保證規範範圍內各裝置之間的相容性。
該模組中還有其它函式如下:
#解析payload中指定adv_type的內容def decode_field(payload, adv_type): i = 0 result = [] while i + 1 < len(payload): if payload[i + 1] == adv_type: result.append(payload[i + 2 : i + payload[i] + 1]) i += 1 + payload[i] return result #解析裝置名,以字串形式返回def decode_name(payload): n = decode_field(payload, _ADV_TYPE_NAME) return str(n[0], "utf-8") if n else ""#解析服務def decode_services(payload): services = [] for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE): services.append(bluetooth.UUID(struct.unpack("<h", u)[0])) for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE): services.append(bluetooth.UUID(struct.unpack("<d", u)[0])) for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE): services.append(bluetooth.UUID(u)) return services#構造單元測試函式,以驗證本模組其它函式工作是否正常def demo(): payload = advertising_payload( name="micropython", services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")], ) print(payload) print(decode_name(payload)) print(decode_services(payload))#作為模組單獨執行時的主入口if __name__ == "__main__": demo()
上述函式基本都是配合advertising_payload函式,用於驗證其是否工作正常。如果直接執行該模組,__name__系統變數為__main__,從最後面的程式碼來看,則直接執行的是demo函式,其使用advertising_payload函式構造了一個廣播報文,並進行各個域的解析,以驗證所構造的報文是否正確。從單元測試的角度來講,這也可以說是Python程式設計中一種比較常規的正規化了。