Servidor Telnet no ESP8266: disponibilizando dados de monitoramento

Com um servidor Telnet via WiFi fica fácil obter dados do Arduino em CSV para usar em programas ou planilhas.

Tecnologias clássicas como as conexões telnet e o formato de dados “universal” CSV continuam servindo como ponte entre variados sistemas, e hoje veremos como usá-las para permitir que um Arduino exporte na rede WiFi, sob demanda, o histórico de medições de um sensor qualquer.

O telnet servia originalmente para abrir terminais interativos remotos via rede, e entrou em desuso – sendo substituído largamente pelo ssh – devido à falta de opções de segurança necessárias no mundo moderno. Mas ele continua prático para aplicações não-interativas e que não exijam recursos de segurança, e usaremos um cliente telnet simples 1 para nosso acesso ao histórico de um sensor.

Já o CSV nunca entrou em desuso: trata-se de um formato2 de dados (geralmente associado à extensão de arquivo .csv) fácil de exportar e de importar, e por isso usado para transferência entre sistemas diferentes. Sua característica básica é ter cada registro em uma linha, e dentro de cada linha usar vírgulas (embora atualmente seja mais popular o ponto e vírgula) para separar os campos.

Nosso exercício de hoje vai usar um Arduino Uno e um módulo ESP-01. Vamos imaginar que o sistema faz3 10 leituras por hora (de algum sensor analógico), e armazena em um array as últimas 80 leituras – ou seja, os dados das últimas 8 horas. O ESP8266 estará conectado à rede sem fio com um IP fixo e, sempre que receber uma conexão via telnet na porta 2222, transmitirá (formatado em CSV) o conjunto das leituras armazenadas, com uma linha para cada hora. Abrindo no Excel o arquivo CSV resultante, a planilha fica como a da imagem acima.

Sugiro fortemente que antes de prosseguir você coloque em dia a leitura dos seguintes artigos que têm detalhes sobre elementos que usarei no programa (e não voltarei a explicar tão detalhadamente como já fiz neles):

O programa

Já mencionei que a montagem física é idêntica à usada no artigo “IP fixo no ESP8266 com Arduino”, portanto consulte-o para relembrar qual pino do Arduino vai em qual pino do ESP-01, e quais os componentes adicionais necessários.

Vamos agora ao que realmente interessa, que é o programa:

#define VELOCIDADE 115200

String REDE="br-arduino";
String SENHA=".org";

String IPLIVRE="192.168.0.235";
String MASCARA="255.255.255.0";
String ROTEADOR="192.168.0.254";

#include <SoftwareSerial.h>
SoftwareSerial monitorSerial(9, 8); // RX, TX

#define debug
#define windows

