电磁类信号定位的方法和原理都大同小异,只是会因为频段的不同而产生了电磁信号不同的损耗特点。所以5G定位也好,WiFi定位也好,算法原理都是差不多,不同的是基站和频谱。对于WiFi定位的实验,先从这种最基本的信号强度方法(这里为指纹定位)入手,通过一部安卓手机来进行信号强度数据的采集工作,下文包含了开发信号采集程序的整个流程。使用的工具如下:

  1. Android Studio(使用java开发)
  2. HUAWEI 荣耀8(Android版本7.0)

说明:苹果手机由于隐私保护,不便于获取WiFi数据;安卓手机尽管可以获取WiFi数据,但是由于扫描时延在系统层面上有限制(至少在1.5s以上),因此利用现有的框架做实时定位是不现实的,参考Android WIFI扫描时延

一、Android的helloworld程序

二、页面布局与程序基本结构

三、按钮与文本事件交互

四、安卓获取WiFi数据

五、安卓定时器使用

六、安卓写文件操作

七、导出数据

八、开发注意事项

九、关键代码


一、Android的helloworld程序

新建一个Empty Activity程序,直接点击运行就可以在手机里面看到helloworld了。

在此之前一定要开打手机的开发者模式,具体操作:在“关于手机”里面连续点击“版本号”7次,然后在“系统和更新”或“设置”中的开发人员选项里面打开“USB调试”。

 

二、页面布局与程序基本结构

做一个方便手机采集数据的程序,最终采集的数据能够保存到一个文件里面,程序本身除了按钮以外,能够显示一些基本的数据,同时按下停止以后,文件名称默认为时间戳;很有可能在一个定点采集二三十条同样的数据,因为数据采集过程可能存在不稳定的情况,因此,取一个定点的信号强度,可以对这二三十条数据进行处理,去掉异常数据,剩下的取平均值,从而得到一个相对稳健的信号强度数据。

新建一个空的程序之后,在里面进行布局,大概如图所示。

 

三、按钮与文本事件交互

页面中的文本加上两个按钮对应了不同的触发事件,以结束按钮事件监听为例(参考:安卓官方开发文档)。

首先在activity_main.xml里面添加按钮事件监听,见其中的“android:onClick=”,后面对应了响应函数。

<Button
    android:id="@+id/end_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="161dp"
    android:layout_marginLeft="161dp"
    android:layout_marginTop="130dp"
    android:layout_marginEnd="162dp"
    android:layout_marginRight="162dp"
    android:text="结束"
    android:onClick="saveFile"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/start_btn" />

然后在MainActivity.java中预写响应函数saveFile。

 /** Called when the user touches the "结束" button */
public void saveFile(View view) {
    // 将数据保存到本地文件
    LogUtil.i("END", "FILE WRITTEN");
    TextView textView = (TextView)findViewById(R.id.status_label);
    textView.setText("数据已保存……");
}

为了方便调试,且避免Android原生Log无法正常工作,自定义了一个工具类LogUtil,参考Android中日志打印 Log的使用,这个WiFi程序只是一个简单的程序,使用System.out.Println无伤大雅。

public class LogUtil {

    //设为false关闭日志
    private static final boolean LOG_ENABLE = true;

    public static void i(String tag, String msg){
        if (LOG_ENABLE){
            Log.i(tag, msg);
        }
    }
    public static void v(String tag, String msg){
        if (LOG_ENABLE){
            Log.v(tag, msg);
        }
    }
    public static void d(String tag, String msg){
        if (LOG_ENABLE){
            Log.d(tag, msg);
        }
    }
    public static void w(String tag, String msg){
        if (LOG_ENABLE){
            Log.w(tag, msg);
        }
    }
    public static void e(String tag, String msg){
        if (LOG_ENABLE){
            Log.e(tag, msg);
        }
    }

}

 

四、安卓获取WiFi数据

参考WLAN 扫描功能概览,扫描流程分为三步:

1. 为 SCAN_RESULTS_AVAILABLE_ACTION 注册一个广播侦听器;系统会在完成扫描请求时调用此侦听器,提供其成功/失败状态。对于运行 Android 10 (API 级别 29) 或更高版本 的设备,系统将针对平台或其他应用在设备上执行的所有完整 WLAN 扫描发送此广播。应用可以使用广播被动监听设备上所有扫描的完成情况,无需发出自己的扫描。

