zoukankan      html  css  js  c++  java
  • 蓝牙开发快速入门

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

    安装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()
  • 相关阅读:
    在eclipse 中添加 Tomcat
    eclipse启动报错:code13
    基础_cup给出的内存地址
    巫师3_战斗_水中水鬼
    git checkout
    git学习
    Linux软件包管理之yum在线管理
    Vagrant入门1
    mvn java项目README.md文件范例
    深入理解yum工作原理
  • 原文地址:https://www.cnblogs.com/erhuabushuo/p/10197463.html
Copyright © 2011-2022 走看看