void cmdESP(String cmd, String msg="", int limite=7000) { if (msg!="") monitorSerial.println(msg); if (cmd!="") Serial.println(cmd); unsigned long chegada=millis(); boolean continuar=true; String S=""; unsigned long ultimochar=0; while (continuar) { if (Serial.available()) { char c = Serial.read(); ultimochar=millis(); S=S+c; if (c==10) { // LF, fim da linha recebida byte p=S.indexOf(13); String S1=S.substring(0,p); if (S1=="OK") continuar=false; else if (S1=="SEND OK") continuar=false; else if (S1=="ready") continuar=false; else if (S1=="no change") continuar=false; else if (S1=="ERROR") continuar=false; monitorSerial.print(S); S=""; } } if (millis()-chegada > limite) continuar=false; } if (S!="") monitorSerial.print(S); }
void ESPsendStr(const char *ESPlinha) { int comprimento=strlen(ESPlinha); Serial.print("AT+CIPSEND=0,"); Serial.println(comprimento,DEC); Serial.find("> "); Serial.print(ESPlinha); cmdESP("","Aguardando envio"); }
void conectaIPfixo() { String cmdConectar="AT+CWJAP=\""+REDE+"\",\""+SENHA+"\""; String cmdMudarIP="AT+CIPSTA_DEF=\"" + IPLIVRE + "\",\"" + ROTEADOR +"\",\"" + MASCARA + "\""; cmdESP("AT+CWMODE=3", "Modo de operacao misto, AP + STATION"); cmdESP(cmdConectar,"Conectando a uma rede WiFi",30000); cmdESP(cmdMudarIP,"Definindo nosso proprio IP fixo"); }
void ativaTelnet() { cmdESP("AT+CIPMUX=1", "Ativa multiplas conexoes, necessario para estabelecer servidor"); cmdESP("AT+CIPSERVER=1,2222", "Estabelece servidor na porta 2222 (acessivel via telnet)"); }
int HIST[90];
boolean recebeConexao() { unsigned long limite=7000; unsigned long chegada=millis(); char resp[100]=""; unsigned long tempo; boolean continuar=true; boolean timeout=false; int contaChars=0; while (continuar) { if (Serial.available()) { unsigned long chegada=millis(); // recomeca a contar quando ha recepcao resp[contaChars] = Serial.read(); contaChars++; if (contaChars>80) contaChars=0; // aqui deveria haver uma condicao de erro #ifdef debug monitorSerial.print(resp[contaChars-1]); #endif if (resp[contaChars-1]==10) { // LF, fim da linha recebida if (contaChars>1) { resp[contaChars-2]=char(0); if (0==strcmp(resp,"OK")) continuar=false; else if (0==strcmp(resp,"ERROR")) continuar=false; else if (0==strcmp(resp,"0,CONNECT")) { continuar=false; ESPsendStr("**+ Segue lista decrescente das leituras armazenadas:\n\r"); for (byte _d=0;_d<=7;_d++) { for (byte _u=0;_u<=9;_u++) { char digitos[4]; itoa(HIST[_d*10+_u],digitos,10); ESPsendStr(digitos); ESPsendStr(";"); } #ifdef windows ESPsendStr("\n\r"); #else ESPsendStr("\n"); #endif } ESPsendStr("**- Fim da lista. Desconectando.\n\r"); cmdESP("AT+CIPCLOSE=0","Encerrando conexao recebida"); } contaChars=0; } } } tempo=millis()-chegada; // tempo sem recepcao if (tempo > limite) { timeout=true; continuar=false; } } }
void setup() { for (byte i=0;i<80;i++) HIST[i]=random(0,1000); monitorSerial.begin(VELOCIDADE); monitorSerial.println("*** ESP8266 Static IP / IP fixo - BR-Arduino.org"); delay(2000); Serial.begin(VELOCIDADE); Serial.setTimeout(5000); conectaIPfixo(); ativaTelnet(); monitorSerial.println("Aguardando conexoes"); } void loop() { recebeConexao(); }

O cabeçalho, você já sabe, possui as definições básicas usadas por outras partes do programa, e a inclusão da biblioteca softwareSerial (detalhes no artigo “Usando outro Arduino como intermediário para debug no Monitor com a softwareSerial”).

Antes de chegar às novidades, vamos começar pela referência às funções que já conhecemos de artigos anteriores:

  • A função cmdESP (marcada com uma barra verde à esquerda do código) é uma velha conhecida, pois já a usei e expliquei no artigo do IP fixo no ESP8266. O que ela faz é receber um comando a ser enviado ao ESP8266, um texto descrevendo esse comando e, opcionalmente, um limite de espera. Aí ela envia para o ESP o comando, envia para o monitor serial o texto descritivo, e aguarda até o ESP dar a sua resposta, exibindo-a também no monitor serial.
  • A função conectaIPfixo (também marcada com uma barra verde) vem da mesma origem: ela repete a sequência de inicialização do artigo do IP fixo no ESP8266. Note que as definições de IPLIVRE, MASCARA e ROTEADOR, do alto do arquivo, serão usados para definir o IP do ESP8266, a máscara de sub-rede e o endereço do gateway, e você deve alterá-las de acordo com a sua rede local, como já vimos no artigo anterior.
  • A função ESPsendStr (marcada com uma barra azul) é uma conhecida do nosso artigo sobre servidor web WiFi com Arduino e ESP8266, que a explica com detalhes. Ela recebe como parâmetro uma string e a envia (com os comandos apropriados) pela conexão TCP que estiver aberta. Esta versão ignora múltiplas conexões, e envia sempre pela conexão número zero, se houver mais do que uma conexão aberta.

Agora a parte detalhada, começando pela mais essencial: a função ativaTelnet, marcada em rosa. O que ela faz é usar o comando AT+CIPMUX para colocar o ESP8266 no modo de múltiplas conexões4, e em seguida usar o AT+CIPSERVER para ativar (com o parâmetro inicial “1”) um servidor TCP na porta 2222 (dada no parâmetro final). A partir daí o ESP8266 passará a aceitar conexões de clientes nessa porta, até receber instrução em contrário ou ser reinicializado.

O comando AT+CIPSERVER faz o ESP8266 aceitar conexões em uma porta TCP, mas é necessário escrever a função que tratará com essas conexões.

Depois de estar pronto para receber conexões, é necessário tratá-las quando elas acontecerem, e é isso que faz a recebeConexao, marcada em laranja. Note que ela é um grande loop while(), que dura até o timeout, ou um indicativo de final de transmissão (como OK ou ERROR) ser recebido, ou a conexão zero ser iniciada (o ESP a anuncia com a string 0,CONNECT).

Quando surge o 0,CONNECT, o programa sabe que um cliente Telnet se conectou à porta 2222 do ESP8266, e assim reage conforme planejado: anuncia que vai enviar a lista decrescente das leituras, inicia um loop para transmitir 8 linhas de 10 leituras cada (armazenadas na variável global HIST, um array de inteiros), separando cada leitura com um ";", informando, ao final, que a lista foi encerrada e, para ficar livre para receber uma próxima conexão tão cedo quanto possível, fechando a conexão com o comando AT+CIPCLOSE.

No cliente Telnet, tudo isso aparece assim:

Um detalhe interessante da função recebeConexao é que ela observa se existe a definição windows (o #define windows que consta no cabeçalho do nosso programa): se existir, ela termina as linhas de dados numéricos seguindo a convenção típica do Windows; se não existir, ela termina usando a convenção típica do Unix. Usuários de Linux e de Macs devem retirar a linha #define windows do programa, portanto, a não ser que não se incomodem de ver linhas em branco entre as linhas de dados, ao importar o CSV em uma planilha ;-)

Agora que a complexidade já está toda definida, resta bem pouco para as funções setup e loop, obrigatórias nas convenções do Arduino. Nosso setup limita-se a preencher a variável global HIST com 80 valores aleatórios (num programa real, seria nela que ficariam armazenadas as 80 últimas leituras de algum sensor), inicializar as 2 portas seriais, e chamar as funções conectaIPfixo() e ativaTelnet(), que já explicamos acima.

O loop() é ainda mais modesto: a cada execução ele chama a função recebeConexao, já explicada acima, que espera e trata uma conexão via telnet na porta 2222, ou encerra automaticamente em caso de erro ou timeout (e em todos esses casos será imediatamente chamada de volta, em uma nova rodada do loop).

Como interagir via Telnet

É provável que seu sistema operacional inclua um comando telnet na linha de comando; se não for o caso, instale a versão que for de sua preferência5.

Imaginando que você manteve o mesmo valor na definição de IPLIVRE que eu usei no exemplo, o endereço IP do seu ESP8266 será 192.168.0.235, e ele estará recebendo conexões na porta 2222. Assim, para receber os dados dos sensores, basta emitir (no formato reconhecido pelo seu sistema operacional), a partir de um computador conectado à mesma rede WiFi, um comando telnet 192.168.0.235 2222, e o sistema responderá.

No meu caso (rodando em um Mac), a sessão fica assim:

~$ telnet 192.168.0.235 2222 
Trying 192.168.0.235...
Connected to 192.168.0.235.
Escape character is '^]'.
**+ Segue lista decrescente das leituras armazenadas:
807;249;73;658;930;272;544;878;923;709;
440;165;492;42;987;503;327;729;840;612;
303;169;709;157;560;933;99;278;816;335;
97;826;512;267;810;633;979;149;579;821;
967;672;393;336;485;745;228;91;194;357;
1;153;708;944;668;490;124;196;530;903;
722;666;549;24;801;853;977;408;228;933;
298;981;635;13;865;814;63;536;425;669;
**- Fim da lista. Desconectando.

Connection closed by foreign host.
~$

A parte em negrito é o comando que eu digitei; em laranja estão as informações emitidas pelo próprio cliente telnet narrando o que ele estava tentando fazer, e em cinza estão as informações recebidas do ESP8266.

Para gerar um arquivo CSV simples, basta redirecionar a saída do comando acima. Por exemplo, digitando telnet 192.168.0.235 2222 > teste.csv será gerado um arquivo teste.csv com todo o conteúdo da conexão que vimos.

Esse arquivo teste.csv pode ser aberto em uma planilha, e as células aparecerão corretamente, mas aquelas mensagens narrando a conexão telnet também estarão presentes. Para evitá-las, você pode usar como passo intermediário um filtro usando a sua linguagem de programação favorita, que deixe passar só as linhas entre a que começa com **+ e a que começa com **-.

Já que estamos falando de tecnologias dos anos 1970, vou ilustrar com um filtro na linguagem AWK que, como faz parte do padrão Posix, deve estar presente em qualquer sistema Unix (como o Mac ou os BSDs) e seus clones (como o Linux). Ficaria assim:

telnet 192.168.0.235 2222 | awk '
  /\*\*\-/ {v=0} 
  /\*\*\+/ {v=1; next} 
  v==1 {print}' > teste2.csv

Provavelmente você consegue fazer com bem mais elegância e legibilidade em linguagens modernas, como Python ou PHP (com ou sem uso do telnet como interface), mas o resultado provavelmente será similar a este:

E boas tabelas!

 
  1.  Seu sistema operacional deve ter um, acessível digitando telnet na linha de comando, ou ele deve estar disponível como um pacote oficial para instalação.

  2.  Ou de múltiplos formatos: existem muitas variações do CSV, com diferentes terminadores de linha, separadores de campos, indicadores de tipos, etc.

  3.  No programa de exemplo, simplesmente vamos preencher o array com 80 números aleatórios.

  4.  Na prática o nosso programa só gerencia uma conexão de cada vez, mas é trivial expandi-lo para gerenciar múltiplas delas - fica como exercício para o leitor.

  5.  Em usos "da vida real", é possível que você prefira usar alguma biblioteca de sockets para interagir de modo programado.

Comentar

Dos leds ao Arduino, ESP8266 e mais

Aprenda eletrônica com as experiências de um geek veterano dos bits e bytes que nunca tinha soldado um led na vida, e resolveu narrar para você o que descobre enquanto explora esse universo – a partir da eletrônica básica, até chegar aos circuitos modernos.

Por Augusto Campos, autor do BR-Linux e Efetividade.net.

Recomendados

Livro recomendado


Artigos já disponíveis

Comunidade Arduino

O BR-Arduino é integrante da comunidade internacional de entusiastas do Arduino, mas não tem relação com os criadores e distribuidores do produto, nem com os detentores das marcas registradas.

Livros recomendados