基于树莓派通过蓝牙进行Wi-Fi网络配置

2 篇文章 0 订阅
1 篇文章 0 订阅

树莓派上有非常丰富的接口,不过有个小问题就是,如果没有屏幕,串口或者有线网络,只能依赖于Wi-Fi网络的话,到一个新环境需要配置Wi-Fi接入网络时,就有点小麻烦。树莓派本身有蓝牙的接口,因此应该可以通过蓝牙来配置Wi-Fi,从而方便地接入Wi-Fi网络。参考网络上的一些方案,我基于python在树莓派4上做了这个功能的测试,记录如下。

1. 环境准备

python版本为3.7.3,pip版本为21.0.1。基于树莓派4,系统版本如下:

Linux raspberrypi4b 5.10.17-v7l+ #1403 SMP Mon Feb 22 11:33:35 GMT 2021 armv7l GNU/Linux

2. 安装依赖

python,pip准备好后,需安装:

sudo pip install wifi

sudo pip install PyBluez

因一些控制能力需要root权限,因此在安装如上模块时使用了sudo。

在/lib/systemd/system/bluetooth.service中,将ExecStart=/usr/lib/bluetooth/bluetoothd替换为ExecStart=/usr/lib/bluetooth/bluetoothd -E -C。不然执行python代码调用bluetooth模块时会报错。

3. 设计参考

参考了github上的如下源代码库:

https://github.com/brendan-myers/rpi3-wifi-conf.git

https://github.com/brendan-myers/rpi3-wifi-conf-android.git

简单来说,将树莓派当做server监听蓝牙的连接请求,然后通过andorid app作为客户端与树莓派进行数据交互,实现Wi-Fi的SSID和Password信息的传输。

上述树莓派上的server功能是基于python2的。我自己用python3重新实现,数据交互采用了JSON格式,同时也实现了一个基于树莓派的蓝牙客户端测试程序。蓝牙通信中的一些问题和Wi-Fi配置的功能也因为系统版本的不同进行了改进和适配。

4. Server实现

1)使用subprocess调用bluetoothctl power on和bluetoothctl discoverable on,使能蓝牙并设置为可发现状态。

subprocess.call(['bluetoothctl', 'power', 'on'])
subprocess.call(['bluetoothctl', 'discoverable', 'on'])

2)通过socket,以RFCOMM协议方式绑定蓝牙接口,启动socket监听,等待蓝牙客户端的连接。

sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock.bind(("", bluetooth.PORT_ANY))
sock.listen(1)

port = sock.getsockname()[1]

client_sock, client_info = server_sock.accept()

3)当有client连接后,等待client发来的命令:

while True:
    # Waiting for command from client
    print('Waiting for command from client')
    try:
        data = client_sock.recv(1024)
    except bluetooth.btcommon.BluetoothError:
        # Client connection closed
        print('Connection closed by client')
        client_sock.close()
        server_sock.close()
        break

    # Convert received data to JSON command
    data = data.decode('utf-8')
    print(f'RECV Command: {data}')

    try:
        json_data = json.loads(data)

        # json command processing
        cmd_data_proc(client_sock, json_data)
    except json.decoder.JSONDecodeError:
        print('Command is not in JSON format!')

client以JSON格式发送命令,如果收到非JSON数据则发生异常,重新接收命令。如果在recv阻塞过程中产生异常,说明连接出现中断,关闭当前的客户端连接,重启socket监听。

4)当接收到命令后,进行命令的解析和相应的处理:

def cmd_data_proc(sock, data):
    cmd_key = 'Command'

    if data.__contains__(cmd_key):
        if data[cmd_key] == 'GetWiFiScanList':
            send_wifi_info(sock)
        elif data[cmd_key] == 'SetWiFiParams':
            set_wifi_params(sock, data)
        elif data[cmd_key] == 'GetWiFiConnectionStatus':
            get_wifi_connect_status(sock)
        else:
            print('Ignore received unknown command and wait for next command...')

支持获取当前WiFi信号列表GetWiFiScanList,设置Wi-Fi参数SetWiFiParams和获取当前Wi-Fi连接状态GetWiFiConnectionStatus共3种命令,及相应的处理。

5)获取当前Wi-Fi信号列表GetWiFiScanList:

def get_wifi_info(interface):    
    # Get all detected Wi-Fi cells
    cells = Cell.all(interface)

    index = 1
    js = { 'Cells':[] }

    # Get info of each cell
    for cell in cells:
        if cell.ssid != '' and cell.ssid.find('\\x') < 0:
            js['Cells'].append(
                {
                    'id':index,
                    'ssid':cell.ssid.encode('raw_unicode_escape').decode('utf-8'),
                    'mac':cell.address,
                    'signal':cell.signal,
                    'frequency':cell.frequency,
                    'encrypted':cell.encrypted,
                    'quality':cell.quality
                }
            )
            index += 1

