This will be the most detailed, easy to follow tutorial over the Web on how to implement Wifi indoor positioning using an Arduino microcontroller and Machine Learning. It contains all the steps, tools and code from the start to the end of the project.
ri-elaborated from https://www.accuware.com/blog/ambient-signals-plus-video-images/
My original post abot Wifi indoor positioning is one of my top-performing post of all time (after motion detection using ESP32 camera and the introductory post on Machine Learning for Arduino). This is why I settled to revamp it and add some more details, tools and scripts to create the most complete free guide on how to implement such a system, from the beginning to the end.
This post will cover all the necessary steps and provide all the code you need: for an introduction to the topic, I point you to the original post.
Table of contents
Features definition
This part stays the same as the original post: we will use the RSSIs (signal strength) of the nearby Wifi hotspots to classifiy which location we're in.
Each location will "see" a certain number of networks, each with a RSSI that will stay mostly the same: the unique combination of these RSSIs will become a fingerprint to distinguish the locations from one another.
Since not all networks will be visible all the time, the shape of our data will be more likely a sparse matrix.
A sparse matrix is a matrix where most of the elements will be zero, meaning the absence of the given feature. Only the relevant elements will be non-zero and will represent the RSSI of the nth network.
The following example table should give you an idea of what our data will look like.
Location | Net #1 | Net #2 | Net #3 | Net #4 | Net #5 | Net #6 | Net #7 |
---|---|---|---|---|---|---|---|
Kitchen/1 | 50 | 30 | 60 | 0 | 0 | 0 | 0 |
Kitchen/2 | 55 | 30 | 55 | 0 | 0 | 5 | 0 |
Kitchen/3 | 50 | 35 | 65 | 0 | 0 | 0 | 5 |
Bedroom/1 | 0 | 80 | 0 | 80 | 0 | 40 | 40 |
Bedroom/2 | 0 | 80 | 0 | 85 | 10 | 20 | 20 |
Bedroom/3 | 0 | 70 | 0 | 85 | 0 | 30 | 40 |
Bathroom/1 | 0 | 0 | 30 | 80 | 80 | 0 | 0 |
Bathroom/2 | 0 | 0 | 10 | 90 | 85 | 0 | 0 |
Bathroom/3 | 0 | 0 | 30 | 90 | 90 | 5 | 0 |
Even though the numbers in this table are fake, you should recognize a pattern:
- each location is characterized by a certain combination of always-visible networks
- some sample could be "noised" by weak networks (the 5 in the table)
Our machine learning algorithm should be able to extract each location's fingerprint without being fooled by this inconsistent features.
Data gathering
Now that we know what our data should look like, we need to first get it.
In the original post, this point was the one I'm unhappy with since it's not as straight-forward as I would have liked. The method I present you in this post, instead, is by far way simpler to follow.
First of all, you will need a Wifi equipped board. I will use an Arduino MKR WiFi 1010, but any ESP8266 / ESP32 or the like will work.
The following sketch will do the job: it scans the visible networks at a regular interval and prints their RSSIs encoded in JSON format.
// file CollectData.h
#include <WiFi.h>
#define print(string) Serial.print(string);
#define quote(string) print('"'); print(string); print('"');
String location = "";
/**
*
*/
void setup() {
Serial.begin(115200);
delay(3000);
WiFi.disconnect();
print("Enter 'scan {location}' to start the scanning and 'stop' to stop scanning\n");
}
/**
*
*/
void loop() {
// look for new command
if (Serial.available()) {
String input = Serial.readStringUntil('\n');
if (input.indexOf("scan ") == 0) {
location = input.substring(5);
print("Started scanning...");
print(location);
print("\n");
}
else {
print("Command not found. Enter 'scan {location}' to start the scanning\n");
location = "";
}
}
// if location is set, scan networks
if (location != "") {
int numNetworks = WiFi.scanNetworks();
// print location
print('{');
quote("__location");
print(": ");
quote(location);
print(", ");
// print each network SSID and RSSI
for (int i = 0; i < numNetworks; i++) {
quote(WiFi.SSID(i));
print(": ");
print(WiFi.RSSI(i));
print(i == numNetworks - 1 ? "}\n" : ", ");
}
delay(1000);
}
}
Upload the sketch to your board and start mapping your house / office: go to the target location and type scan {location}
in the serial monitor, where {location}
is the name you want to give to the current location (so, for example, if you're mapping the kitchen, type scan kitchen
).
Move around the room a bit so you capture a few variations of the visible hotspots: this will lead to a more robust classification later on.
To stop the recording just type stop
in the serial monitor.
Now repeat this process for each location you want to classify. At this point you should have something similar to the following:
{"__location": "Kitchen", "N1": 100, "N2": 50}
{"__location": "Bedroom", "N3": 100, "N2": 50}
{"__location": "Bathroom", "N1": 100, "N4": 50}
{"__location": "Bathroom", "N5": 100, "N4": 50}
In your case, "N1", "N2"... will contain the name of the visible networks.
When you're happy with your training data, it's time to convert it to something useful.
Generating the features converter
Given the data we have, we want to generate C code that can convert a Wifi scan result into a feature vector we can use for classification.
Since I'm a fan of code-generators, I wrote one specifically for this very project. And since I already have a code-generator library I use for Machine Learning code written in Python, I updated it with this new functionality.
Start by installing the library.
# be sure it installs version >= 1.1.8
pip install --upgrade micromlgen
Now create a script with the following code:
from micromlgen import port_wifi_indoor_positioning
if __name__ == '__main__':
samples = '''
{"__location": "Kitchen", "N1": 100, "N2": 50}
{"__location": "Bedroom", "N3": 100, "N2": 50}
{"__location": "Bathroom", "N1": 100, "N4": 50}
{"__location": "Bathroom", "N5": 100, "N4": 50}
'''
X, y, classmap, converter_code = port_wifi_indoor_positioning(samples)
print(converter_code)
Of course you have to replace the samples
content with the output you got in the previous step.
In the console you should see a C++ class we will use later in the Arduino sketch. The class should be similar to the following example code.
// Save this code in your sketch as Converter.h
#pragma once
namespace Eloquent {
namespace Projects {
class WifiIndoorPositioning {
public:
float features[5] = {0};
/**
* Get feature vector
*/
float* scan() {
uint8_t numNetworks = WiFi.scanNetworks();
for (uint8_t i = 0; i < 5; i++) {
features[i] = 0;
}
for (uint8_t i = 0; i < numNetworks; i++) {
int featureIdx = ssidToFeatureIdx(WiFi.SSID(i));
if (featureIdx >= 0) {
features[featureIdx] = WiFi.RSSI(i);
}
}
return features;
}
protected:
/**
* Convert SSID to featureIdx
*/
int ssidToFeatureIdx(String ssid) {
if (ssid.equals("N1"))
return 0;
if (ssid.equals("N2"))
return 1;
if (ssid.equals("N3"))
return 2;
if (ssid.equals("N4"))
return 3;
if (ssid.equals("N5"))
return 4;
return -1;
}
};
}
}
I will briefly explain what it does: when you call getFeatures()
, it runs a Wifi scan and for each network it finds, it fills the corresponding element in the feature vector (if the network is a known one).
At the end of the procedure, your feature vector will look something like [0, 10, 0, 0, 50, 0, 0]
, each element representing the RSSI of a given network.
Generating the classifier
To close the loop of the project, we need to be able to classify the features vector into one of the recorded locations. Since we already have micromlgen
installed, it will be very easy to do so.
Let's update the Python code we already have: this time, instead of printing the converter code, we will print the classifier code.
# install scikit package first
pip install scikit-learn
from sklearn.tree import DecisionTreeClassifier
from micromlgen import port_wifi_indoor_positioning, port
if __name__ == '__main__':
samples = '''
{"__location": "Kitchen", "N1": 100, "N2": 50}
{"__location": "Bedroom", "N3": 100, "N2": 50}
{"__location": "Bathroom", "N1": 100, "N4": 50}
{"__location": "Bathroom", "N5": 100, "N4": 50}
'''
X, y, classmap, converter_code = port_wifi_indoor_positioning(samples)
clf = DecisionTreeClassifier()
clf.fit(X, y)
print(port(clf, classmap=classmap))
Here I chose Decision tree because it is a very lightweight algorithm and should work fine for the kind of features we're working with.
If you're not satisfied with the results, you can try to use SVM, Gaussian Naive Bayes or RandomForest, which are all supported by micromlgen
.
In the console you will see the generated code for the classifier you trained. In the case of DecisionTree
the code will look like the following.
// Save this code in your sketch as Classifier.h
#pragma once
namespace Eloquent {
namespace ML {
namespace Port {
class DecisionTree {
public:
/**
* Predict class for features vector
*/
int predict(float *x) {
if (x[2] <= 25.0) {
if (x[4] <= 50.0) {
return 1;
}
else {
return 2;
}
}
else {
return 0;
}
}
/**
* Convert class idx to readable name
*/
const char* predictLabel(float *x) {
switch (predict(x)) {
case 0:
return "Bathroom";
case 1:
return "Bedroom";
case 2:
return "Kitchen";
default:
return "Houston we have a problem";
}
}
protected:
};
}
}
}
Wrapping it all together
Now that we have all the pieces together, we only need to merge them to get a complete working example.
// file GetPosition.h
#include "WiFi.h"
#include "Converter.h"
#include "Classifier.h"
Eloquent::Projects::WifiIndoorPositioning positioning;
Eloquent::ML::Port::DecisionTree classifier;
void setup() {
Serial.begin(115200);
}
void loop() {
positioning.scan();
Serial.print("You're in ");
Serial.println(classifier.predictLabel(positioning.features));
delay(3000);
}
To the bare minimum, the above code runs the scan and tells you which location you're in. That's it.
Troubleshooting
It can happen that when running micromlgen.port(clf)
you get a TemplateNotFound
error. To solve the problem, first of all uninstall micromlgen
.
pip uninstall micromlgen
Then head to Github, download the package as zip and extract the micromlgen
folder into your project.
Disclaimer
This system should be pretty accurate and robust if you properly gather the data, though I can quantify how much accurate.
This is not an indoor navigation system: it can't tell you "the coordinates" of where you are, it can only detect in which room you're in.
If your location lack of nearby Wifi hotspots, an easy and cheap solution would be to spawn a bunch of ESP8266 / ESP32 boards around your house each acting as Access Point: with this simple trick you should be able to be as accurate as needed by just adding more boards.
With this in-depth tutorial I hope I helped you going from start to end of setting up a Wifi indoor positioning system using cheap hardware as ESP8266 / ESP32 boards and the Arduino IDE.
As you can see, Machine learning has not to be intimidating even for beginners: you just need the right tools to get the job done.
If this guide excited you about Machine learning on microcontrollers, I invite you to read the many other posts I wrote on the topic and share them on the socials.
You can find the whole project on Github. Don't forget to star the repo if you like it.