Site menu Fase idIoTa, parte 2

Fase idIoTa, parte 2

A febre IoT continua ardendo aqui. Conforme os projetos vão sendo aplicados na prática, os problemas vão aparecendo, mas renova-se a graça da brincadeira. É gratificante ver seu aparelho funcionando um dia, uma semana, duas semanas a fio, de forma confiável e sem supervisão.

Este texto é uma coleção desorganizada de observações e ruminações do que tem acontecido por aqui.

Pense bem as métricas

Como dito no texto anterior, a febre IoT vem acompanhada da comorbidade do "Big Data". Os dispositivos IoT publicam métricas via protocolo MQTT. Um script Python subcreve os tópicos MQTT e armazena-os num servidorzinho inFluxDB local. O Grafana torna fácil visualizar as séries de dados.

Figura 1: Nível da caixa de água, e uptime do sensor

Às vezes uma métrica que parece boa mostra seus defeitos quando entra na seara do BigData. Por exemplo, no projeto H2OControl, inventei de publicar o nível da caixa de água em dois tópicos:

A ideia é que e.g. se foram bombeados 50L depois que o nível mudou para 80%, o nível total estimado é 80%+50L. Preferi publicar dois números simples, facilmente parseáveis, em vez de uma string. O uso de porcentagem em Level1 evitar ter de hardcodear o volume total da caixa. No Grafana, uma simples operação aritmética com as duas time series traça o gráfico do nível total.

Porém, isto causa o seguinte problema: as duas métricas são publicadas sequencialmente e não podem mudar exatamente ao mesmo tempo, mesmo quando deveriam. Por exemplo, ao mudar de 60%+199L para 80%+0L, ocorre um breve "glitch" 60%+0L ou 80%+199L no Grafana, dependendo de qual métrica virou primeiro.

A solução foi fazer a soma dentro do próprio sensor e publicar uma nova métrica EstimatedLevel, em litros. A métrica Level1 sobrevive em espírito, nas novas métricas CoarseLevelPct e CoarseLevelVol.

Outro problema potencial, que ainda não enfrentei mas certamente vou enfrentar, é a escolha errada no "ritmo" de publicação, afinal o destino final é um banco de dados time-series. Por exemplo: devo ficar republicando um valor estável, ou só publicar quando ele muda? Faz sentido usar o recurso "retain" do MQTT, correndo o risco dos clientes MQTT enxergarem um valor "velho" quando na verdade o sensor está fora do ar faz dias?

Aqui temos, numa escala pequena, problemas análogos à arquitetura de microsserviços, que parece uma ideia muito boa, até a hora que se descobre que cada microsserviço a mais é uma parte móvel adicional para quebrar ou interagir mal com as demais.

Confiabilidade e robustez

O grosso do código para Arduino que circula por aí não foi pensado para ser robusto. Coletivamente, é bom que esse código exista, é documentação gratuita e valiosa. Mas, para atingir um resultado robusto, você tem de "capinar". Nada substitui diligência e teste de campo.

Algo muito semelhante acontece com código que envolve comunicação de rede/Internet. Existem inúmeros exemplos que parecem corretos, que funcionam na maior parte do tempo, porém falham em tratar os casos fortuitos. Na hora de colocar em produção, o que mais aparece é caso fortuito...

O que é um programa robusto? É aquele que procura resistir a todas as falhas possíveis: falta de rede, mau contato no sensor, bug de software, bug de firmware, falha de hardware. Em programação embarcada, até falhas de hardware têm de ser tratadas, pois o dispositivo pode estar inacessível, não é como um PC que você está acostumado a reiniciar todo dia.

Pensando nisso, todo microcontrolador oferece o recurso de watchdog de hardware, um dispositivo que reseta automaticamente o controlador se não for "alimentado" a cada poucos segundos, o que pode acontecer se a CPU travar completamente.

Algumas plataformas também oferecem watchdog de software, que serve para pegar bugs de software (mas não um travamento da CPU, pois quem executaria o código do watchdog?).

A mistura do watchdogs com código de rede é explosiva, pois os códigos de exemplo e até mesmo muitas bibliotecas são mal-feitos. Às vezes não é culpa da biblioteca, ela esbarra em limitações da plataforma. Seja qual for o motivo, o resultado final é que seu código pode bloquear por muito tempo em determinados pontos, por exemplo um connect().

