蓝牙开发快速入门

本文旨在作为入门蓝牙开发的一个简单介绍

安装BlueZ和PyBluez

$ sudo apt install libglib2.0-dev libbluetooth-dev bluetooth
$ pip install pybluez

介绍蓝牙编程

概览

  • 找到通信的设备
  • 确认如何进行通信
  • 创建一个发送的连接
  • 创建一个接收的连接
  • 发送数据
  • 接收数据

选一个通信伙伴

每个蓝牙芯片包含唯一的48-bit地址,称为Bluetooth addressdevice 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

www.bluetooth.org/spec

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()
posted @ 2018-12-29 18:17  疯人院主任  阅读(7777)  评论(0编辑  收藏  举报