No mundo Arduino, os sensores de temperatura mais comuns são os veneráveis DHT11 e DHT22. Eles costumam vir em todo kit Arduino, até naqueles para crianças. Também estimam umidade relativa, então parecem perfeitos para montar aquela "estação metereológica" (*). Porém, eles têm alguns defeitos:
Para aplicações onde só é necessário ler a temperatura, há um sensor igualmente comum, porém muito melhor: o DS18B20.
Esse dispositivo implementa o protocolo 1-Wire, que é um padrão da indústria e pode conectar muitos dispositivos num mesmo barramento de dados. (Apesar de que não há muitos dispositivos 1-Wire à venda no Brasil, fora o próprio DS18B20.) O protocolo 1-Wire também funciona a distâncias relativamente longas: dezenas ou até centenas de metros a depender da qualidade do cabo.
O nome "1-Wire" vem do fato de alguns dispositivos funcionarem com apenas um fio ativo, o fio de dados, no chamado "modo parasita". Na verdade o fio preto também tem de existir, então é na verdade 2-Wire :) No modo parasita, o sensor extrai energia do próprio fio de dados para funcionar. O consumo de energia dos sensores 1-Wire é minúsculo. Deve haver um resistor "pull-up" no fio de dados.
O modo parasita funciona, mas tem suas desvantagens. O protocolo tem de seguir certas convenções para manter um fluxo de energia suficiente. Há limitações no comprimento do cabo, no número de sensores do barramento e na temperatura máxima de operação. Sempre que possível, é melhor usar 1-Wire no modo alimentado, ou seja, com um terceiro fio fornecendo 3.3V ou 5V (outra vantagem: funciona com 3.3V direto, não precisa de conversor de nível). O resistor pull-up deve ser mantido.
Em todo caso, o sensor 1-Wire desliga quando está em desuso, portanto nunca vai quedar travado. Ele deve ser ativado antes de fornecer uma leitura. Isso colabora para sua resiliência e baixo consumo de energia.
O sensor de temperatura DS18B20 é barato e fácil de encontrar, mesmo no Brasil. É fornecido em diversos empacotamentos, inclusive à prova d'água, podendo ser utilizado em aplicações expostas.
O interfaceamento com o DS18B20 é relativamente simples, há bibliotecas para Arduino e para todas as plataformas embarcadas, e os exemplos são abundantes na Internet. Esta é minha aplicação em MicroPython ESP32, que faz uso de módulos inclusos no MicroPython.
O erro de programação mais comum é esperar tempo de menos entre o comando de ativação do sensor convert_temp e o comando de leitura read_temp. O datasheet recomenda 750ms para a precisão máxima, eu espero um segundo inteiro.
Outro problema é a presença de chips falsos no mercado, cujo comportamento é semelhante porém não exatamente igual ao DS18B20 original. A qualidade dos fakes varia, indo de inutilizáveis até aqueles que implementam recursos extras.
A maioria dos chips falsos falha em modos de uso que o Arduinista típico nunca exercita, então o problema passa desapercebido. O maior sintoma de um chip falsificado ruim é não funcionar em modo parasita. Se o seu exemplar funciona em modo parasita, deve ser bom o suficiente para o feijão-com-arroz que é ler temperatura.
Na verdade, a maior chance é que você adquira um chip falsificado. Para adquirir o original, teria de comprar numa Mouser da vida, pagar DHL e um caminhão de imposto sobre o frete, o que ninguém quer fazer só para comprar um sensor do tamanho de um comprimido...
Os dados dos sensores vão via MQTT para um "observador" de monitoramento, que dispara alarmes tanto para temperatura absoluta quanto para "rampa" positiva de temperatura. Meus sensores monitoram um armário com equipamentos de informática, e o alarme dispara no caso de temperatura absoluta acima de 40ºC ou rampa maior que +1ºC por minuto.
O alarme por rampa é importante pois, num incêndio, é possível que os sensores quedem fora do ar antes de conseguir reportar uma temperatura absoluta alta. A rampa combinada com o desligamento são um canário bastante relevante.
Para que este monitoramento seja efetivo, o sensor deve reportar mudanças tão rápido quanto possível. Por outro lado, não queremos "poluir" o MQTT com mensagens muito frequentes, tanto porque nosso link secundário é bem lento (100kbps!) quanto porque a nobreza obriga.
Também queremos evitar oscilações "falsas", que poderiam causar falsos alarmes de rampa. Um último fator é melhorar a aparência dos gráficos. A temperatura deve evoluir de forma natural, como uma onda, não ficar desenhando um serrilhado.
O primeiro problema que observamos, e chamamos de dithering, é o seguinte. Se publicamos a temperatura como um número redondo, mas ela ficar oscilando entre e.g. 24.49ºC e 24.51ºC, isto daria causa a um grande número de mensagens MQTT, reportando ora 24ºC ora 25ºC. O gráfico mostraria uma oscilação que não está realmente acontecendo na vida real.
No nosso caso, publicamos com 1 casa decimal, mas o tópico MQTT só publica uma mensagem nova se a temperatura for diferente da anterior em pelo menos 0.1ºC (ou após um timeout de vários minutos). Isto significa que cada mensagem nova corresponde a uma mudança relevante de temperatura. O gráfico acima mostra uma curva muito mais agradável depois das 21:00 quando introduzimos esta mudança.
O próximo problema foi uma instabilidade em apenas um dos sensores, semelhante ao dithering. A provável fonte da oscilação é um servidor que tem carga intermitente e ora gera mais calor, ora menos.
O primeiro instinto seria aplicar um filtro, como uma média móvel, mas filtros aplicados sem critério reduzem o tempo de resposta do sensor, o que negaria a principal utilidade do sensor, que é detectar um aquecimento anormal (possivelmente seguido de uma falha). Seria estúpido passar um filtro extremamente restritivo e um sobreaquecimento demorar 10 minutos para aparecer no gráfico.
Tentamos aplicar um filtro de Kalman, algo que já ensaiamos fazer várias vezes no nosso sisteminha IoT, mas só agora pareceu fazer sentido. O filtro de Kalman é uma média móvel, porém com peso variável e baseado em critérios estatísticos.
Como se pode ver no gráfico acima, o filtro de Kalman até ajuda, a partir das 9:00, porém a melhoria foi bem discreta. Mas outro recurso do filtro de Kalman é que ele também nos informa a precisão da estimativa, que muda ao longo do tempo. E se usássemos essa precisão de alguma forma?
O gráfico acima mostra uma grande melhora na linha lilás. Em vez de usar a constante de 0.1 para limitar o dithering, utilizamos a precisão informada pelo próprio filtro de Kalman. A lógica é que, se a mudança de temperatura é menor que a precisão calculada para a medição, não é necessário publicá-la.
Da forma que nosso filtro de Kalman foi calibrado, essa precisão converge para 0.17ºC, ainda um valor pequeno, mas quase o dobro que 0.1ºC. Além de ser um filtro consideravelmente mais forte, ele é baseado em estatística, não é um número tirado de onde o sol não brilha.
Em primeiro lugar, nós publicamos a temperatura com precisão de 0.1ºC, mas isto é uma escolha arbitrária e certamente não é adequada para muitas aplicações. Planejo tornar isto configurável no futuro (supondo que alguém vá utilizar diretamente meu código-fonte IoT, o que é improvável, mas a nobreza obriga).
Para calibrar o filtro de Kalman, é preciso entender superficialmente como ele funciona. Há muitos textos e livros excelentes sobre o tema. Nós mesmos publicamos um artigo sobre o tema, com um adendo sobre o assunto quando se trata de sistemas estáveis e incontroláveis.
Um termômetro é um sistema estável, porque a temperatura futura tende a ser igual à temperatura presente. Também é um sistema incontrolável, porque não podemos influir na temperatura ambiente com ele. (A antítese disso seria um aquecedor de água..)
A calibração do nosso filtro de Kalman pode ser encontrada no código-fonte. Em se tratando de um sistema estável incontrolável, um sensor e uma medida escalar, os parâmetros são os seguintes:
O valores "x" e "P" são atualizados a cada rodada do filtro de Kalman, enquanto "Q" e "R" são constantes, em nosso caso.
O valor de "R" sai quase que diretamente do datasheet do DS18B20. Lá diz que a precisão do sensor é +/-0.5ºC. Em geral, "precisão" quer dizer desvio-padrão. Uma vez que P, Q e R têm unidade de variância, R = quadrado de 0.5 = 0.25.
O valor inicial de "x" pode ser uma temperatura arbitrária e.g. 30ºC, ou pode ser a primeira medição obtida do sensor. Adotamos a segunda opção. O valor inicial de "P" reflete a qualidade da estimativa inicial de "x". No nosso caso, arbitramos "P" bem grande pois não confiamos muito nessa estimativa.
As saídas do filtro de Kalman que utilizamos diretamente são "x" e "P", que expressam a estimativa da temperatura atual e a incerteza dessa estimativa, respectivamente. O filtro anti-dithering utiliza a raiz quadrada de P, que é a incerteza expressa como desvio-padrão.
Enquanto "P" expressa a incerteza da estimativa no presente, "Q" expressa a incerteza da estimativa no futuro. Se a temperatura ambiente não pudesse mudar, "Q" seria igual a zero. Se a incerteza é função do tempo, como é o caso da temperatura ambiente, é preciso expressar Q como variância/tempo e escalar o valor conforme a freqüência de medições do sensor.
Estimar "Q" é a parte complicada. Sabemos que normalmente a temperatura ambiente é muito estável, mas se desejamos que o sensor detecte anomalias, precisamos admitir uma instabilidade maior. No momento, adotamos um desvio-padrão de 0.25ºC por minuto, devidamente convertido para variância por milissegundo (pois nosso sistema IoT mede o tempo em ms). Fizemos testes aquecendo os sensores com a mão ou com um isqueiro e a detecção da anomalia permaneceu rápida.
O valor de "Q" é uma medida de variância, não um teto absoluto. Uma incerteza de 0.25ºC por minuto não limita a mudança de temperatura do sistema a essa taxa. Significa que, em 2/3 dos casos, a mudança ficará abaixo dessa taxa (e 1/3 acima), de acordo com as regras da distribuição normal. A "beleza" do filtro de Kalman é justamente usar todas as incertezas para produzir a melhor estimativa possível.
(*) Pessoalmente, montei minha estação metereológica com um chip BME280, que também mede pressão atmosférica. O código está aqui.
Porém esse negócio não é tão simples. Achei muito difícil posicionar a estação de modo a obter leituras realistas. Não pode ficar exposto ao sol, não pode pegar chuva, mas também não pode ficar num ambiente confinado. Tem de ficar "ao tempo", como numa varanda grande, longe de qualquer superfície aquecida direta ou indiretamente pelo sol, e longe de qualquer fonte de calor, por menor que seja (inclusive o próprio Arduino, bem como sua fonte de alimentação).
Se a estação metereológica tivesse anemômetro e pluviômetro, teríamos requisitos de funcionamento adicionais e conflitantes com os supracitados. Teria de posicionar os sensores remotamente, ou fazer uma estação separada para cada um. Em resumo, achei trabalho demais para benefício de menos (a Copel tem um sensor perto de casa e fornece a informação de graça no site) e larguei de mão, pelo menos por ora.