ESP8266 + Firebase = localização baseada em WiFi

ESP8266 + Firebase = localização baseada em WiFi 2

Uma prática comum de desenvolvedores, quando estão lidando com uma nova tecnologia ou linguagem, é fazer o famoso “Hello World”. Bom, nesse post vamos fazer o que seria um “Hello IoT World” usando um ESP8266 , conectando-se, via WiFi, ao Firebase.

Nosso objetivo principal: explorar a possibilidade de localização indoor baseada em WiFi (WiFi-based Location). Se interessou? Então vamos lá.

Por que o Firebase?

O Firebase é um serviço de nuvem (adquirido pela Google) muito usado para aplicações mobile, que tem um serviço de banco de dados não-relacionais muito robusto e simples de usar: o Firebase Realtime Database. Além disso, os protocolos de comunicação mais comuns no mundo do IoT são o MQTT e o HTTP – sendo esse último o usado nesse post para alimentar o banco de dados.

Para começar, basta acessar firebase.google.com e logar com a própria conta google, depois acessar o console e criar um projeto. Não é necessário ativar o Google Analytics agora.

ESP8266 + Firebase = localização baseada em WiFi
Clique em “Add project”.
ESP8266 + Firebase = localização baseada em WiFi
Dê um nome ao projeto.

Após isso, basta provisionar um Realtime Database (pela seção Database) com configurações de teste e capturar a URL de host do banco de dados, que é algo como:

project_name.firebaseio.com

(vamos ignorar o “https://” e a última barra “/” )

ESP8266 + Firebase = localização baseada em WiFi
Clique em “Create Database”
ESP8266 + Firebase = localização baseada em WiFi
Clique em “Enable” com o modo teste habilitado

Além de obter também o Database Secret, que é um código de acesso (ou token):

ESP8266 + Firebase = localização baseada em WiFi
Guarde esse token, vamos precisar no código

Por que o ESP8266?

O ESP8266 é um microcontrolador que possui o mínimo para um hardware de IoT standalone: conectividade wifi.

ESP8266 + Firebase = localização baseada em WiFi

Além disso, é uma das placas mais vendidas e usadas de IoT, por vários motivos , dentre eles: o custo-benefício, menor curva de aprendizado e integração com a plataforma e comunidade Arduino.

Para programar o ESP8266 via IDE do Arduino, basta ir em File > Preferences > Additional Boards Manager URL e adicionar a seguinte URL:

http://arduino.esp8266.com/stable/package_esp8266com_index.json

Agora, basta procurar pela placa em Tools > Board > Boards Manager e instalar a versão mais recente.

ESP8266 + Firebase = localização baseada em WiFi

Como conectar o ESP82266 ao Firebase?

Existe um cliente do Firebase implementado em C++ (que basicamente consome a API REST do Realtime Database) chamado firebase-arduino. Ele suporta as operações básicas usando Json, através da biblioteca ArduinoJson, que é uma dependência e deve ser instalada para que tudo funcione bem.

Para instalar a ArduinoJson, basta seguir a opção Sketch > Include Library > Manage Libraries, pesquisar por ArduinoJson e utilizar a versão 5.13.1.

ESP8266 + Firebase = localização baseada em WiFi
Basta procurar no Library Manager e instalar a versão correta.

O processo de instalação da outra biblioteca, a firebase-arduino, também é simples, basta fazer o download ZIP da biblioteca no Github.

Depois disso, deve-se instalar via opção Sketch > Include Library > Add .ZIP Library, buscando o arquivo .ZIP da biblioteca no seu computador.

Talk is cheap, show me the code!

O código basicamente rastreia todas as redes próximas, conecta-se a uma rede específica e faz o upload de cada uma (com as informações de nome, força do sinal e tempo atual).

*Dados sensíveis foram omitidos, use as informações do seu firebase e suas redes wifi

#include <ArduinoJson.h>
#include <FirebaseArduino.h>
#include "ESP8266WiFi.h"
#include <NTPClient.h>
#include <WiFiUdp.h>
 
// Credenciais de Rede e do Firebase
#define FIREBASE_HOST "project_name.firebaseio.com"
#define FIREBASE_AUTH "DATABASE_SECRET"
#define WIFI_SSID "NOME_DA_REDE"
#define WIFI_PASSWORD "SENHA_DA_REDE"
 
// Configurações do servidor NTP
#define NTP_OFFSET   60 * 60      
#define NTP_INTERVAL 60 * 1000    
#define NTP_ADDRESS  "pool.ntp.org"
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, NTP_ADDRESS, NTP_OFFSET, NTP_INTERVAL);
 
