本文旨在作為入門藍牙開發的一個簡單介紹
安裝BlueZ和PyBluez
$ sudo apt install libglib2.0-dev libbluetooth-dev bluetooth
$ pip install pybluez
介紹藍牙編程
概覽
- 找到通信的設備
- 確認如何進行通信
- 創建一個發送的連接
- 創建一個接收的連接
- 發送數據
- 接收數據
選一個通信伙伴
每個藍牙芯片包含唯一的48-bit地址,稱為Bluetooth address和device address。可以把它看作為以太網的MAC地址,同樣由IEEE注冊授權。在制作的時候就寫入到芯片李,作為藍牙編成最基本的地址單元。
一個藍牙設備需要和另外一個設備通信,必須要有某種機制獲悉到其他設備的藍牙地址。
在互聯網中,通常會利用DNS技術實現一個簡單域名來進行IP地址轉換,在藍牙中通常是提供一個友好的名字,例如"My Phone",客戶端通過查找附近藍牙設備來轉換為數字地址。
選擇傳輸協議
現在客戶端已經確認好要通信的設備,現在就需要確認使用什么樣的傳輸協議了。
RFCOMM + TCP
RFCOMM類似TCP可靠性,雖然協議規范定義設為模仿 RS-232串口通信,就如類似TCP場景操作一樣簡單。
通常,應用程序使用TCP考慮使用點對點的連接,進行可靠的數據流傳輸。如果出現了固定次數失敗傳輸,那么連接會斷開並且會拋出一個錯誤。
RFCOMM和TCP最大的不同就是端口的選擇,TCP支持最大65525端口,RFCOMM僅支持30。
L2CAP + UDP
UDP通常設計用來在對可靠性傳輸沒有強制要求,就是為了足夠輕量。L2CAP提供了類似的設計。
L2CAP,默認提供了一個面向連接,通過發送固定最大長度的單個數據包來提供可靠性。L2CAP可以定制為不同的可靠級別。為了提供該能力,L2CAP提供了傳輸和確認的方式,為被確認包進行重傳。有三種可用的策略:
- 不重傳
- 傳輸直到連接失敗
- 丟棄包,如果數據包在特定時間(0-1279毫秒)沒有確認放到隊列里。這在需要設計為特定時間傳輸時很有用。
雖然藍牙允許應用可以使用最大努力通信而不是可靠傳輸,有幾點還是需要注意。
| Requirement | Internet | Bluetooth |
|---------------------- |---------- |------------------------------------------ |
| 基於流的可靠傳輸 | TCP | RFCOMM |
| 可靠的數據報傳輸 | TCP | RFCOMM or L2CAP with infinite retransmit |
| 最大努力的數據報傳輸 | UDP | L2CAP (0-1279 ms retransmit) |
端口號和服務發現協議
在搞清楚傳輸協議之后,第二個要弄清楚與遠程機器通信部分就是端口號。在網絡傳輸協議里,端口號是用來在同一個主機上來區分不同的具體應用的能力。藍牙也不例外,但是使用了略微不同的術語。在L2CAP中,端口稱為Protocol Service Mutiplexers,可以取1到32767基數端口號。在RFCOMM,有1-30通道可以使用。除了這些差別,兩個協議提供類似TCP/IP多路復用功能。L2CAP,和RFCOMM不一樣,存在(1-1023)保留端口。
| protocol | terminology | reserved/well-known ports | dynamically assigned ports |
|---------- |------------- |--------------------------- |---------------------------- |
| TCP | port | 1-1024 | 1025-65535 |
| UDP | port | 1-1024 | 1025-65535 |
| RFCOMM | channel | none | 1-30 |
| L2CAP | port | odd numbered 1-4095 | odd numbered 4097 - 32765 |
在網絡編程中,服務器通常會使用常用端口進行服務,客戶端也使用常用端口進行連接。缺點就是不能在同一個服務器使用相同的端口應用程序,應用TCP/UDP可選擇的端口非常多,所以這里也沒有多大的問題。
藍牙傳輸協議中,設計了較少的有效端口,我們不可以隨意在設計期間選擇任意端口。雖然在L2CAP中也不是什么問題,它存在15,000保留端口,RFCOMM僅有30個端口。結果就是有在7個應用程序就有可能超過50%的可能性端口沖突。藍牙解決這個問題的方式使用Service Discovery Protocol(SDP)。
不是在設計時確定端口,藍牙通過在運行時通過發布-訂閱模型來確定端口。宿主服務器提供一個叫做SDP服務,它使用了L2CAP一些保留端口。其他服務器在運行時應用程序使用動態端口,並且注冊一些它們的描述信息。客戶端應用程序會從SDP服務(使用定義號的端口號)獲取需要的信息。
這里的問題是客戶端端如何知道哪個描述信息時它要找的呢?標准方式時通過給藍牙賦於128-bits的數值,成為UUID(Universally Unique Identifier),客戶端和服務端使用了相同的UUID機制一邊SDP服務可以找到它們。
SDP還可以用來描述哪個傳輸協議在使用,SDP在通信當中也不是必須的,也可以使用TCP/UDP的方式來提前定義端口,但要小心端口沖突。
藍牙 RFC
PyBluez
PyBluez提供了藍牙編程的能力。
選擇通信伙伴
下面的代碼示例,是將附近所有的設備打印出來
import bluetooth
nearby_devices = bluetooth.discover_devices(lookup_names=True)
print("found %d devices" % len(nearby_devices))
for addr, name in nearby_devices:
print(" %s - %s" % (addr, name))
結果:
# python bluetooth_ex1.py
found 1 devices
C0:A5:3E:88:F8:BB - 何文祥的 iPhone
我們可以看到地址使用16進制方式呈現,每個占用8位,一共48-bit。
discover_devices()大概花費10秒來檢測設備列表。lookup_names是請求時獲取到名稱,例如這里的何文祥的 iPhone。
discover_devices()有時候會檢測失敗,lookup_name()有時也會返回None。
使用RFCOMM通信
在Python中藍牙編程遵循了socket編程。這對於大部分網絡程序編寫過的程序員來說應該是非常熟悉,切換過來也相當簡單,以下展示了如何使用RFCOMM建立連接,以及傳輸數據,最后進行了斷開操作。
rfcomm_server.py
import bluetooth
server_sock=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
port = 1
server_sock.bind(("",port))
server_sock.listen(1)
client_sock,address = server_sock.accept()
print "Accepted connection from ",address
data = client_sock.recv(1024)
print "received [%s]" % data
client_sock.close()
server_sock.close()
rfcomm_client.py
import bluetooth
bd_addr = "01:23:45:67:89:AB"
port = 1
sock=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
sock.connect((bd_addr, port))
sock.send("hello!!")
sock.close()
在socket通信中,socket提供了一個通信通道,創建時還沒有連接,直到有另一端有發起連接過來,一旦連接建立,就可以進行收發數據。
PyBluez支持兩種BluetoothSocket對象:RFCOMM和L2CAP。上面的例子就是RFCOMM socket,通過傳遞RFCOMM作為BluetootheSocket構造參數。L2CAP我們會在下節描述
我們必須使用bind方法綁定給操作系統,bind方法接受一個元組參數,提供給藍牙適配器進行監控的地址和端口號。通常我們在設備上只有一個藍牙適配器,第一個參數使用為空就可以了,之后我們就可以使用listen將socket置入監聽模式,等待連接。
BluetoothSocket向外連接需要使用connect方法,需要傳遞一個元組參數,該傳輸是指定需要建立連接的地址和端口號。后面我們將介紹使用動態端口號以及使用SDP服務查找端口
使用L2CAP通信
接下來我們了解如何使用L2CAP作為傳輸協議,L2CAP通信方式和RFCOMM sockets通信方式極其相像。唯一區別就是傳遞給BluetoothSocket構造參數更改為L2CAP,選擇一個(0x1001到0x8FFF)基數號碼,默認連接配置提供了可靠的數據包大小為672bytes。
l2cap-server.py
import bluetooth
server_sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
port = 0x1001
server_sock.bind(("", port))
server_sock.listen(1)
client_sock, address = server_sock.accept()
print("Accepted connection from", address)
data = client_sock.recv(1024)
print("Received [{:r}]".format(data))
client_sock.close()
server_sock.close()
l2cap-client.py
import bluetooth
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
bd_addr = "9C:B6:D0:E8:F9:D4"
port = 0x1001
sock.connect((bd_addr, port))
sock.send(b"hello!")
sock.close()
L2CAP發送的數據包有最大限制,兩個設備端都維護了一個MTU來指定可以收到的最大包大小。如果兩者調整各自的MTU,那么它們的默認672字節可以調整到65535字節。但通常,都會使用默認的MTU值進行茶unshu。在PyBluez通過設置set_l2cap_mtu方法來調整該值
bluetooth.set_l2cap_mtu( l2cap_sock, 65535 )
該方法使用也很直觀,第一個參數是創建BluetoothSocket對象,第二個參數就是具體要調整的值大小了。
有時候連接不可靠,需要使用set_packet_timeout方法()
bluetooth.set_packet_timeout( bdaddr, timeout )
set_packet_timeout接收藍牙地址,和一個毫秒參數,作為調整L2CAP和RFCOMM連接發送包的超時時間,需要有管理員權限,而且該操作是全局影響的。
服務發現協議
當前我們已經學習如何檢測到附近藍牙設備,並且進行兩種傳輸協議的連接,均使用的是固定的藍牙地址和端口號。在實踐中我們並不推薦這么做。
動態開辟端口和使用SDP(Service Discovery Protocol)進行查找,get_available_port方法來找到有效的L2CAP和RFCOMM端口,advertise_service通過本地SDP服務器發送服務,find_service找到特定設備的藍牙設備。
server_sock.bind(("", bluetooth.PORT_ANY))
bluetooth.PORT_ANY任意端口,后面我可以使用server_sock.getsockname()[1]來獲取到實際端口號
bluetooth.advertise_service( sock, name, uuid )
bluetooth.stop_advertising( sock )
bluetooth.find_service( name = None, uuid = None, bdaddr = None )
這三個方法提供了一個在本地藍牙設備提供了通知服務的方式.advertise_service接收一個socket用來綁定監聽, service name和UUID作為參數。socket打開的話通知功能也一直打開,直到調用stop_advertising來關閉該socket。
find_service可以查找單個或者所有附近特定設備。通過匹配name和uuid進行service查找,必須至少指定其中一個。如果bdaddr是None,那么所有附近的設備都會進行查找。如果提供了localhost作為bdaddr參數,那么會對本地的SDP進行查找。否着,就會指定的bdaddr藍牙設備進行查找。
find_service返回一個列表,每個列表是個字典類型,包含了host, name, protocol, port。
rfcomm-server-sdp.py
import bluetooth
server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(("", bluetooth.PORT_ANY))
server_sock.listen(1)
port = server_sock.getsockname()[1]
print("listening on port {:d}".format(port))
uuid = "1e0ca4ea-299d-4335-93eb-27fcfe7fa848"
bluetooth.advertise_service(server_sock, "FooBar Service", uuid)
client_sock, address = server_sock.accept()
print("Accepted connection from ", address)
data = client_sock.recv(1024)
print("received [{:r}]".format(data))
client_sock.close()
server_sock.close()
rfcomm-client-sdp.py
import sys
import bluetooth
uuid = "1e0ca4ea-299d-4335-93eb-27fcfe7fa848"
service_matches = bluetooth.find_service(uuid=uuid)
if len(service_matches) == 0:
print("couldn't find the FooBar service")
sys.exit(0)
first_match = service_matches[0]
port = first_match["port"]
name = first_match["name"]
host = first_match["host"]
print("connecting to {} on {}".format(name, host))
sock=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
sock.connect((host, port))
sock.send(b"hello!!")
sock.close()
