Python使用hcitool实现低功耗蓝牙设备搜索,详解

发表时间:2021-05-11

本文将介绍如何在 python 程序中使用 hcitool 工具对周围低功耗蓝牙设备(BLE)进行扫描。

设备及系统软件需求

设备依赖:

树莓派3B及更新版本设备

其他带有蓝牙功能的开发板(需确认软件支持)

配备蓝牙功能的x86主机或已安装蓝牙适配器

系统及软件依赖:

树莓派OS:Raspbian、Ubuntu Core

X86主机:Ubuntu、或其他Linux发行版 (Window及MacOS无原生支持Gatttool及Hcitool,因此不适应,若需要相应功能,可参考pybluez库)

软件:bluetooth、rpi-bluetooth (树莓派)

参考资料

参考开源库 penlin/pygatt ,以其中 /pygatt/backends/gatttool/gatttool.py 中 scan 方法为例,作相应修改完成。

前置知识

首先需要了解 hcitool 工具的使用。对于该工具的详细功能和参数的使用方法,可以使用:

hcitool --help

在开始使用 hcitool 工具之前,需要先进行一些配置。默认情况下, hcitool 工具需要有 root 权限,但是我们在使用中或者python程序中调用,都希望不需要使用 root 权限或者使用 sudo 来提权,我们可以通过以下配置来实现不需要 root 权限而对工具进行调用:

sudo setcap 'cap_net_raw,cap_net_admin+eip' `which hcitool`

完成配置后,我们就可以开始来了解 hcitool这个工具。以下介绍几个最主要常用的功能。首先是获取当前蓝牙设备的信息:

hcitool dev
>
Devices:
        hci0    00:1A:7D:DA:**:**

如果蓝牙设备正常,可以看到返回当前蓝牙的 Mac 地址,如果蓝牙设备不能正常使用,则不会有返回信息,我们可以通过手动关闭蓝牙来实验以下。

sudo hciconifg hci0 down
hcitool dev
>
Devices:

sudo hciconfig hci0 up
hcitool dev
>
Devices:
        hci0    00:1A:7D:DA:**:**

这里还有一个技巧,如果使用蓝牙扫描是出现 Set scan parameters failed: Input/output errer 的错误信息,可以使用上述的方法,手动重启蓝牙,即可解决问题。

接下来是对周围的低功耗蓝牙设备进行搜索:

hcitool lescan
>
LE Scan ...
20:91:**:B9:0B:** (unknown)
20:91:48:**:0B:EE UPots
20:91:48:B9:**:89 (unknown)
20:91:**:B9:08:89 UPots
20:91:48:**:0B:C2 (unknown)
20:91:48:B9:**:C2 UPots
20:91:48:**:0B:EE (unknown)

执行命令后,可以看到会不断打印出周围蓝牙设备的名称和 MAC 地址。

到这里为止,我们就完成对 hcitool 工具的基本了解了,下面将进入 python 编程部分。

使用 Python 调用 hcitool 工具完成 BLE 扫描

我们在刚才是试验中,已经了解到,当使用 hcitool 时,该工具不会自动停止,而是会不断打印搜索结果。这里我们需要使用 pexpect timeout 来控制运行一定时间后就自动停止。

import pexpect

