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 microserviços, que parece uma ideia muito boa, até a hora que se descobre que cada microserviç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 simplesmente perder conectividade, sem razão aparente. O programa continua rodando, mas a rede 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.

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.

Seja como for, o fato é que uma fonte decente, de qualquer tipo, custará mais caro que a plaquinha do microcontrolador.

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 para 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 do ESP-IDF para Rust, e é 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.

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 ESP-IDF. Ou seja, a pilha completa de desenvolvimento é 100% puro Rust.

Enfim, isto aqui está parecendo uma 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. A linguagem interpretada exige unit tests com idealmente 100% de cobertura (ok, precisamos disto em qualquer caso, mas para cada linguagem a prioridade das motivações é diferente). É preciso entender a interação do coletor de lixo do Python com outros elementos.

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 MicroPython. Muitas bibliotecas Arduino são mantidas pela Adafruit, principalmente aquelas que suportam periféricos vendidos pela Adafruit.)

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.