// Periodo entre scans, em segundos
const int period = 2;
 
void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
 
  // conectando-se ao wifi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("connecting");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print("connected: ");
  Serial.println(WiFi.localIP());
  
  // Se conectando ao Firebase e iniciando o servidor NTP
  Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
  timeClient.begin();
 
  Serial.println("Setup done");
}
 
void loop() {
  // mantendo o led ligado durante a fase de scan
  digitalWrite(LED_BUILTIN, LOW); //o LED no ESP8266 é ligado de maneira que acende em LOW
  timeClient.update();
 
  // iniciando o scan de redes
  Serial.println("scan start");
  int n = WiFi.scanNetworks();
  Serial.println("scan done");
  timeClient.update();
  if (n == 0) {
    Serial.println("no networks found");
  } else {
    Serial.print(n);
    Serial.println(" networks found");
 
    // capturando o tempo atual
    unsigned long epochTime =  timeClient.getEpochTime();
    for (int i = 0; i < n; ++i) {
      // enviando o nome da rede(SSID) e a força da rede(RSSI), além do timestamp atual
      Firebase.setInt("wifi_log/" + String(epochTime) +"/" +  WiFi.SSID(i) , WiFi.RSSI(i));
      
      // tratando erros
      if (Firebase.failed()) {
          Serial.print("upload was failed");
          Serial.println(Firebase.error());  
          return;
      }
      delay(500);
    }
  }
  // Esperando um periodo específico até o proximo scan, com o led desligado
  digitalWrite(LED_BUILTIN, HIGH);
  delay(period*60000);
}

Fazendo o upload na placa, é possível acessar o Realtime Database no console e visualizar os dados sendo atualizados em tempo real. Ótimo, agora é possível monitorar nome e potência de todas as redes no entorno do ESP8266, logando esses valores a cada 2 minutos no banco de dados.

Vamos usar isso para estimar a posição da placa em relação aos roteadores do ambiente, acompanhe.

Mapeamento

Bom, agora, em posse desse instrumento de coleta de dados, podemos mapear pontos de referência no ambiente, baseados na proximidade de roteadores wifi de posições conhecidas e, de preferência, fixas. Nessa etapa, é interessante também que os roteadores sejam similares, de preferência o mesmo modelo, o que pode facilitar muito as aproximações.

Eu coletei a intensidade de 2 redes, geradas por roteadores espaçados em aproximadamente 3 metros de um corredor, e usei 6 pontos de calibração, como na figura abaixo:

ESP8266 + Firebase = localização baseada em WiFi

O procedimento foi simples: posicionar o dispositivo em cada posição, fazer 3 leituras e exportar o resultado pelo Firebase. É possível exportar para JSON diretamente do console do Firebase:

ESP8266 + Firebase = localização baseada em WiFi

Esse arquivo deve ser nomeado dados.json e outro arquivo deve ser criado, chamado referencia.json, que relaciona os pontos de referência aos momentos (timestamps) da seguinte forma:

{
    "p1" : ["1613950802", "1613950939", "1613951075"],
    "p2" : ["1613951184", "1613951328", "1613951470"],
    "p3" : ["1613951570", "1613951707", "1613951843"],
    "p4" : ["1613952066", "1613952204", "1613952340"],
    "p5" : ["1613952402", "1613952568", "1613952597"],
    "p6" : ["1613953132", "1613953285", "1613953287"]
}

O código, que foi implementado em matlab para facilitar as demonstrações, faz a leitura destes arquivos, ajusta uma função de aproximação para cada um dos roteadores (o que pode compensar diferenças de instalação, obstáculos e modelos diferentes de roteadores). Essa função é do tipo:

f(x) = a0 + a1x + a2x2

onde,

x é a potência da rede(RSSI)

f(x) é a distância calculada em metros

