softAP配网,即利用设备的无线芯片,将设备进入到softAP模式,开启一个无线局域网,手机(或其它移动设备)通过连入设备开启的无线局域网后,向设备发送路由器的ssid及password等信息,让设备在无屏幕的情况下,获取到路由器的ssid信息,达到联网的目的。
配网的流程其实还是比较繁杂的,手机要先和原来的路由器断开连接,然后设备开softap,然后手机连入softap,然后发送ssid和密码,然后关闭softap模式,退回到正常的station模式,然后设备连接路由器,手机也视情况重新连接路由器。但是这个过程是可以在软件端整合起来的,以达到一键联网的结果,断开和连接的操作都整合起来。在这方面小米的体验就做得很好。我本来是比较推崇无线数据帧配网的方式的,因为在软件端只要不断地往空气中发无线数据包就可以了,设备端也只需要捕捉到这些包就可以了。但是无线数据帧配网的方式极不稳定,配网成功率低,受环境、手机型号、路由器型号、设备laytout和无线芯片的选型影响较大。而softAp配网,本身稳定性较高,通过开发者的整合,可以达到和无线数据帧配网一样的用户体验。
softAp配网流程图如下:
我这里实现配网的两端设备,一边是一个跑Linux系统的开发板,另一边是我们日常用的普通Android手机。如图:
设备端的linux系统,需要移植好wifi芯片对应的驱动,并且可以配置softAp模式(p2p模式),这个一般是有wifi芯片厂商在fw里实现好的,我们只要切换节点,加载驱动,push fw就可以了。不同的厂商实现的方法可能有异,我在此就不介绍了。
基于已经移植好wifi驱动的设备,我这里介绍如何在应用层实现softAP配网的demo。
设备端实现
设备端需要实现的流程有如下:
(1)将设备进入到softAP模式,需要所使用的wifi芯片支持spftAP模式;
(2)创建一个socket用于接收手机端发来的ssid信息,demo中使用广播的方式收发信息;
(3)接收手机发来的ssid信息;
(4)收到手机发来的ssid信息后返回确认信息,让手机暂时停止发送ssid信息并断开和设备端的连接(即使不主动断开在设备端退出softAP模式之后,手机也会因为找不到softAP而被动断开连接);
(5)退出sopAP模式;
(6)解析从手机端得到的ssid、password等信息;
(7)连接路由器;
(8)结束。如因ssid信息错误等原因,未能成功连上路由器,可根据用户体验需求重新开始配网流程,该循环的触发流程,请根据实际功能定位自行设计。
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <net/if.h>
#include <errno.h>
#include <sys/ioctl.h>
#include "include/smartlink.h"
#define IP_SIZE 16
#define MSG_LEN_MX 256
#define CMD_LEN_MX 128
#define PORT_DEFAULT 8066
//server
int main(int argc, char **argv){
int port = PORT_DEFAULT;
if(argc >=2){
port = atoi(argv[1]);
}
printf("*****softAP demo*****\n");
//先把设备设置为softAP模式
if(set_softAP_up()==0){
printf("start softAP success.\n");
}
/*创建socket接收手机发来的ssid信息,并返回确认信息*/
//创建socket
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
printf("port:%d \n", port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int sock;
if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
printf("socket error!\n");
printf("%s \n", strerror(errno));
exit(1);
}else{
printf("socket success.\n");
}
//printf("addr:%s \n",addr.sin_addr.s_addr);
if(bind(sock, (struct sockaddr *)&addr, sizeof(addr))< 0){
printf("bind error!\n");
printf("%s \n", strerror(errno));
exit(1);
}else{
printf("bind success.\n");
}
//接收手机发来的ssid信息
char buff[256];
struct sockaddr_in clientAddr;
int len = sizeof(clientAddr);
int n;
while(1){
printf("recvfrom...\n");
n = recvfrom(sock, buff, 511, 0, (struct sockaddr*)&clientAddr, &len);
printf("addr: %s -port: %u says: %s \n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port), buff);
if(n>0){
printf("recvfrom success.\n");
break;
}
}
//往手机返回确认信息
char buff_confirm[3];
strcpy(buff_confirm,"OK");
while(1){
n = sendto(sock, buff_confirm, n, 0, (struct sockaddr *)&clientAddr, sizeof(clientAddr));
printf("addr: %s -port: %u says: %s \n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port), buff_confirm);
if(n>0){
printf("sendto success.\n");
//usleep(10*1000000);
break;
}
}
usleep(5*1000000);
close(sock);
//设备退出softAP模式,退出后将自动恢复到station模式
if(set_softAP_down()==0){
printf("down softAP success.\n");
}
usleep(5*1000000);
//解析设备端发过来的ssid信息
char* smartlink_softAP_ssid;// = "12345678";
char* smartlink_softAP_password;// = "12345678";
char *p =NULL;
int ssid_len = strstr(buff, "::Password::")-buff-8;
printf("ssid_len %d\n",ssid_len);
smartlink_softAP_ssid = (char *)malloc(ssid_len+1);
if((p = strstr(buff, "::SSID::")) != NULL){
p += strlen("::SSID::");//跳过前缀
if(*p){
if(strstr(p, "::Password::") != NULL){
memset((void*)smartlink_softAP_ssid,'\0',ssid_len+1);
strncpy(smartlink_softAP_ssid, p, ssid_len);
}
}
}
printf("%s\n",smartlink_softAP_ssid);
int password_len = strstr(buff, "::End::") - strstr(buff, "::Password::") - 12;
printf("password_len %d\n",password_len);
smartlink_softAP_password = (char *)malloc(password_len+1);
if((p = strstr(buff, "::Password::")) != NULL){
p += strlen("::Password::");//跳过前缀
if(*p){
if(strstr(p, "::End::") != NULL){
memset((void*)smartlink_softAP_password,'\0',password_len+1);
strncpy(smartlink_softAP_password, p, password_len);
}
}
}
printf("%s\n",smartlink_softAP_password);
//使用解析到的ssid信息连接wifi
smartlink_softAP_connect_ap(smartlink_softAP_ssid, smartlink_softAP_password);
free(buff);
free(smartlink_softAP_ssid);
free(smartlink_softAP_password);
return 0;
}
手机端实现
手机端需要实现的流程如下:
(1)检查手机当前wifi状态,如未开启需先开启wifi,如已连接某路由器需与该路由器断开连接;
(2)搜索设备开启的wifi,直到找到或超时为止;
(3)连入设备开启wifi;
(4)创建socket,与设备进行通信,demo中选用广播的方式进行通信;
(5)开启接收线程,等待接收设备端发来的确认信息。接收线程建议在发送线程开启前先开启,以免收不到确认信息;
(6)开启发送线程,向设备发送ssid信息;
(7)在收到设备发送的确认信息后,断开与设备的连接(即使不主动断开,也会在设备退出softAP模式后,手机也会因为找不到AP而被动断开连接);
(8)重新连入路由器。可在重新连入路由器后询问设备是否也成功连入路由器,如没有可重新进行配网,设备端也要做相应的操作,具体可根据场景需求自行设计。
Android手机需要实现的权限:
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
代码示例
package com.example.chenkunyao.ckysoftapdemo;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import com.example.chenkunyao.ckysoftapdemo.tools.SmartlinkInfo;
import com.example.chenkunyao.ckysoftapdemo.tools.WifiAdmin;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class MainActivity extends AppCompatActivity {
private Button btnSend;
private Button btnStop;
private int sendNum=0;
int recvNum=0;
private EditText etTargetIP;
private EditText etTargetPort;
private EditText etLocalIP;
private EditText etLocalPort;
private EditText etSSID;
private EditText etPassword;
private Spinner spEncrypt;
private String strTargetIP;
private String strTargetPort;
private String strLocalIP;
private String strLocalPort;
private String strSSID;
private String strPassword;
private String strEncrypt;
private Button btnConnect;
DatagramSocket socket;
InetAddress addr;
public int STOPNUM = 1024;
public String SOTFAPSSID = "Smart-AW-HOSTAPD";
private Context context;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context=this.getBaseContext();
init();
}
private void init(){
btnSend = (Button) this.findViewById(R.id.btnSend);
btnSend.setOnClickListener(click);
btnStop = (Button) this.findViewById(R.id.btnStop);
btnStop.setOnClickListener(click);
etTargetIP= (EditText) findViewById(R.id.etTargetIP);
etTargetPort= (EditText) findViewById(R.id.etTargetPort);
etLocalIP= (EditText) findViewById(R.id.etTargetPort);
etLocalPort= (EditText) findViewById(R.id.etLocalPort);
etSSID= (EditText) findViewById(R.id.etSSID);
etPassword= (EditText) findViewById(R.id.etPassword);
spEncrypt= (Spinner) findViewById(R.id.spEncrypt);
spEncrypt.setOnItemSelectedListener(select);
spEncrypt.setSelection(1,true);
strEncrypt= String.valueOf(spEncrypt.getSelectedItemId());
btnConnect= (Button) findViewById(R.id.btnConnect);
btnConnect.setOnClickListener(click);
}
private Spinner.OnItemSelectedListener select= new Spinner.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String[] languages = getResources().getStringArray(R.array.encrypt);
Log.e("Encrypt:",languages[position]);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
};
private Button.OnClickListener click =new Button.OnClickListener(){
public void onClick(View v){
switch (v.getId()){
case R.id.btnStop:{
sendNum = STOPNUM;
recvNum = STOPNUM;
break;
}
case R.id.btnConnect:{
SmartlinkInfo info = new SmartlinkInfo();
info.ssid=SOTFAPSSID;
info.password="wifi1111";
info.wlansecurity=3;
//获取屏幕上输入的IP\port\ssid等信息
strTargetIP= String.valueOf(etTargetIP.getText());
strTargetPort= String.valueOf(etTargetPort.getText());
strLocalIP= String.valueOf(etLocalIP.getText());
strLocalPort= String.valueOf(etLocalPort.getText());
strSSID= String.valueOf(etSSID.getText());
strPassword = String.valueOf(etPassword.getText());
Log.e("Info",strTargetIP+" "+strTargetPort);
strSSID= String.valueOf(etSSID.getText());
strPassword = String.valueOf(etPassword.getText());
try {
socket = new DatagramSocket(Integer.parseInt(strTargetPort));
socket.setBroadcast(true);
addr = InetAddress.getByName(strTargetIP);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
}
connectWifi(info);
break;
}
default:break;
}
}
};
private int connectWifi(SmartlinkInfo info) {
WifiAdmin mWifiAdmin = new WifiAdmin(this);
mWifiAdmin.openWifi();
boolean FOUNDSOFTAP=false;
for(int j=0;j<20;j++) {
mWifiAdmin.startScan();
for (int i = 0; i < mWifiAdmin.getWifiList().size(); i++) {
//Log.e("WIFIINFO", i+": " + mWifiAdmin.getWifiList().get(i).SSID);
if (mWifiAdmin.getWifiList().get(i).SSID.equals(SOTFAPSSID)) {
FOUNDSOFTAP=true;
Log.e("WIFIINFO", i+": " + mWifiAdmin.getWifiList().get(i).SSID);
Log.e("WIFIINFO", "FOUND SOFTAP.");
break;
}
}
if(FOUNDSOFTAP){
mWifiAdmin.creatWifiLock();
WifiConfiguration config = mWifiAdmin.CreateWifiInfo(info.ssid,info.password,info.wlansecurity);
mWifiAdmin.addNetwork(config);
mWifiAdmin.releaseWifiLock();
break;
}
}
if (context != null) {
Log.e("SLEEP","if (context != null)");
ConnectivityManager mConnectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
Log.e("SLEEP"," ConnectivityManager mConnectivityManager = (ConnectivityManager) context");
//NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
//Log.e("SLEEP"," NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();");
while (mConnectivityManager.getActiveNetworkInfo()==null) {//WIFI_STATE_ENABLING = 2;
Log.e("SLEEP", "mNetworkInfo==null sleep 2 s~");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while (!mConnectivityManager.getActiveNetworkInfo().isConnected()) {//WIFI_STATE_ENABLING = 2;
Log.e("SLEEP", "!mNetworkInfo.isConnected() sleep 2 s~");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.e("mNetworkInfo","mNetworkInfo == null");
}
//打开接收线程
Thread recvThread = new Thread(recvRunnable);
recvThread.start();
//打开发送线程
Thread sendThread = new Thread(sendRunnable);
sendThread.start();
/*
mWifiAdmin.creatWifiLock();
WifiConfiguration config = mWifiAdmin.CreateWifiInfo(info.ssid,info.password,info.wlansecurity);
mWifiAdmin.addNetwork(config);
mWifiAdmin.releaseWifiLock();
*/
return 0;
}
private Runnable sendRunnable = new Runnable() {
@Override
public void run() {
String strInfo="::SSID::"+strSSID+"::Password::"+ strPassword+"::End::";
try {
byte[] buffer = strInfo.getBytes();
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
packet.setAddress(addr);
packet.setPort(Integer.parseInt(strTargetPort));
for(sendNum=0;sendNum<60;sendNum++) {
socket.send(packet);
Thread.sleep(1000);
Log.e("Send","Num:"+sendNum+" "+strInfo);
}
} catch (Exception e) {
e.printStackTrace();
Log.e("发送线程",e+"");
}
/*
SmartlinkInfo info = new SmartlinkInfo();
info.ssid=strSSID;
info.password=strPassword;
info.wlansecurity=3;
connectWifi(info);
*/
}
};
private Runnable recvRunnable = new Runnable() {
@Override
public void run() {
try {
byte[] buffer ="XX".getBytes();
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
packet.setAddress(addr);
packet.setPort(Integer.parseInt(strTargetPort));
for(recvNum=0;recvNum<65;recvNum++) {
Log.e("recv", "正在接收..."+recvNum );
socket.receive(packet);
if(new String(packet.getData()).equals("OK")){
sendNum = STOPNUM;
Log.e("recv", "收到消息:" + new String(packet.getData()));
break;
}
Thread.sleep(1000);
}
/*
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
//mTextView.setText("收到消息" + new String(packet.getData()) + ":" + String.valueOf(++index));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
*/
//socket.close();
} catch (Exception e) {
e.printStackTrace();
Log.e("接收线程",e+"");
}
}
};
}
手机端界面:
设备端收到手机端发来的广播,解析出wifi密码并返回wifi信息: