In this Arduno Machine learning project we're going to use the nearby WiFi access points to locate where we are. For this project to work you will need a Wifi equipped board, such as ESP8266 or ESP32.
Table of contents
What is Wifi indoor positioning?
We all are used to GPS positioning: our device will use satellites to track our position on Earth. GPS works very well and with a very high accuracy (you can expect only a few meters of error).
But it suffers a problem: it needs Line of Sight (a clear path from your device to the satellites). If you're not in an open place, like inside a building, you're out of luck.
The task of detecting where you are when GPS localization is not an option is called indoor positioning: it could be in a building, an airport, a parking garage.
There are lots of different approaches to this task (Wikipedia lists more than 10 of them), each with a varying level of commitment, difficulty, cost and accuracy.
For this tutorial I opted for one that is both cost-efficient, easy to implement and readily available in most of the locations: WiFi indoor positioning.
What will we use it for?
In this tutorial about Machine learning on Arduino we're going to use Wifi indoor positioning to detect in which room of our house / office we are. This is the most basic task we can accomplish and will get us a feeling of level of accuracy we can achieve with such a simple setup.
On this basis, we'll construct more sophisticated projects in future posts.
What you need
To accomplish this tutorial, you really need 2 things:
- a WiFi equipped board (ESP8266, ESP32, Arduino MKR WiFi 1010...)
- be in a place with a few WiFi networks around
If you're doing this at home or in your office, there's a good change your neighbours have WiFi networks in their apartments you can leverage. If you live in an isolated contryside, sorry, this will not work for you.
How it works
So, how exactly does Wifi indoor positioning works in conjuction with Machine learning?
Let's pretend there are 5 different WiFi networks around you, like in the picture below.
As you can see there are two markers on the map: each of these markers will "see" different networks, with different signal strengths (a.k.a RSSI).
As you move around, those numbers will change: each room will be identified by the unique combination of the RSSIs.
1. Features definition
The features for this project are going to be the RSSIs (Received signal strength indication) of the known WiFi networks. If a network is out of range, it will have an RSSI equal to 0.
2. Record sample data
Before actually recording the sample data to train our classifier, we need to do some preliminary work. This is because not all networks will be visible all the time: we have to work, however, with a fixed number of features.
2.1 Enumerate the access points
First of all we need to enumerate all the networks we will encounter during the inference process.
To begin, we take a "reconnaissance tour" of the locations we want to predict and log all the networks we detect. Load the following sketch and take note of all the networks that appear on the Serial monitor.
#include <WiFi.h>
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
}
void loop() {
int numNetworks = WiFi.scanNetworks();
for (int i = 0; i < numNetworks; i++) {
Serial.println(WiFi.SSID(i));
delay(3000);
}
2.2 Create an access point array
Now that we have a bunch of SSIDs, we need to assign each SSID to a fixed index, from 0 to MAX_NETWORKS
.
You can implement this part as you like, but in this demo I'll make use of a class I wrote called Array
(you can see the source code and example on Github), which implements 2 useful functions:
push()
to add an element to the arrayindexOf()
to get the index of an element.
See how to install the Eloquent library if you don't have it already installed.
At this point we populate the array with all the networks we saved from the reconnaissance tour.
#include <eDataStructures.h>
#define MAX_NETWORKS 10
using namespace Eloquent::DataStructures;
double features[MAX_NETWORKS];
Array<String, MAX_NETWORKS> knownNetworks("");
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
knownNetworks.push("SSID #0");
knownNetworks.push("SSID #1");
knownNetworks.push("SSID #2");
knownNetworks.push("SSID #3");
// and so on
}
2.3 Convert to features vector
The second step is to convert the scan results into a features vector. Each feature will be the RSSI of the given SSID, in the exact order we populated the knownNetworks
array.
In practice:
features[0] == RSSI of SSID #0;
features[1] == RSSI of SSID #1;
features[2] == RSSI of SSID #2;
features[3] == RSSI of SSID #3;
// and so on
The code below will do the job.
void loop() {
scan();
printFeatures();
delay(3000);
}
void scan() {
int numNetworks = WiFi.scanNetworks();
resetFeatures();
// assign RSSIs to feature vector
for (int i = 0; i < numNetworks; i++) {
String ssid = WiFi.SSID(i);
uint16_t networkIndex = knownNetworks.indexOf(ssid);
// only create feature if the current SSID is a known one
if (!isnan(networkIndex))
features[networkIndex] = WiFi.RSSI(i);
}
}
// reset all features to 0
void resetFeatures() {
const uint16_t numFeatures = sizeof(features) / sizeof(double);
for (int i = 0; i < numFeatues; i++)
features[i] = 0;
}
void printFeatures() {
const uint16_t numFeatures = sizeof(features) / sizeof(float);
for (int i = 0; i < numFeatures; i++) {
Serial.print(features[i]);
Serial.print(i == numFeatures - 1 ? 'n' : ',');
}
}
Grab some recordings just staying in a location for a few seconds and save the serial output to a file; then move to the next location and repeat: 10-15 samples for each location will suffice.
If you do a good job, you should end with distinguible features, as show in the plot below.
// replace
features[networkIndex] = WiFi.RSSI(i);
// with
#define MIN_RSSI -90 // adjust to your needs
features[networkIndex] = WiFi.RSSI(i) > MIN_RSSI ? WiFi.RSSI(i) : 0;
3. Train and export the classifier
For a detailed guide refer to the tutorial
from sklearn.ensemble import RandomForestClassifier
from micromlgen import port
# put your samples in the dataset folder
# one class per file
# one feature vector per line, in CSV format
features, classmap = load_features('dataset/')
X, y = features[:, :-1], features[:, -1]
classifier = RandomForestClassifier(n_estimators=30, max_depth=10).fit(X, y)
c_code = port(classifier, classmap=classmap)
print(c_code)
At this point you have to copy the printed code and import it in your Arduino project, in a file called model.h
.
4. Run the inference
#include "model.h"
void loop() {
scan();
classify();
delay(3000);
}
void classify() {
Serial.print("You are in ");
Serial.println(classIdxToName(predict(features)));
}
Move around your house/office/whatever and see your location printed on the serial monitor!
Did you find this tutorial useful? Was is it easy to follow or did I miss something? Let me know in the comments so I can keep improving the blog.
Check the full project code on Github