2. 使用 WifiManager.startScan() 请求扫描。请务必检查方法的返回状态,因为调用可能因以下任一原因失败:

  • 由于短时间扫描过多,扫描请求可能遭到节流。
  • 设备处于空闲状态,扫描已禁用。
  • WLAN 硬件报告扫描失败。

3. 使用 WifiManager.getScanResults() 获取扫描结果。系统返回的扫描结果为最近更新的结果,但如果当前扫描尚未完成或成功,则可能返回以前扫描的结果。也就是说,如果在收到成功的 SCAN_RESULTS_AVAILABLE_ACTION 广播前调用此方法,您可能会获得较旧的扫描结果。

参考Android 实时监听获取系统Wifi列表,首先定义一个工具类WifiUtil放在MainActivity.java同目录下(程序员小白:最后直接通过WifiUtil.fn的方式引用即可),里面包含了startScan与getScanResults部分。

public class WifiUtil {
    /**
     * 扫描之前需要刷新附近的Wifi列表,这需要使用startScan方法
     */
    public static void scanStart(Context context) {
        WifiManager mWifiManager = (WifiManager) context.getSystemService(WIFI_SERVICE);
        mWifiManager.startScan();
    }

    /**
     * 扫描附近wifi
     */
    public static List<ScanResult> scanWifiInfo(Context context) {
        WifiManager mWifiManager = (WifiManager) context.getSystemService(WIFI_SERVICE);
        List<ScanResult> mWifiList = new ArrayList<>();
        mWifiList.clear();
        if (!mWifiManager.isWifiEnabled()) {
            mWifiManager.setWifiEnabled(true);
        }
        mWifiList = mWifiManager.getScanResults();
        LogUtil.i("WIFI-SCAN", "mWifiList size  :" + mWifiList.size());
        return mWifiList;
    }
}

接下来包含注册广播与函数调用(具体细节我不是很理解,目前笔者只是想实现数据获取的功能),最终代码可以在本文最后查看。

public class MainActivity extends AppCompatActivity {
    private WifiBroadcastReceiver wifiReceiver;
    private List<ScanResult> wifiResultList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        registerWifiReceiver();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(wifiReceiver);
    }

    /** Called when the user touches the "开始" button */
    public void collectRSSI(View view) {
        // 读取wifi数据并保存
        LogUtil.i("START", "RSSI DATA COLLECTING");
        TextView textView = (TextView)findViewById(R.id.status_label);
        textView.setText("数据记录中……");

        WifiUtil.scanStart(this);
        // 上面函数里面包含了一个回调函数,不能实时显示数据,因此这里我增加一个部分
        // 没有实时数据的时候显示老数据
        LogUtil.i("REPEAT", wifiResultList.toString());
    }

    /** Called when the user touches the "结束" button */
    public void saveFile(View view) {
        // 将数据保存到本地文件
        LogUtil.i("END", "FILE WRITTEN");
        TextView textView = (TextView)findViewById(R.id.status_label);
        textView.setText("数据已保存……");
    }

    //注册广播
    private void registerWifiReceiver() {
        wifiReceiver  = new WifiBroadcastReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);//监听wifi是开关变化的状态
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);//监听wifi连接状态广播,是否连接了一个有效路由
        filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);//监听wifi列表变化(开启一个热点或者关闭一个热点)
        this.registerReceiver(wifiReceiver, filter);
    }

    //监听wifi状态
    public class WifiBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())) {
                LogUtil.i("SCAN", "网络已变化");
                scanWifi();
            }
        }
    }

    private void scanWifi() {
        wifiResultList = WifiUtil.scanWifiInfo(this);
    }
}

最终输出效果大概像这样,后面需要提取出MAC地址和RSSI值,这里略去,每次更新都直接更新了数组wifiResultList的值,因此后续定位采集数据的话直接访问wifiResultList即可,不过运行的时候发现网络变化间隔实验至少大于1.5s,因此做实验只能进行慢速实验。

 

五、安卓定时器使用

private Timer timer = new Timer();