O timeout desse bloqueio tem de ser menor que o prazo fatal do watchdog. Se esse timeout não for configurável (ou o tempo do watchdog for fixo, como é o caso do ESP8266), o jeito é apelar, alimentando o watchdog com uma thread ou com um timer que invoca uma interrupção. O que não dá, é ficar sem watchdog.

Não é impossível o controlador perder conectividade, sem razão aparente (aliás, isto acontece também em computadores e celulares, por que não aconteceria em IoT?). O programa continua rodando, mas a rede simplesmente não funciona mais. Um reset resolve. A falha pode acontecer tanto na camada Wi-Fi quanto TCP/IP. Mais um mecanismo semelhante a watchdog é necessário para detectar esta situação. (Ainda não implementei este watchdog nas versões C++ dos meus projetos, e talvez não implemente mais, já que a ideia é migrar para outras linguagens.)

A quem interessar possa, meus projetinhos estão no GitHub (aqui, aqui e aqui). Considero-me um amador na seara IoT, mas pelo menos eu coloco o meu na reta: esse código está sendo usado no mundo real e roda há um bom tempo num lugar que por ora é de difícil acesso. Dentro das minhas possibilidades, ele foi bastante trabalhado na questão da robustez.

Fonte de alimentação

Um componente muito importante e muito negligenciado para a estabilidade de longo prazo do seu IoT, é a fonte de alimentação. Instabilidades "misteriosas" podem surgir dali. Fontes também são riscos de choque elétrico e incêndio, então é bom não usar qualquer porcaria.

Aqui, o controle da caixa de água estava travando a cada poucos dias em seu posto, mas não em "laboratório". O watchdog mitigou o problema. Mas a culpada era uma fonte USB "sem-marca". Trocar por um carregador de celular velho fez o problema sumir para sempre.

A fonte descartada entregava 5.30V de tensão, acima do máximo absoluto do padrão USB que é 5.25V. Não sei se era isso que causava o travamento, mas é motivo suficiente para reprová-la. No mínimo, estava fazendo o regulador de tensão 3.3V esquentar.

Normalmente não deixo entrar fontes duvidosas na minha gaveta, sou assinante do canal DiodeGoneWild, mas essas fontes USB ruins (havia 2 iguais!) furaram o bloqueio. Realmente não lembro de onde vieram, mas acredito que eram fontes de algum aparelho menos exigente, tipo abajur LED.

Tenho usado carregadores de celular pois acumulei alguns ao longo dos últimos anos (santa regulamentação europeia). Ainda prefiro ligar o dispositivo IoT numa fonte externa via cabo USB, pois facilita ligar alternativamente num computador. Outra opção é usar uma mini-fonte Hi-Link ou similiar.

Por último, uma fonte boa é a última linha de defesa entre um transiente e seu equipamento.

Seja como for, o fato é que uma fonte decente, de qualquer tipo, custará mais caro que a plaquinha do microcontrolador. É a vingança da eletrônica analógica!

Sensores

Novamente a confirmar que a eletrônica analógica é a parte cara e complicada do IoT, já tive dois problemas com sensores. No começo do projeto você pensa: um sensor tão simples, produzido aos milhões, que funciona segundo princípios tão básicos, jamais vai dar dor-de-cabeça. Vai vendo...

O sensor de fluxo de água pifou, assim do nada, numa bela tarde fagueira. E agora que estragou, achei diversos relatos na Internet explicando que sensores Hall estragam fácil. Se soubesse disso, teria optado por um sensor reed relay, menos tecnológico e mais caro, porém mais robusto.

Por ora, reprogramei o controlador para funcionar sem o sensor de fluxo. O código possui agora uma alternate law para adaptar-se à falta desse sensor. A propósito, o controlador está no meu GitHub, assim como todo o resto do projeto IoT caseiro.

Já comprei outro, os parafusos externos sugerem que é possível trocar apenas trocar o "miolo" eletrônico, e tomara seja mesmo, porque desmontar o encanamento será bem difícil (leia-se caro para pagar um encanador). Se soubesse que havia chance de estragar assim, teria usado uniões, do tipo que se usa em bomba de água para fácil remoção.

