树莓派上有非常丰富的接口,不过有个小问题就是,如果没有屏幕,串口或者有线网络,只能依赖于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,欢迎测试提出宝贵意见。