Isso gera uma aproximação razoável do comportamento do sinal (pelo menos no contexto do meu apartamento), como é possível ver nesse ajustamento de uma das redes:

ESP8266 + Firebase = localização baseada em WiFi

Além disso, o código é capaz de receber uma nova exportação JSON do banco e calcular a posição aproximada para cada ponto, como demonstrarei a seguir.

Código do processamento dos dados

clear;
clc;
 
% dados de referencia
rede_1 = "VIVO_F609";
rede_2 = "VIVO_F609_plus";
n_medidas = 3;
n_pontos = 6;
 
% posicoes dos pontos de referencia
distancias = [[2.4 5.6],
              [0.6 3.0],
              [1.4 2.2],
              [2.4 1.2],
              [3.0 0.6],
              [5.3 2.3]];
 
% leitura dos arquivos
dados = read_json('dados.json');
referencia = read_json('referencia.json');
 
% calculando medias de rssi em cada ponto
medidas_rede_1 = zeros(n_medidas, n_pontos);
medidas_rede_2 = zeros(n_medidas, n_pontos);
medias_rede_1 = zeros(1, n_pontos);
medias_rede_2 = zeros(1, n_pontos);
 
for n = 1:n_pontos
   for amostra = 1:n_medidas
      medidas_rede_1(amostra, n) = dados.wifi_log.('x' + string(referencia.('p' + string(n))(amostra))).(rede_1);
      medidas_rede_2(amostra, n) = dados.wifi_log.('x' + string(referencia.('p' + string(n))(amostra))).(rede_2);
   end
   % calculando a media de n_medidas
   medias_rede_1(n) = mean(medidas_rede_1(:,n));
   medias_rede_2(n) = mean(medidas_rede_2(:,n));
end
 