Uma das chaves-boia da caixa também teve um episódio de mau funcionamento. Logo a boia de 100%, o que fez transbordar pelo ladrão.

Ainda não sei exatamente por que falhou, mas no dia anterior tinha refeito a ponta RJ-45 do cabo que uso para conectar aos sensores, e também havia um fragmento de plástico boiando na caixa, resultado de uma intervenção do encanador, que poderia ter enroscado na chave-boia. Sanei essas questões e por ora não houve mais falhas de boia.

Novamente, se soubesse que isso podia acontecer, teria comprado um modelo diferente de chave-boia, menos suscetivel a travar com sujeiras. Também teria adicionado redundância na boia de 100%, o que planejava fazer mas desisti por achar que era paranoia.

Por outro lado, o software novamente beneficiou-se do caso fortuito, ganhando um timeout mais estrito entre 80% e 100%. A falha do sensor 100% ainda faria água sair pelo ladrão, porém pouca. (O controlador já tratava a faixa 80%-100% da caixa de forma especial, retardando o bombeamento enquanto o nível não cai abaixo de 80%.)

Rust

Como disse no texto anterior, considero C/C++ totalmente inadequado para desenvolvimento novo. É bem verdade que meus projetinhos ainda são em C++ (framework Arduino), mas é meio como um relacionamento abusivo, é o "demônio conhecido". Estou trabalhando há vários anos com ele, desde o projeto LoRaMaDoR. Então já deu tempo de criar ferramentas para mitigar as deficiências. Os projetos têm unit test, code coverage e quase 100% do código roda no Valgrind.

Em primeiro lugar, sei que é meio amador usar o framework Arduino, mas acho-o prático, é portável e as bibliotecas são abundantes. Utilizar diretamente a API da plaquinha, por exemplo o ESP-IDF para a família ESP32 da Espressif, é muito mais poderoso e "leve", porém mais complicado e obviamente incompatível com outras famílias de hardware.

Mas a linguagem C++ tem de ir embora. No momento, Rust parece ser a grande sucessora do C/C++. A Espressif parece concordar com isso, e está investindo muito forte em criar um ecossistema Rust para seus produtos. Dá até gosto ver a união de um hardware bom como é a família ESP com um suporte de software de primeira linha. (Bem que eles podiam lançar um ESP64 capaz de rodar Linux, aí a gente mandava o caríssimo Raspberry Pi Zero W às favas.)

A linguagem Rust especifica padrões na forma de traits, que são semelhantes a interfaces ou classes abstratas. Os pacotes embedded-hal e embedded-svc especificam traits para desenvolvimento embarcado. A ideia é que cada plataforma de hardware implemente os mesmos traits para manter a portabilidade do código do usuário final.

Existem duas doutrinas ou modalidades de desenvolvimento embedded no Rust:

Num primeiro momento a Espressif concentrou os esforços na modalidade "std". O pacote esp-idf-sys é um adaptador da biblioteca ESP-IDF para Rust, e isto é a base para todo o resto. O pacote esp-idf-hal implementa os traits de embedded-hal, e o pacote esp-idf-svc implementa os traits de embedded-svc.

Se você experimentar com esse framework, vai notar logo que o firwmare gerado fica bem gordinho. Até o processo de flash demora bem mais que, por exemplo, no Arduino. E não é um framework 100% Rust, uma vez que a biblioteca ESP-IDF é escrita em C.

Mais recentemente, a modalidade "no_std" passou a receber mais atenção. O suporte aos recursos e às placas está cada vez mais próximo da paridade com "std". O pacote esp-hal implementa os traits da embedded-hal. Os pacotes opcionais esp-alloc, esp-println, esp-storage, etc. suprem as lacunas causadas pela ausência da biblioteca-padrão.

Além das vantagens naturais do "no_std", é notável que o pacote esp-hal conversa diretamente com o hardware, não depende da biblioteca ESP-IDF. Ou seja, a pilha completa de desenvolvimento é 100% puro Rust.

Enfim, isto aqui está parecendo um panfleto de propaganda da Espressif. E a linguagem Rust em si? Já desbancou o C++ na caixa de água?

