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.
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 “/” )
Além de obter também o Database Secret, que é um código de acesso (ou token):
Por que o ESP8266?
O ESP8266 é um microcontrolador que possui o mínimo para um hardware de IoT standalone: conectividade 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.
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.
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:
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:
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:
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:
Legal, né? Comente aqui no blog o que você achou e não deixe de ficar de olho no Blog MakerHero para encontrar outros posts como esse.
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 ?
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.