声波配网,即通过手机发出声波,将ssid、password等信息传给设备的一种配网方式。适用于没有触屏或触屏较小不易于信息输入,但是拥有麦克风的智能设备,如智能音箱、智能家庭助手等。其优点是配网速度快、可人耳感知,缺点是受环境干扰较大。
实现声波配网,首先需要一套特定的算法库(我司有专门的算法部门在做,由于保密的原因,算法库不能公开),算法库分手机端和设备端两部分。手机端算法库将ssid信息由字符串转化为声音信号(PCM),然后将声音信号通过音频模块播放出来。同时,设备端录下这一段声音,然后用同一套算法库将声音信息解析出来,还原成原来的ssid信息(字符串),最后用解析到的ssid信息用于连接wifi。
声波配网主要流程如下:
(1)首先,在手机(或平板等其它一代设备)输入ssid信息(或获取当前或系统保存的ssid信息),使用我司提供的算法库,将信息由buffer编码为pcm数据;
(2)将使用算法库编码出来的pcm数据通过喇叭播放出来,同时,设备端打开录音,捕获pcm数据;
(3)设备端将pcm数据通过算法库解码回原来的buffer数据;
(4)从数据中解析出ssid、password等信息,并将其用于连接路由器。
编解码可选择范围分为低频、中频、高频三种,其中低频的频率范围为2K~5K,中频的范围为8K~12K,高频的范围为16K~20K。频率越高,声音越尖锐,抗噪性能越强。
手机端实现
此处已Android端实现为例。
在Android系统上实现声波配网时,需要通过jni调用我司提供的算法库。
接收端的代码由于用的是我司自己做的系统,代码不能公开,而写出来也没有意义,总的一个实现思路就是,接收声波,并将声波用算法库转换会原来的ssid等信息。
package com.aw.soundauthenticationtest; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import com.aw.SoundAuthentication; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.app.Activity; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends Activity { private final String TAG = "llh>>>MainActivity"; private AudioTrack mAudioTrack; private SoundAuthentication mSoundAuthentication; private EditText mInputSsidEditText; private EditText mInputPasswordEditText; private Button mStartBt; private File mSaveTransferedPcmFile; private DataOutputStream mDataOutputStream; private final int ENABLE_START_BUTTON_MSG = 0; private final int DISABLE_START_BUTTON_MSG = 1; private int mMaxStrLen = 128; private int mSampleRate = 44100; private int mFreqType = 1;//0 is low freq,1 is middle freq,2 is high freq private int mErrorCorrect = 1; private int mErrorCorrectNum = 4; private int mGroupSymbolNum = 10; private Handler mHandler = new Handler(){ public void handleMessage(android.os.Message msg) { switch (msg.what) { case ENABLE_START_BUTTON_MSG: mStartBt.setEnabled(true); break; case DISABLE_START_BUTTON_MSG: mStartBt.setEnabled(false); break; default: break; } }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, "onCreate"); mSoundAuthentication = new SoundAuthentication(); mInputSsidEditText = (EditText)findViewById(R.id.ssid_editText_id); mInputPasswordEditText = (EditText)findViewById(R.id.password_editText_id); mStartBt = (Button)findViewById(R.id.start_bt_id); mStartBt.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // TODO Auto-generated method stub Log.d(TAG, "mStartBt is clicked"); if(mInputPasswordEditText.getText().toString().length() < 8){ Toast toast = Toast.makeText(MainActivity.this, "the password length is less than 8,please input more data", Toast.LENGTH_LONG); toast.setGravity(Gravity.TOP, 50, 220); toast.show(); }else{ mStartBt.setEnabled(false); // creatTransferFile(); new Thread(){ public void run() { String tmpInputSsidStr = mInputSsidEditText.getText().toString(); String tmpInputPasswordStr = mInputPasswordEditText.getText().toString(); String inputSsidStr = null; String inputPasswordStr = null; try { byte[] tmpSsidByte = tmpInputSsidStr.getBytes(); byte[] tmpPasswordByte = tmpInputPasswordStr.getBytes(); inputSsidStr = new String(tmpSsidByte, "utf-8"); inputPasswordStr = new String(tmpPasswordByte, "utf-8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } String allWifiStr = null; if(inputSsidStr.equals("")){//没有手动输入wifi名称,所以直接使用已连接的那个wifi名称 Log.d(TAG, "do not input ssid"); //把wifi的ssid和password组成一个字符串,用"::div::"这个字符串把他们分隔开,接收端也要根据这个分隔字符串取出相应的ssid和password allWifiStr = getSSIDname()+"::div::"+inputPasswordStr; }else{//手动输入了wifi名称,所以用手动输入的那个wifi名称 Log.d(TAG, "manual input ssid:"+inputSsidStr); //把wifi的ssid和password组成一个字符串,用"::div::"这个字符串把他们分隔开,接收端也要根据这个分隔字符串取出相应的ssid和password allWifiStr = inputSsidStr+"::div::"+inputPasswordStr; } mSoundAuthentication.setDebugFlag(true); mSoundAuthentication.setEnDecoderParameters(mMaxStrLen,mSampleRate,mFreqType,mErrorCorrect,mErrorCorrectNum,mGroupSymbolNum); short[] pcm_data = mSoundAuthentication.nativeEncodeStrToPcm(allWifiStr);//把需要编码的字符串(allWifiStr)传给编码器 if(pcm_data == null){//编码失败 Log.e(TAG, "encode error"); }else{//编码成功,返回值为该字符串编码后的pcm数据 Log.d(TAG, "pcm_data len = "+pcm_data.length); // savePcmDataInFile(pcm_data); playPcm(pcm_data);//把编码后的pcm数据拿去播放 stopPcm();//停止播放 // closeFile(); } mHandler.sendEmptyMessage(ENABLE_START_BUTTON_MSG); }; }.start(); } } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } /*注:以下两个参数不能改变:采样精度为 16bit , 采样通道为:单声道(mono)。采样率需要和接收端的采样率一致,一般设为44100*/ private void playPcm(short[] pcmData){ int index = 0; int minBufSizeInByte = AudioTrack.getMinBufferSize(mSampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); Log.d("llh>>>", "minBufSizeInByte = "+minBufSizeInByte); if(mAudioTrack == null){ mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufSizeInByte, AudioTrack.MODE_STREAM); } int minBufSizeInShrot = minBufSizeInByte/2; mAudioTrack.play(); while((index*minBufSizeInShrot) < pcmData.length){ if((pcmData.length - index*minBufSizeInShrot)>=minBufSizeInShrot){ mAudioTrack.write(pcmData, index*minBufSizeInShrot, minBufSizeInShrot); }else{ mAudioTrack.write(pcmData, index*minBufSizeInShrot, pcmData.length - index*minBufSizeInShrot); } index++; } } private void stopPcm(){ if(mAudioTrack != null){ mAudioTrack.flush(); mAudioTrack.stop(); mAudioTrack = null; } } private String getSSIDname(){ String ssidName = null; WifiManager wifiManager = (WifiManager)getSystemService(WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); String tempSsidName = wifiInfo.getSSID().substring(1, wifiInfo.getSSID().length()-1); try { ssidName = new String(tempSsidName.getBytes(), "utf-8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } Log.d(TAG, "ssid name = "+ssidName); Log.d(TAG, "ssid name len = "+ssidName.length()); Log.d(TAG, "bssid name = "+wifiInfo.getBSSID()); Log.d(TAG, "wifi info = "+wifiInfo.toString()); return ssidName; } /*注:以下3个函数是调试的时候为了保存数据用的,实际使用不需要用到这3个函数。 * creatTransferFile(),savePcmDataInFile(short[] inputPcmData),closeFile()*/ private void creatTransferFile(){ //在这里我们创建一个文件,用于保存字符串转换成pcm的内容 File fpath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/llhtest/"); fpath.mkdirs();//创建文件夹 try { //创建临时文件,注意这里的格式为.pcm mSaveTransferedPcmFile = File.createTempFile("transfer", ".pcm", fpath); Log.d(TAG, "record file path name = "+mSaveTransferedPcmFile.getAbsolutePath()); mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mSaveTransferedPcmFile))); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void savePcmDataInFile(short[] inputPcmData){ if(mDataOutputStream != null){ for(int i=0; i < inputPcmData.length; i++){ try { mDataOutputStream.writeShort(inputPcmData[i]); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } private void closeFile(){ if(mDataOutputStream != null){ try { mDataOutputStream.close(); mDataOutputStream = null; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }