构建一个WIFI室内定位系统

室内定位可以应用在很多场景,由于受到室内环境的限制,GPS信号无法有效的接收,这时可以利用室内的WIFI热点提供的信号强度来进行辅助定位。通常在室内都会有很多的WIFI热点,我们可以把室内的区域划分为多个网格,在每一个网格测量所接收到的WIFI热点的信号强度,根据这些信息来建立一个WIFI信号指纹库,以后我们就可以通过比对指纹库,来确定在室内的位置了。

手机APP测量WIFI信号

首先我们先编写一个APP,用于测量WIFI的信号强度并且上报给服务器保存。这里我采用了HBuilderX来编写,这个HBuilderX采用了HTML 5+的技术,可以快速的用我熟悉的网页+JS的方式来写Android和IOS的应用。

新建一个HbuilderX的项目,在目录下新建一个index.html文件,内容如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title></title>
	<link rel="stylesheet" type="text/css" href="css/jquery.dataTables.min.css">
	<script src="js/vconsole.min.js"></script>
	<script src="js/jquery-3.6.1.slim.js"></script>
	<script type="text/javascript" charset="utf8" src="js/jquery.dataTables.min.js"></script>
	<script src="js/axios.min.js"></script>
	<script>
	  // VConsole will be exported to `window.VConsole` by default.
	  var vConsole = new window.VConsole();
	</script>
</head>
<body>
	<div id="inputlocation">
		<p>输入室内网格编号: <input type="text" id='gridId'></p>
		<button id="submit" onclick="submitMeasurement()" disabled>开始测量</button>
	</div>
	<div id="orientation">Device Orientation: </div>-->
	<table id="wifilist" class="display">
	    <thead>
	        <tr>
	            <th>BSSID</th>
	            <th>Level</th>
	        </tr>
	    </thead>
	    <tbody>
	    </tbody>
	</table>
</body>
<script>
	var Context;
	var wifiManager;
	document.addEventListener('plusready', function(){
		//console.log("所有plus api都应该在此事件发生后调用,否则会出现plus is undefined。")
		document.getElementById("submit").disabled = false;
		$('#wifilist').DataTable( {
		    data: [],
			searching: false,
			paging: false,
		});
		Context = plus.android.importClass("android.content.Context");
		wifiManager = plus.android.runtimeMainActivity().getSystemService(Context.WIFI_SERVICE);
		if (window.DeviceOrientationEvent) {
			window.addEventListener('deviceorientation', deviceOrientionHandler, false);
		}else {
			alert('not support device oriention');
		}
	});
	function submitMeasurement() {
		var flag = plus.android.invoke(wifiManager, "startScan");
		if (flag) {
			var wifilist = plus.android.invoke(wifiManager, "getScanResults");
			var size = plus.android.invoke(wifilist, "size");
			var rows = [];
			var table = $('#wifilist').DataTable();
			var data = {
				"GridId": $('#gridId').val(),
				"Orientation": $('#orientation').text(),
				"WifiSignal": []
			}
			table.clear().draw();
			if (size>0) {
				for (var i=0;i<size;i++) {
					var sr = plus.android.invoke(wifilist, "get", i);
					var bssid = plus.android.getAttribute(sr, "BSSID");
					var level = plus.android.getAttribute(sr, "level");
					rows.push([bssid, level]);
					data.WifiSignal.push({
						"BSSID": bssid,
						"Level": level
					});
				}
				table.rows.add(rows).draw();
			}
			axios.create().post(
				'http://123.123.123.123:8080/senddata',
				data,
				{headers: {'Content-Type':'application/json'}}
			).then(
				res=>{
					if (res.status!=202) {
						alert("Measurement data upload failure! Error code:"+res.status.toString());
					}
					else {
						alert("Measurement data upload Success!");
					}
				}
			)
		}
	}
	function deviceOrientionHandler(eventData) {
		$('#orientation').text(eventData.alpha.toString());
	}
</script>
</html>

这里用到了plus.android来调用android的原生方法,例如通过调用WifiManager来对WIFI信号进行扫描,把扫描结果的BSSID和信号强度保存下来。另外HTML5的规范支持获取设备的方向信息,以0-360度来表示设备的朝向,因为设备指向不同的方向也会影响信号的强度,因此也需要记录这个信息。最后当点击开始扫描这个按钮的时候,就会把这些信息提交到后台的服务器,记录到数据库中。