O fato é que há uma curva de aprendizado pela frente, e bateu uma preguicinha...

E quando a gente quer procrastinar uma tarefa desagradável, logo acha algo mais interessante para ocupar o tempo, e essa coisa no momento é

MicroPython e CircuitPython

Decidi revisitar o ecossistema MicroPython/CircuitPython. Já brinquei com CircuitPython no passado, a ponto de ter colaborado um módulo para ele.

Sim, um motivo de tentar o MicroPython foi "fugir" do Rust. Mas o outro objetivo era buscar uma abordagem mais produtiva para desenvolvimento IoT. Para mim o "Santo Graal" do desenvolvimento IoT seria o uso de uma IDL ou ferramenta low-code/no-code, no estilo do desenvolvimento de aviônicos. Usar Python ou outra linguagem de alto nível é um bom passo nessa direção.

"Ah, mas é lento.". É, mas não precisa muito cérebro para fazer o que o típico projeto IoT faz: ler um sensor e enviar uma mensagem MQTT. Na pior das hipóteses, será necessário escrever um módulo em C (aliás, é mais fácil fazer isso em MicroPython que no Python convencional).

Note que usar Python não torna tudo "facinho" como num passe de mágica. O desenvolvedor ainda tem de entender o que está acontecendo embaixo do capô, e isso um cursinho de 6 semanas não ensina. A palavra-chave aqui é produtividade: chegar mais rápido no destino que você já sabe onde é.

Usar Python em vez de C++ traz desafios novos. Quando um programa C++ quebra ou a memória acaba, o aparelho reinicia. Não é ideal, mas é o melhor desfecho possível. Já o Python exibe uma exceção e pára.

É preciso entender a interação do coletor de lixo do Python com outros elementos, principalmente quando os objetos por coletar representam recursos de hardware — no ESP32, TCP/IP é implementado em hardware e o número de conexões simultâneas é muito limitado, portanto um objeto socket é um recurso de hardware.

A linguagem interpretada exige unit tests com idealmente 100% de cobertura. Ok, isto é verdadeiro para qualquer linguagem (em minha opinião), porém a motivação é diferente: em linguagem interpretada, os testes fazem o papel de "compilador".

Em se tratando de Python embarcado, MicroPython é a "coisa original", digamos assim. Embora ele defina e comercialize um hardware de referência, tem sido portado para inúmeras placas. Recentemente, o Arduino anunciou parceria com o MicroPython; imagino que a ideia de longo prazo seja substituir o C++ por Python.

CircuitPython é um fork do MicroPython mantido pela Adafruit Industries para suas próprias placas. Aparenta ser um projeto mais dinâmico e a API é mais uniforme, independente de hardware. (Também é digno de nota que a Adafruit produz muito código aberto dentro e fora do CircuitPython. Muitas bibliotecas Arduino são mantidas pela Adafruit, o que beneficia a todos nós.)

Minha primeira escolha seria o CircuitPython, exceto que as poucas placas suportadas são caras, e quase nenhuma possui Wi-Fi. Já o MicroPython suporta hardware popular, como a família ESP. (A Raspberry Pico é suportada por ambos.)

As placas CircuitPython possuem suporte nativo a USB e simulam um pendrive, o que torna muito fácil copiar ou editar os fonte Python contidos lá dentro. O MicroPython reserva parte do flash para arquivos, mas é preciso usar alguma ferramenta extra para transferir dados de/para aquele espaço. Nada que não possa ser facilmente automatizado com um Makefile.

Devo dizer que gostei do MicroPython, e me arrependo de não ter perseverado por esse caminho desde 2019. No momento, estou tendendo a converter todos meus projetos IoT para ele. Mas ainda estou acompanhando a robustez. Já encontrei um par de problemas relacionados a MQTT. Estou testando uma reescrita do sensor de temperatura. Quando estiver 100% satisfeito, uso o template para os outros projetos.

O MicroPython roda bem no ESP8266/NodeMCU, porém a escassa RAM limita o código em aproximadamente 500 linhas. A linguagem oferece diversas técnicas para diminuir o footprint do código, mas para projetos casuais/amadores é mais fácil simplesmente adotar o ESP32.