# 这里的 timeout 设置为 3 ,即 3 秒后就会停止
scan = pexpect.spawn('hcitoon lescan, timeout=3)

在运行后,正常情况下的输出结果应该和上面类似,以 LE Scan … 开头,下面接着就是搜索出来的设备和 MAC 地址,我们可以通过用 expect 来获取结果。这里有个技巧,因为我们的 hcitool 会不断打印到时间停止,所以我们可以通过 expect 捕获随便一个不存在的字符,这样程序就会直接进入 pexpect.TIMEOUT ,我们再通过 before 就能拿到所有的打印结果,接而进一步对结果处理即可。

scan.expect('foooooo') # 设置捕获一个不存在的字符串即可
scan.before
>
 b'LE Scan ...\r\n20:91:48:**:0B:C2 (unknown)\r\n
20:91:48:**:08:89 (unknown)\r\n
20:91:48:**:0B:EE (unknown)\r\n
20:91:48:**:0B:EE UPots\r\n
20:91:48:**:0B:C2 (unknown)\r\n
......

这里可以看到我们已经拿到了返回的结果。还有一点需要注意的,上面的返回结果是正常的情况,而实际上也会存在其他的异常情况,包括有 No such device Set scan parameters failed: Input/output errer 及其他错误等。所以需要做一个异常处理:

try:
    scan.expect('foooo')
except pexpect.EOF:
    before_eof = scan.before.decode('utf-8', 'replace')
    if "No such device" in before_eof:
        message = "No BLE adapter found"
    elif "Set scan parameters failed: Input/output errer" in before_eof:
        message = ("BLE adapter requires reset after a scan as root""- call adapter.reset()")
    else:
        message = "Unexpected error when scanning %s" % before_eof
    raise Exception(message)

接下来,我们就开始对返回结果进行处理。

首先,我们可以对返回结果进行 decode 以及换行分割处理 split ,这样就可以得到一个MAC地址和设备名称的数组:

scan.before.decode('utf-8', 'replace').split('\r\n')
>
['LE Scan ...',
 '20:91:48:**:0B:C2 (unknown)',
 '20:91:48:**:08:89 (unknown)',
 '20:91:48:**:0B:EE (unknown)',
 '20:91:48:**:0B:EE UPots',
...
]

接下来只需要通过正则表达式将MAC地址和设备名称分开,然后在筛选掉 unknown 的设备和重复的 MAC地址 即可,这部分的操作在 expect pexpect.TIMEOUT 下完成:

... # 上面的代码忽略
except pexpect.TIMEOUT:
    devices = {}
    for line in scan.before.decode('utf-8', 'replace').split('\r\n'):
        match = re.match(r'(([0-9A-Fa-f][0-9A-Fa-f]:?){6}) (\(?.+\)?)', line)
        if match is not None:
            address = match.group(1)
            name = match.group(3)
            # print(match.group(3))
            if name == "(unknown)":
                name = None
            
            if address in devices:
                if (name is not None) and (address not in devices.keys()):
					# 这里筛选掉 unknown 设备 和 重复的MAC地址设备
					# 如果需要处理,可以在这里完成相应的逻辑
					# 由于这里不需要,所以就直接使用 pass
                    pass
            else:
                if (name is not None) and (address not in devices.keys()):
                    print("Discovered %s (%s)" % (address, name))
                    devices[address] = {
                        'address': address, 
                        'name': name
                    }
    print('Found %d BLE devices' % len(devices))

这样,我们就可以获取到所有非 unknown 的设备了。到这里,我们的功能基本已经完成了,但还需要在程序执行完成后执行关闭 hcitool 的工作,这部分代码将在 finally 中实现:

... #上述代码省略
finally:
    try:
        scan.kill(signal.SIGINT)

        while True:
            try:
                scan.read_nonblocking(size=100)
            except (pexpect.TIMEOUT, pexpect.EOF):
                break
        
        if scan.isalive():
            scan.wait()
    except OSError:
        print("Unable to gracefully stop the scan - "
              "BLE adapter may need to be reset")

这里的代码将会在执行完毕后关闭 hcitool 进程。

完整代码

以下为完整的代码展示:

import pexpect
import re
import signal

scan = pexpect.spawn('hcitool lescan', timeout=3)

try:
    scan.expect('foooo')
except pexpect.EOF:
    before_eof = scan.before.decode('utf-8', 'replace')
    if "No such device" in before_eof:
        message = "No BLE adapter found"
    elif "Set scan parameters failed: Input/output errer" in before_eof:
        message = ("BLE adapter requires reset after a scan as root""- call adapter.reset()")
    else:
        message = "Unexpected error when scanning %s" % before_eof
    raise Exception(message)
except pexpect.TIMEOUT:
    devices = {}
    for line in scan.before.decode('utf-8', 'replace').split('\r\n'):
        match = re.match(r'(([0-9A-Fa-f][0-9A-Fa-f]:?){6}) (\(?.+\)?)', line)
        if match is not None:
            address = match.group(1)
            name = match.group(3)
            # print(match.group(3))
            if name == "(unknown)":
                name = None
            
            if address in devices:

                if (name is not None) and (address not in devices.keys()):
                    pass
            else:
                if (name is not None) and (address not in devices.keys()):
                    print("Discovered %s (%s)" % (address, name))
                    devices[address] = {
                        'address': address, 
                        'name': name
                    }
    print('Found %d BLE devices' % len(devices))
finally:
    try:
        scan.kill(signal.SIGINT)

        while True:
            try:
                scan.read_nonblocking(size=100)
            except (pexpect.TIMEOUT, pexpect.EOF):
                break
        
        if scan.isalive():
            scan.wait()
    except OSError:
        print("Unable to gracefully stop the scan - "
              "BLE adapter may need to be reset")

运行上面的代码,如果正常就能打印搜索到的设备:

Discovered 20:91:48:**:08:89 (UPots)
Discovered 20:91:48:**:0B:EE (UPots)
Discovered 20:91:48:**:0B:C2 (UPots)
Discovered 58:2D:34:**:**:1E (MJ_HT_V1)
Discovered 58:80:3C:**:**:03 (Amazfit Watch-E435)
Discovered 38:18:4C:**:**:AE (LE_WH-1000XM3)
Found 6 BLE devices

小结

本文从开始介绍 hcitool 工具的简单使用,以及后面一步步分解介绍如何使用 pexpect 配合 hcitool 完成扫描周围蓝牙设备的功能代码。

这里的代码只是演示和研究代码的实践,我们最终可以根据实际的需求,将上述代码进行封装,来实现后续的蓝牙功能开发。

如果你觉得文章对你用,记得关注收藏。你的关注和收藏是继续更新的动力哦。

文章来源互联网,如有侵权,请联系管理员删除。邮箱:417803890@qq.com / QQ:417803890

微配音

Python Free

邮箱:417803890@qq.com
QQ:417803890

皖ICP备19001818号-4
© 2019 copyright www.pythonf.cn - All rights reserved

微信扫一扫关注公众号:

联系方式

Python Free