使用了wifi module中的Cell类获取接口当前扫描到的Wi-Fi信息,并提取需要的信息,输出为字典,再转为JSON格式

6)获得当前连接的Wi-Fi信息:

def get_connected_wifi_info(interface, key):
    js = { key:{} }

    # Get current Wi-Fi cell info if connected
    p = subprocess.Popen(['wpa_cli', '-i', interface, 'status'],
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    out, err = p.communicate()
    p.wait()
    result=out.decode().strip().split('\n')
    for l in result:
        if l.startswith('ssid='):
            js[key]['ssid']=l.split('=')[1]
        elif l.startswith('freq='):
            js[key]['freq']=l.split('=')[1]
        elif l.startswith('bssid='):
            js[key]['mac']=l.split("=")[1].upper()
        elif l.startswith('ip_address='):
            js[key]['ip']=l.split('=')[1]

    return js

调用wpa_cli -i wlan0 status命令,获得当前Wi-Fi连接状态,截取出当前连接的Wi-Fi信号信息。

7)设置Wi-Fi参数SetWiFiParams并连接所设置的Wi-Fi网络:

def run_wifi_connect(ssid, psk):
    wpa_supplicant_conf = "/etc/wpa_supplicant/wpa_supplicant.conf"

    # write wifi config to file
    with open(wpa_supplicant_conf, 'a+') as f:
        f.write('network={\n')
        f.write('    ssid="' + ssid + '"\n')
        f.write('    psk="' + psk + '"\n')
        f.write('}\n')

    # Restart wifi adapter
    subprocess.call(['sudo', 'ifconfig', wifi_interface_name, 'down'])
    time.sleep(2)

    subprocess.call(['sudo', 'ifconfig', wifi_interface_name, 'up'])
    time.sleep(6)

    subprocess.call(['sudo', 'killall', 'wpa_supplicant'])
    time.sleep(1)
    subprocess.call(['sudo', 'wpa_supplicant', '-B', '-i', wifi_interface_name, '-c', wpa_supplicant_conf])
    time.sleep(2)
    subprocess.call(['sudo', 'dhcpcd', wifi_interface_name])
    time.sleep(10)

从client发送来的命令SetWiFiParams中获取要设置的Wi-Fi的SSID和Password,写入wpa_supplicant.conf文件中,然后调用linux命令连接上相应的Wi-Fi网络。

8)获取当前Wi-Fi连接状态GetWiFiConnectionStatus

def get_connected_wifi_info(interface, key):
    js = { key:{} }

    # Get current Wi-Fi cell info if connected
    p = subprocess.Popen(['wpa_cli', '-i', interface, 'status'],
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    out, err = p.communicate()
    p.wait()
    result=out.decode().strip().split('\n')
    for l in result:
        if l.startswith('ssid='):
            js[key]['ssid']=l.split('=')[1]
        elif l.startswith('freq='):
            js[key]['freq']=l.split('=')[1]
        elif l.startswith('bssid='):
            js[key]['mac']=l.split("=")[1].upper()
        elif l.startswith('ip_address='):
            js[key]['ip']=l.split('=')[1]

    return js

调用wpa_cli命令,获得当前Wi-Fi接口的连接状态,从返回的数据中提取需要的SSID和IP信息,生成字典并发送给client。

9)JSON数据包的发送:

def send_json_data(sock, js):
    # Convert JSON data to byte stream
    data = bytes(js, encoding='utf-8')

    # Send byte stream length to client
    size = len(data)
    print(f'Send byte stream length {size}')
    sock.send(str(size).encode('utf-8'))
    
    # Send byte stream content to client
    print(f'Send byte stream content')
    sock.send(data)

发送JSON数据包时,做了一个先发送数据包长度,再发送数据包的处理,便于客户端根据数据包的长度知道要当前要接收的数据量。

上述为server端主要的功能实现。我使用了2个树莓派,一个树莓派4作为server,一个树莓派3作为client进行了测试,可以完成server的Wi-Fi配置,并成功连接上网络。

其中还有一些问题没有去仔细处理:

1)在写wpa_supplicant.conf时,直接进行的追加,没有去分析文件中是否已经存在了要写入的SSID信息。只要写入,就是简单的在文件末尾追加新的配置信息。

2)在调用linux一系列命令连接Wi-Fi网络时的几个延时函数。因为发现如果延时不够,当client调用GetWiFiConnectionStatus要获取当前连接状态时,可能还处在连接过程中会返回未连接上的状态,因此延迟用的比较多。

3)蓝牙的发送可能会抛出异常。因为仅仅是用来进行测试验证的,所以一些异常没有仔细去处理。

其实比较方便的还是用比如手机端来作为client,树莓派作为server,这样操作很方便。限于时间,手机端后面有时间再搞一个玩玩。

以上测试代码在Gitee开源:https://gitee.com/daniel-008/rpi_bt_wifi_cfg.git,欢迎测试提出宝贵意见。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值