Build a WIFI indoor positioning system

Indoor positioning can be applied in many scenarios. Due to the limitation of indoor environment, GPS signals cannot be received effectively. At this time, the signal strength provided by indoor WIFI hotspots can be used for auxiliary positioning. Usually there are many WIFI hotspots indoors, we can divide the indoor area into multiple grids, measure the signal strength of the received WIFI hotspots in each grid, and build a WIFI signal fingerprint library based on these information, In the future, we can determine the indoor position by comparing the fingerprint library.

Mobile APP to measure WIFI signal

First of all, we first write an APP to measure the signal strength of WIFI and report it to the server for storage. Here I use HBuilderX to write. This HBuilderX uses HTML 5+ technology, which can quickly write Android and IOS applications in the way of webpage + JS that I am familiar with.

Create a new HbuilderX project, create a new index.html file in the directory, the content is as follows:

<!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>

Here, plus.android is used to call the native method of android, for example, scan the WIFI signal by calling WifiManager, and save the BSSID and signal strength of the scanning result. In addition, the HTML5 specification supports obtaining the direction information of the device, and the orientation of the device is represented by 0-360 degrees, because different directions of the device will also affect the strength of the signal, so this information also needs to be recorded. Finally, when the button to start scanning is clicked, the information will be submitted to the background server and recorded in the database.

To support WIFI scanning, you also need to set the corresponding permissions in the manifest.json file. According to the Android documentation, Android 10 and above versions also need to enable ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, CHANGE_WIFI_STATE permissions, and the device needs to enable location information services. In addition, Android will have a throttling limit on startScan by default, that is, limit the number of calls within a certain period of time. You can turn it off and cancel the limit under Developer Options -> Network -> WIFI Scanning Adjustment.

The following is the effect of this APP operation:

WIFI measurement APP

Background application records WIFI measurement data

Write a background application that exposes an API interface for receiving WIFI measurement data reported by the APP.

The architecture of springboot+JPA+Postgresql is adopted here.

Create a new application in the start.spring.io website, the artifact name is wifiposition, select spring web, JPA in the dependencies, open the application, and create a new Entity class named WifiData in it, the code is as follows:

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;
    }
}

This code will save the JSON array of measurement to the data column in JSONB format of PG, because hibernate does not provide this type by default, and com.vladmihalcea.hibernate.type.json.JsonBinaryType is introduced here to provide support.

The following dependencies need to be added in pom.xml:

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

Create a new interface class named WifiRepository, the code is as follows:

package cn.roygao.wifiposition;

import org.springframework.data.repository.CrudRepository;

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

Create a new class named WifiController to implement the HTTP interface, the code is as follows:

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");
    }
}

Add postgres related configuration in application.properties, as follows:

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

Run ./mvnw clean install to compile and package, and then run it.

Generate WIFI fingerprint library

Now that I have a measurement app, I have selected two locations at home to measure WIFI data respectively. For each location, I measured the four directions of east, south, west, and north ten times, for a total of forty times at each location. The collected data is saved to the background server.

Then we can read these measurements and generate WIFI fingerprints. Let's use Python pandas to process the data.

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)

The processed wifi fingerprint data is in the following format:

{'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]},
  ...
}}}

To explain, the function of this code is to find the BSSID that occurs in multiple measurements in the same direction at the same location, calculate its occurrence probability and average the multiple signal strength measurements.

Now we can do a test, select some other measurement points indoors, record the measurement data and compare it with this fingerprint library.

Here I select a number of measurement points, and their numbers and the distance relationship with the two locations of our fingerprint library are as follows:

Test1: In the same room as Home1, about 2 meters away from Home1.

Test2: The room next to Home1 is about 5 meters away from Home1.

Test3: Both Home2 and Home2 are in the living room, about 2 meters away from Home2.

Test4: In the corridor, roughly between Home1 and Home2.

Collect the BSSID and signal strength of these test points, and then compare them with the fingerprint library. The following is the test code:

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

This code is to compare the measurement data in a certain direction of the test point with the fingerprint library to see how many percent of the BSSID obtained by the test point matches the BSSID of the point in the fingerprint library, and then compare the signal strength of the matched BSSID Calculate the difference and finally the average.

From multiple tests, when the test point is near the point of the fingerprint database, the matching degree of BSSID is about 40%, and the average difference of signal strength is within 5. Therefore, the measurement data of the test point and all the measurement data of the fingerprint library can be traversed according to this threshold to find the corresponding anchor point of the fingerprint library.

Of course, this is a very rough method of WIFI fingerprint positioning. The industry has done a lot of research in this area. If I have time, I will refer to the theory of the industry to further improve this positioning method.

Guess you like

Origin blog.csdn.net/gzroy/article/details/127860197