要支持WIFI扫描,还需要在manifest.json文件里面设置相应的权限,按照Android文档的说法,Android 10及以上的版本还需要开启ACCESS_FINE_LOCATION,ACCESS_WIFI_STATE,CHANGE_WIFI_STATE的权限,以及设备需要启用位置信息服务。另外Android默认会对startScan有节流限制,即一定时间内限制调用的次数,可以在开发者选项->网络->WIFI扫描调节下进行关闭,取消限制。

以下是这个APP运行的效果:

WIFI测量APP

后台应用记录WIFI测量数据

编写一个后台应用,暴露一个API接口,用于接收APP上报的WIFI测量数据。

这里采用springboot+JPA+Postgresql的架构。

在start.spring.io网站里面新建一个应用,artifact名字为wifiposition,依赖里面选择spring web, JPA,打开应用,在里面新建一个名为WifiData的Entity类,代码如下:

package cn.roygao.wifiposition;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;

import com.alibaba.fastjson.JSONArray;
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;

@Entity
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
@Table(name = "wifidata")
public class WifiData {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private String gridId;
    private Float orientation;
    
    @Type(type = "jsonb")
    @Column (name="measurement", nullable = true, columnDefinition = "jsonb")
    private JSONArray measureArray;

    @CreationTimestamp
    private Date createdTime;

    public WifiData() {
    }

    public WifiData(String gridId, Float orientation, JSONArray measureArray) {
        this.gridId = gridId;
        this.orientation = orientation;
        this.measureArray = measureArray;
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getGridId() {
        return this.gridId;
    }

    public void setGridId(String gridId) {
        this.gridId = gridId;
    }

    public Float getOrientation() {
        return this.orientation;
    }

    public void setOrientation(Float orientation) {
        this.orientation = orientation;
    }

    public JSONArray getMeasureArray() {
        return this.measureArray;
    }

    public void setMeasureArray(JSONArray measureArray) {
        this.measureArray = measureArray;
    }
}

这个代码里面会保存measurement的JSON数组到PG的JSONB格式的数据列里面,因为hibernate默认没有提供这种类型,这里引入了com.vladmihalcea.hibernate.type.json.JsonBinaryType来提供支持。

在pom.xml里面需要添加以下的依赖:

<dependency>
	<groupId>com.vladmihalcea</groupId>
	<artifactId>hibernate-types-52</artifactId>
	<version>2.3.4</version>
</dependency>

新建一个名为WifiRepository的接口类,代码如下:

package cn.roygao.wifiposition;

import org.springframework.data.repository.CrudRepository;

public interface WifiDataRepository extends CrudRepository<WifiData, Long>{
    
}

新建一个名为WifiController的类,实现HTTP接口,代码如下:

package cn.roygao.wifiposition;

import java.util.logging.Logger;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

@RestController
public class WifiController {
    @Autowired
    private WifiDataRepository repository;

    private final static Logger LOGGER = Logger.getLogger(WifiController.class.getName());

    @PostMapping("/senddata")
    public ResponseEntity<String> sendData(@RequestBody JSONObject data) {
        Float orientation = data.getFloat("Orientation");
        String gridId = data.getString("GridId");
        JSONArray wifiSignal = data.getJSONArray("WifiSignal");
        repository.save(new WifiData(gridId, orientation, wifiSignal));
        return ResponseEntity.accepted().body("OK");
    }
}

在application.properties里面增加postgres的相关配置,如下:

spring.datasource.url= jdbc:postgresql://localhost:5432/wifidb
spring.datasource.username= postgres
spring.datasource.password= postgres

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update

运行./mvnw clean install进行编译打包,然后运行即可。

生成WIFI指纹库

现在有了测量的APP,我在家里选取了两个地点,分别测量WIFI的数据。每个地点我都分别对东、南、西、北四个方向测量十次,总共每个地点测量四十次。搜集到的数据都保存到后台的服务器。

然后我们就可以读取这些测量数据,生成WIFI指纹了。下面用Python pandas来处理数据。

import pandas as pd
import pickle

df = pd.read_sql_table('wifidata', 'postgresql://postgres@localhost:5432/wifidb')  

grids = ['home1', 'home2']
orien_query = {
    'north': 'orientation<10 or orientation>350',
    'east': 'orientation<100 and orientation>80',
    'south': 'orientation<190 and orientation>170',
    'west': 'orientation<280 and orientation>260',
}
grid_measure = {}
for grid in grids:
    df1 = df.query('grid_id=="'+grid +'"')
    grid_measure[grid] = {}
    for key in orien_query.keys():
        df2 = df1.query(orien_query[key])
        bssid_stat = {}
        for i in range(df2.id.count()):
            measure = df2.iloc[i].measurement
            for j in range(len(measure)):
                bssid = measure[j]['BSSID']
                level = measure[j]['Level']
                if bssid not in bssid_stat.keys():
                    bssid_stat[bssid] = [1, level]
                else:
                    count = bssid_stat[bssid][0]
                    bssid_stat[bssid][0] = count+1
                    bssid_stat[bssid][1] = (bssid_stat[bssid][1]*count+level)//(count+1)
        threshold = (int)(df2.id.count() * 0.8)
        delkeys = []
        for key1 in bssid_stat.keys():
            if bssid_stat[key1][0] < threshold:
                delkeys.append(key1)
            else:
                bssid_stat[key1][0] = bssid_stat[key1][0]/df2.id.count()
        for key1 in delkeys:
            del bssid_stat[key1]
        grid_measure[grid][key] = bssid_stat

with open('wififingerprint.pkl', 'wb') as f:
    pickle.dump(grid_measure, f)

处理后的wifi指纹数据如下格式:

{'home1': {'north': {'14:ab:02:6f:31:5c': [1.0, -60],
   '50:21:ec:d7:4e:24': [1.0, -63],
   '14:ab:02:6f:31:60': [1.0, -74],
   '18:f2:2c:cb:31:e9': [0.8, -75],
   '1a:f2:2c:ab:31:e9': [0.8, -75],
   '42:97:08:72:81:48': [0.8, -83],
   '18:f2:2c:cb:31:eb': [0.9, -89]},
  'east': {'14:ab:02:6f:31:5c': [1.0, -69],
   '50:21:ec:d7:4e:24': [1.0, -63],
   '14:ab:02:6f:31:60': [1.0, -75],
   '42:97:08:72:81:48': [0.8, -86],
   '74:c1:4f:29:27:35': [0.8, -80]},
  ...
}}}

解释一下,这个代码的作用是找出同一地点同一方向的多次测量中都有出现的BSSID,计算其出现概率以及多次信号强度测量的平均值。

现在我们可以做一下测试,在室内选取其他的一些测量点,记录测量数据之后和这个指纹库进行比对。

这里我选取多个测量点,其编号以及与我们的指纹库的两个地点的位置距离关系如下:

Test1:与Home1在同一房间内,距离Home1大约为2米。

Test2:在Home1旁边的房间,距离Home1大约为5米。

Test3:  与Home2都在客厅内,距离Home2大约为2米。

Test4:在走廊,大致位于Home1与Home2中间。

采集这些测试点的BSSID和信号强度,然后和指纹库进行比对,以下是测试代码:

df1 = df.query('grid_id=="test3"')
measure_test1 = df1.iloc[1].measurement
count = 0
dev = 0
orien = 'west'
truth_count = len(grid_measure['home2'][orien].keys())
for bssid in grid_measure['home2'][orien].keys():
    for item in measure_test1:
        if item['BSSID'] == bssid:
            count += 1
            dev += abs(item['Level']-grid_measure['home2'][orien][bssid][1])
            break

这个代码是取测试点的某个方向的测量数据与指纹库比对,看看测试点所获取的BSSID有百分之多少和指纹库的点的BSSID吻合,然后对相吻合的BSSID的信号强度计算差值,最后计算平均值。

从多次测试来看,当测试点在指纹库的点附近时,BSSID的吻合度大概在40%以上,信号强度的平均差值在5以内。因此可以根据这个阈值来把测试点的测量数据与指纹库的所有测量数据进行遍历,找到对应的指纹库的定位点。

当然这个是一个很粗糙的WIFI指纹定位的方法,业界在这方面也做了很多的研究,有时间的话我将参考业界的理论来进一步改进这个定位方法。

猜你喜欢

转载自blog.csdn.net/gzroy/article/details/127860197