// 定时调用
TimerTask timerTask = new TimerTask() {
    @Override
    public void run() {
        WifiUtil.scanStart(context);
        // do something here:
        
};

// 参数:fn、delay、interval
timer.schedule(timerTask, 0, 200);

// 停止使用
timer.cancel();

由于后面要实时更新控件Label文本的值,会提示Only the original thread that created a view hierarchy can touch its views的错误,所以这里将使用Handler。

// 1.首先创建一个Handler对象  
Handler handler=new Handler();  
// 2.然后创建一个Runnable对
Runnable runnable=new Runnable(){  
   @Override  
   public void run() {
     //要做的事情,这里再次调用此Runnable对象,以实现每两秒实现一次的定时器操作  
     handler.postDelayed(this, 2000);  
   }  
};  
// 3.使用PostDelayed方法,两秒后调用此Runnable对象  
handler.postDelayed(runnable, 2000);  
// 4.如果想要关闭此定时器,可以这样操作
handler.removeCallbacks(runnable);  

 

六、安卓写文件操作

读写文件,笔者定义了一个文件工具类FileUtil,参考数据存储与访问之-文件存储读写,这里我保存到了app的data目录下,注意也要在手机的权限管理里面将"存储"打开。

public class FileUtil {
    //写数据
    public static void writeFile(String fileName, String writeData, Context context) throws IOException {
        try{
            FileOutputStream fout = context.openFileOutput(fileName, MODE_PRIVATE);
            byte [] bytes = writeData.getBytes();
            fout.write(bytes);
            fout.close();
            LogUtil.i("FILE", "文件已保存……");
        }

        catch(Exception e){
            e.printStackTrace();
        }
    }

    /*
     * 这里定义的是文件读取的方法
     * */
    public static String readFile(String filename, Context context) throws IOException {
        //打开文件输入流
        FileInputStream input = context.openFileInput(filename);
        byte[] temp = new byte[1024];
        StringBuilder sb = new StringBuilder("");
        int len = 0;
        //读取文件内容:
        while ((len = input.read(temp)) > 0) {
            sb.append(new String(temp, 0, len));
        }
        //关闭输入流
        input.close();
        return sb.toString();
    }
}

同样,这里也有权限问题,下面第三项为SD卡的权限设置,本实验未涉及可以忽略。

<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- If you need to modify files in external storage, request
             WRITE_EXTERNAL_STORAGE instead. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="18" />

 

七、导出数据

可以使用adb(Android Debug Bridge)工具导出文件,为了方便操作,在环境变量里面配置了adb,这样方便全局操作。

后面操作的时候会涉及到权限问题,所以可以提前设定,在adb shell里面使用chmod 777 data,参考adb控台中Permission denied的解决方案

> adb shell
$ su
# chmod 777 data

剩下就是导出文件了,分为两步,第一步将内存文件导出到SD卡里面。

> adb shell
$ su
# cp -R /data/data/com.example.data /sdcard/documents

然后进入想要保存文件的电脑目录下打开shell,从SD卡里面导出到电脑中。

> adb pull /sdcard/documents

关于为什么需要分两步,而不是一步了事,参见使用adb在电脑和手机间传文件,该参考博客里面解决1似乎有误,/data/local目录在笔者这里也是需要权限才能进入的,因此会出现同样的错误,但是/sdcard/documents不需要权限,因此这里使用这个目录。

另外可能用得上的命令:rmdir删除文件夹,rm删除文件(rm -rf *为删除所有文件)

 

八、开发注意事项

  1. 在采集WiFi数据的时候需要打开手机的“位置”服务和权限管理中的”位置“权限,不然会getScanResults返回的是空数组;
  2. 文件读写请打开手机的”存储“权限,不然会写入失败。
  3. 苹果设备获取WiFi数据需要苹果公司授权,参考iOS 获取系统wifi列表,wifi信号强度,iOS获取WiFi数据可以参考iOS Wi-Fi管理API,从笔者了解的情况来看,iOS用来做实验非常麻烦。
  4. 安卓系统安卓手机尽管可以获取WiFi数据,但是由于扫描时延在系统层面上有限制(至少在1.5s以上),因此利用现有的框架做实时定位是不现实的,参考Android WIFI扫描时延
  5. 为了实验方便,最终的代码扫描wifi的同时,筛选出了做实验的几个AP。
  6. 由于笔者更关心最终能够实现效果,因此对于一些开发细节不求甚解,有兴趣的同仁可以在这个基础上钻研,希望能够有帮助。

 

九、关键代码

已上传github:https://github.com/salmoshu/location/tree/master/android

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