% plot(medias_rede_1(2:6), distancias(2:6,1)','b');
% xlabel("RSSI")
% ylabel("Distancia")
% legend("dados reais")
 
% ajustando polinomios de ordem 2
pol_ajuste_rede1 = polyfit(medias_rede_1(2:6), distancias(2:6,1)',2);
pol_ajuste_rede2 = polyfit(medias_rede_2(1:5), distancias(1:5,2)',2);
 
% desenhando um gráfico 2D mostrando a posicao aproximada
x = -5:0.1:5;
y = -5:0.1:5;
pos_rede_1 = [0 2];
pos_rede_2 = [-1 -1];
[X,Y] = meshgrid(x,y);
Z = [];
for i = 1:length(x)
    for j = 1:length(y)
        if all([X(i,j),Y(i,j)] == pos_rede_1) ||  all([X(i,j),Y(i,j)] == pos_rede_2)
            disp ([i j]);
            Z(i,j) = 0; % os roteadores serao destacados na figura
        else
            Z(i,j) = 1;
        end
    end      
end
 
%%%% Reconstruindo posicao através do log de redes
dados_trajeto = read_json('trajeto.json');
 
rssi_rede1 = zeros(1, numel(fieldnames(dados_trajeto.wifi_log)));
rssi_rede2 = zeros(1, numel(fieldnames(dados_trajeto.wifi_log)));
tempos = fieldnames(dados_trajeto.wifi_log);
%v = VideoWriter('video2.avi');
%open(v);
 
for t = 1:length(rssi_rede1)
    % plotando as estimativas de distancia aos pontos de wifi
    pcolor(X,Y,Z);
    colormap gray;
    shading interp;
    hold on;
    
    % calculado os raios estimados para cada rede
    try
        r1 = polyval(pol_ajuste_rede1, dados_trajeto.wifi_log.(string(tempos(t))).(rede_1));
    catch
        r1 = Inf;
        disp("falha : rede " + rede_1 + " ausentes na medida " + string(tempos(t)))
    end
    try
        r2 = polyval(pol_ajuste_rede2, dados_trajeto.wifi_log.(string(tempos(t))).(rede_2));
    catch
        r2 = Inf;
        disp("falha : rede " + rede_2 + " ausentes na medida " + string(tempos(t)))
    end
    
    % calculando a posicao estimada do dispositivo baseado na posicao
    % relativa das duas circunferencias
    pos_aproximada = estimativa(pos_rede_1, pos_rede_2, r1, r2 );
    
    % desenhando as circunferências estimadas de cada rede em verde
    circle(pos_rede_1(1), pos_rede_1(2), r1,'g');
    circle(pos_rede_2(1), pos_rede_2(2), r2,'g');
    
    % desenhando a posicao estimada em azul
    circle(pos_aproximada(1) , pos_aproximada(2), 0.1,'b*');
    pause(0.2);
    
%     for c=1:24
%         assumindo 24fps, cada frame durara 1seg
%         writeVideo(v,getframe);
%     end
    hold off;
end
 
%close(v);
 
function posicao_aproximada = estimativa(pos_rede_1, pos_rede_2, r1, r2)
    % calculo da posicao aproximada através de dois métodos: se os circulos
    % tem interseccao, a posicao aproximada é no ponto médio de interseccao
    % senao, a posicao aproximada é no ponto médio entre a possicao de
    % maior proximidade dos circulos
    direcao = pos_rede_1 - pos_rede_2;
    angulo = atan(direcao(2)/direcao(1));
    if r1 == Inf
        posicao_aproximada = pos_rede_2 + [-r2*cos(angulo)  -r2*sin(angulo)];
    elseif r2 == Inf
        posicao_aproximada = pos_rede_1 + [r1*cos(angulo)  r1*sin(angulo)];
    else   
        % checando se os circulos tem interseccao
        d2 = sum((-direcao').^2);
        P0 = (pos_rede_1' + pos_rede_2')/2 + ( r1^2 - r2^2)/d2/2*(-direcao');
        distancia_intersecao = ((r1+r2)^2-d2)*(d2-(r2-r1)^2);
        if distancia_intersecao <= 0
            if r1 > norm(direcao)
                posicao_aproximada = pos_rede_1 + [-f(r1, r2, norm(direcao))*cos(angulo)  -f(r1, r2, norm(direcao))*sin(angulo)];
            else 
                posicao_aproximada = pos_rede_2 + [f(r1, r2, norm(direcao))*cos(angulo)  f(r1, r2, norm(direcao))*sin(angulo)];
            end
        else
            % P0 é o ponto equidistante das duas interseccoes dos circulos (ou
            % o ponto exato de interseccao)
            Pa = P0 - sqrt(distancia_intersecao)/d2/2*[0 -1;1 0]*(-direcao'); % Pa and Pb are circles' intersection points
            posicao_aproximada = Pa;
            %disp(Pint);
        end
    end
end
 
function json = read_json(file)
    % funcao usada para ler um arquivo json e retornar como struct
    file_pointer = fopen(file);
    raw = fread(file_pointer,inf); 
    str = char(raw'); 
    fclose(file_pointer);
    json = jsondecode(str);
end
 
function circle(x_centro, y_centro, r, style)
    % funcao usada para desenhar uma circunferencia
    ang = 0:0.01:2*pi; 
    xp = r*cos(ang);
    yp = r*sin(ang);
    plot(x_centro + xp,y_centro + yp, style);
end
 
function e = f(r1, r2, d)
    % funcao que calcula o modulo do vetor da posicao aproximada
    e = ( d + r1 + r2)/2;
end

Localização

Com as funções de estimativa de posição calculadas, é possível reconstruir o trajeto aproximado do objeto baseado nos dados históricos dos logs. Por exemplo, eu movimentei o objeto na seguinte sequência:

Sala→Banheiro→Sala→Banheiro→Quarto

E o trajeto reconstruído através das funções de estimativa foi bastante coerente com essa movimentação:

ESP8266 + Firebase = localização baseada em WiFi

Legal, né? Comente aqui no blog o que você achou e não deixe de ficar de olho no Blog FilipeFlop para encontrar outros posts como esse.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

2 Comentários

  1. Olá Guilherme,
    Como vai, tudo bem ?
    Também sou de Recife.
    Gostaria de saber se seria possível usar esta ideia para localizar um animal de estimação dentro de uma residência ?

    1. Olá Ivan, tudo bem ?
      É possível sim, basta colocar um cliente wifi na coleira ( um esp8266-01 alimentado por bateria, por exemplo ) rodando o código desse post, seguir o passo a passo para armazenar os dados coletados no Firebase (ou outro banco), extrai-los e executar a análise desses dados pra reconstruir a movimentação do animal.