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信息: