Really Smart Box: Solução Inteligente de Armazenamento IoT com Sigfox
Componentes e Materiais
| Chapa de acrílico (ex: 3mm perspex) | 1 |
| Arduino MKR FOX 1200 | 1 |
| Amplificador de Célula de Carga HX711 (SparkFun) | 1 |
| Célula de carga 5kg | 2 |
| Sensor BME280 (Adafruit) | 1 |
Ferramentas e Máquinas Necessárias
| Cortadora a laser (genérica) |
| Impressora 3D (genérica) |
Aplicativos e Serviços Online
| Tinamous |
| Sigfox |
Sobre o Projeto
A plataforma Really Smart Box transforma uma Really Useful Box (tm) em uma caixa de armazenamento inteligente, conectada à internet, para monitoramento de estoque. Baseada no Arduino MKR FOX 1200 com Sigfox, ela detecta o peso dos itens armazenados na caixa, junto com temperatura e umidade, e usa o rádio Sigfox de baixa potência para transmitir essas informações.
Caso de Uso - Armazenamento de Filamento para Impressora 3D
Se você possui uma impressora 3D, provavelmente se importa com como seu filamento é armazenado. Isso vale não só para filamento, mas muitas outras coisas precisam ser armazenadas em faixas aceitáveis de temperatura e umidade (por exemplo, massa de vedação pode se tornar inutilizável se exposta a temperaturas abaixo de zero).
Como uma das pessoas responsáveis por manter as impressoras 3D no meu makerspace local, preciso garantir que temos estoque suficiente de filamento e que ele seja mantido seco.
Com a Really Smart Box, consigo monitorar o peso do filamento e saber se está acabando, além de monitorar o nível de umidade na caixa para saber se o gel de sílica precisa ser trocado.
Caso de Uso - Controle de Estoque de Consumíveis
Um contratante de limpeza pode querer manter um estoque de aromatizadores, sabonetes ou outros consumíveis no local do cliente. O cliente provavelmente não permitiria acesso WiFi ao contratante ou alimentar um dispositivo como este quando não presente, no entanto, a empresa do contratante precisa saber quando enviar novo estoque, adicionando overhead ao tempo dos limpadores e papelada extra que ninguém gosta.
A plataforma Really Smart Box simplesmente cai dentro de uma caixa de armazenamento. Como usa Sigfox, não precisa conectar à rede do cliente e é de baixa potência, funcionando com um conjunto de baterias. Como o conteúdo da caixa mudaria muito infrequentemente, o Arduino pode ser mantido em estado de baixa potência na maior parte do tempo, ajudando a estender a vida útil da bateria.
A plataforma pode ser informada do peso do tipo de item armazenado na caixa (ex: os aromatizadores) e assim calcular quantos estão na caixa. Isso pode então ser enviado à empresa do contratante para alertá-los quando precisarem de mais estoque para ser entregue no local do cliente.
Construindo a Plataforma
A plataforma é de construção simples. O corpo principal é feito de duas peças de acrílico cortadas a laser (usei 3mm, mais grosso seria melhor para itens pesados como filamento de impressora) com um par de células de carga entre elas.
Eu manualmente fiz furação cônica nos furos dos parafusos da célula de carga para dar um acabamento melhor. Ainda não encontrei uma cortadora a laser que faça furação cônica!
O acrílico pode ser cortado em qualquer tamanho para combinar com a caixa que você gosta, porém observe o posicionamento das células de carga e o comprimento do fio nelas, pois é bastante curto. A chapa superior de acrílico é ligeiramente menor que a inferior para garantir que não enganche nas laterais da caixa.
A chapa inferior tem um recorte para permitir que a eletrônica seja montada no acrílico sem espaçadores e tenha as pernas dos componentes soldadas atravessando. Usei rosca M3 nas seções dos cantos do recorte para parafusar diretamente a PCB. Pés impressos em 3D também foram instalados nos cantos para garantir que quaisquer parafusos da célula de carga que não ficassem nivelados com o acrílico não afetassem o equilíbrio.
O peso é detectado usando duas células de carga de 5Kg. Um conjunto de 4 é normalmente usado em balanças de banheiro e isso poderia ser melhor, porém não encontrei uma forma legal de fixá-las ao acrílico e fornecer o espaçamento necessário para a eletrônica.
As células de carga precisam de algum amortecimento na parte superior e inferior para permitir uma pequena flexão e os sensores de extensômetro reais (a parte branca na foto) da célula de carga são mais grossos que o bloco de montagem. Isso é alcançado sob a célula de carga com as duas placas finais "Really Smart Box" impressas em 3D que têm um pequeno bloco para elevar a célula de carga. No topo da célula de carga estão alguns blocos de acrílico cortados a laser como amortecimento.
As células de carga são conectadas a um amplificador HX711. Este tem dois canais (A e B) que podem ser selecionados, o que é perfeito para este uso.
Cada célula de carga é construída com extensômetros em configuração de ponte Wheatstone, criando um par desbalanceado de divisores de potencial. Quando a célula de carga é colocada sob carga, a resistência dos extensômetros muda e, portanto, uma diferença entre os dois divisores de potencial é criada. Isso é amplificado e medido pelo HX711, que realiza a conversão analógico-digital para nós.
Usei duas células de carga de 5kg para este projeto. Você pode obter diferentes classificações (ex: 1kg e 10kg) que funcionam exatamente da mesma forma, mas com sensibilidade diferente.
Ao posicionar as células de carga, certifique-se de que a seta na extremidade da célula aponta para baixo (na direção da carga). As células têm furos roscados M5 em uma extremidade (geralmente a extremidade fixa) e M4 na outra (o lado onde você coloca a carga).
Os fios vermelho/preto são alimentação, isso alimenta o topo e a base dos divisores de potencial e é compartilhado entre as duas células de carga. Verde e branco são os fios de sentido do meio dos divisores de potencial, conectados aos Canais A e B no HX711.
O HX711 suporta 3 fatores de ganho, mas estes também são usados para seleção de canal. Ganhos de 128 e 64 estão disponíveis no canal A, enquanto selecionar ganho 32 seleciona o canal B. Isso significa que nosso segundo canal não será tão sensível quanto o canal principal, o que é bom para esta aplicação.
O HX711 pode ser conectado a quaisquer pinos digitais no Arduino. Usei D0 (Dados) e D1 (Clock), então o amplificador só precisa ser conectado à alimentação 3v3 do Arduino.
Você pode ler mais sobre células de carga e o HX711 no excelente tutorial de célula de carga da SparkFun.
Finalmente, um BME280 é conectado ao barramento I2C e usado para detectar a temperatura e umidade dentro da caixa. Isso também pode detectar pressão, porém provavelmente é de pouco interesse e temos apenas 12 bytes de dados Sigfox para trabalhar, então não é reportado.
A eletrônica é montada em uma placa de prototipagem ThingySticks Arduino. Adicionei um suporte de bateria (colado com cola quente na chapa inferior de acrílico) e conectei a antena, que tem um design plano agradável, funcionando perfeitamente para a plataforma.
Calibração da Célula de Carga
Antes de usar a plataforma, as células de carga precisam ser calibradas. Cada célula de carga é única, feita anexando extensômetro a um bloco de metal e furos perfurados para fornecer flexão suficiente que pode ser detectada sem quebrar. Por isso, precisamos calibrar cada célula para sua resposta ao peso.
Uma vez calibradas, aplicamos a equação y=mx+c ao valor ADC medido (x) para obter o peso real (y). Precisamos encontrar c (o offset) e m (a inclinação) para nossa célula de carga.
Removi a tampa superior da plataforma principal e fixei um pequeno quadrado de acrílico em cada célula de carga por vez, e monitorei os valores medidos (há uma rotina no firmware para fazer isso, que pode ser iniciada enviando "c" para a porta serial).
Inicialmente, a leitura para uma plataforma vazia foi medida, isso dá o valor do offset (c). Então, colocando uma carga de peso conhecido na célula, a diferença nas leituras nos dá a inclinação.
Inclinação = (medido - offset) / peso (g).
Usei tanto uma pequena lata de aromatizador (cerca de 230g) quanto um carretel de filamento de impressora (cerca de 1.5kg) para verificar os valores. Ambos deram aproximadamente a mesma inclinação, o que foi tranquilizador.
Naturalmente, o offset medido com o pequeno bloco de acrílico é diferente do experimentado com a tampa completa. Da mesma forma, há também uma pequena diferença de inclinação ao usar ambas as células de carga, então uma calibração secundária é necessária. Por enquanto, um offset de tara de um ponto é usado, isso é definido no firmware mas também pode ser definido via conexão serial USB ou através de uma mensagem downlink Sigfox uma vez implantado.
Conexão Sigfox
Com a Really Smart Box ligada, inicialmente usei a porta serial USB para monitorar a saída e ajudar a depurar e ajustar o sistema. Assim você pode ver as células de carga individuais, mudanças e ruído. Porém isso não vai funcionar para uma caixa implantada, pois precisa ser totalmente sem fio.
Com Sigfox, podemos enviar 12 bytes de dados até 140 vezes por dia para nosso serviço online, isso é mais que suficiente para a Really Smart Box. A estrutura de dados abaixo é usada no Arduino e descreve como usamos os 12 bytes.
typedef struct __attribute__ ((packed)) sigfox_message {
uint8_t status; // flags de status
int8_t humidity; // umidade::int:8 (alguns sensores HTU21D leem umidade negativa)
int8_t temperature; // temperatura::int:8 (sem casas decimais)
int16_t zeroWeight; // zeroWeight::int:16:little-endian
int16_t weight; // weight::int:16:little-endian
int16_t itemCount; // itemCount::int:16:little-endian (100x contagem real para permitir 2.01)
int8_t driftCorrection;// Correção de deriva para mudanças no peso zero
int8_t filler; // Nada para ver aqui, siga em frente...
int8_t lastStatus; // Último status Sigfox
} SigfoxMessage;
O primeiro byte (status) é dividido em flags de bits para indicar problemas:
// status::uint:8 -> Dividido em 8 bits
// B7 - Primeira execução
// B6 - Falha HX711
// B5 - Falha BME280
// B4 - Alarme de temperatura
// B3 - Alarme de umidade
// B2 - Alarme de peso
// B1 - Estoque baixo
// B0 - reserva
Esta estrutura compacta para 12 bytes, porém precisamos desempacotá-la no lado Sigfox para enviar ao Tinamous. Usamos uma Configuração de Payload Personalizada para isso e é melhor resolver isso conforme a estrutura de dados é definida. A nossa é:
firstRun::bool:7 hx711Fault::bool:6 bmeFault::bool:5 temperatureAlarm::bool:4 humidityAlarm::bool:3 weightAlarm::bool:2 lowStock::bool:1 b0::bool:0 status::int:8 humidity::int:8 temperature::int:8 zeroWeight::int:16:little-endian weight::int:16:little-endian itemCount::int:16:little-endian
O payload personalizado divide nossos 12 bytes conforme é parseado.
Note que precisamos especificar a natureza little-endian de qualquer coisa maior que 1 byte, pois Sigfox padroniza para big-endian e o Arduino usa little-endian (o byte menos significativo vem primeiro em palavras multi-byte).
Também note que separar as flags booleanas no primeiro byte não avança o marcador de byte como faz com todas as outras leituras, então o byte de status que contém todas as flags também é lido para pular o primeiro byte.
Incluídos nas flags estão alarmes de faixa de temperatura, umidade e peso. Poderíamos usar um serviço online (ex: Tinamous) para monitorar fora de faixa, porém estes podem ser curtos (algumas horas) e nossa caixa pode enviar infrequentemente (uma ou duas vezes por dia). A condição ambiental danosa resultante poderia facilmente ser perdida, então são sinalizados no dispositivo e enviados (e resetados após envio bem-sucedido).
O itemCount é na verdade 100x a contagem real. Queria permitir valores como 2.2 itens (devido a erro de peso ou outros itens na caixa) sem forçar arredondamento. Também não queria usar float que requer mais espaço, então usei uma palavra de 16 bits e apliquei um fator para permitir conversão fácil (também é signed para permitir erro zero, que pode resultar em nível de estoque -1 ou -2 etc.).
Muito pouco precisa ser feito para habilitar comunicação Sigfox. No Arduino, a biblioteca Sigfox é adicionada e funções apropriadas chamadas para enviar os dados conforme exemplos Arduino para a biblioteca Sigfox. Porém precisamos registrar nosso dispositivo no Sigfox.
Enviando "s" para a porta serial da Really Smart Box imprime o ID Sigfox e código PAC, usados para ativar o dispositivo no backend Sigfox. Então vamos ao serviço de ativação backend Sigfox e seguimos o assistente: primeiro selecionando nosso dispositivo, depois país/provedor e alguns detalhes.
E finalmente nosso dispositivo está ativado e listado:
Sigfox atribui dispositivos a agrupamentos de Tipo de Dispositivo, o que é sensato pois você pode normalmente ter muitos (centenas, milhares, etc.) do mesmo tipo de dispositivo que deseja agir como grupo. Com o Tipo de Dispositivo definido, podemos configurar um callback personalizado para enviar os dados recebidos ao nosso serviço online. Estou usando Tinamous para isso.
Usando a Really Smart Box
Uma vez ligada, parafusada, firmware gravado e baterias instaladas, a plataforma é simplesmente colocada dentro de uma Really Useful Box (tm) e pronta para uso.
A alimentação deve ser aplicada o mais tarde possível, pois uma vez que o dispositivo é ligado, ele enviará a primeira mensagem Sigfox após 2 minutos e solicitará dados downlink. Estes dados podem incluir um comando "Zero" para zerar o peso da plataforma. Caso contrário, uma conexão serial USB é necessária ou aguardar a próxima solicitação downlink - estas são feitas a cada 12 horas.
Uma vez funcionando, a plataforma publicará mensagens Sigfox a cada 15 minutos para enviar peso, contagem de itens, temperatura, umidade e estados de alarme. Temperatura, umidade e peso são medidos a cada minuto para garantir que não estejam fora de faixa, com alarmes sinalizados para a próxima transmissão se tiverem sido acionados.
Monitoramento com Tinamous
Tinamous suporta callbacks personalizados Sigfox adicionando um "Sigfox Bot" à nossa conta. Para instruções, veja meu tutorial "Get Your Sigfox On" no Hackster.io.
Ao adicionar um Sigfox Bot à sua conta Tinamous, se você incluir as configurações da API, o Sigfox Bot fará lookup dos seus dispositivos e os adicionará à sua conta Tinamous. Porém não precisa fazer isso, pois o dispositivo será adicionado automaticamente quando dados forem publicados.
Quando você adiciona o Bot, é apresentada uma tela de configuração de callback para ajudar a configurar os Callbacks Sigfox.
Você pode então criar um callback personalizado no Sigfox. Note que a Really Smart Box usa callback DATA -> BIDIR que lida com o callback UPLINK normal e callbacks BIDIR (up e downlink).
É aqui que o payload personalizado anterior ajuda. Cole isso do código fonte no payload personalizado e atualize a seção de campos para refletir isso.
Lat e Lng são especificados neste callback, dando uma localização aproximada. Porém Sigfox no Arduino suporta configuração de localização melhorada, mas isso precisa de um segundo callback. Se usar a facilidade de Geo location, não especifique Lat/Lng nesta mensagem, pois a Really Smart Box parecerá estar se movendo entre localizações.
Uma vez configurado, também precisa ser habilitado para Downlink, que é desabilitado por padrão mesmo com BIDIR configurado.
Com um callback Downlink, também queremos especificar um callback SERVICE -> ACKNOWLEDGE para saber que nosso dispositivo recebeu os dados downlink. Clicando no Sigfox Bot no Tinamous, outras configurações de callback são mostradas. Siga as instruções para callbacks ACKNOWLEDGE e GEOLOC.
Note que você precisa copiar o header de autorização do primeiro callback uplink/bidir, pois esta é uma senha criptografada de mão única no Tinamous e não está mais disponível para exibição.
Com nossos callbacks no lugar, os dados publicados pelo nosso dispositivo agora devem estar sendo enviados ao Tinamous. Também poderíamos adicionar callbacks de email no Sigfox, o que pode ajudar a confirmar que os dados estão chegando (mas também pode ficar muito barulhento rapidamente).
Configurando o Dispositivo Tinamous
Uma vez que o dispositivo Sigfox foi visto no Tinamous (seja através da API lookup ou um callback), ele será mostrado na página de Dispositivos. A partir daqui, podemos editar as propriedades. Campos são adicionados automaticamente conforme vêm do callback Sigfox, então é melhor esperar até o dispositivo estar publicando dados.
Defini o tempo "Not Reporting After" para 1 hora (atualmente publicando a cada 15 minutos) para saber se o dispositivo quebrou e opcionalmente ser notificado sobre isso.
Não queria ver todos os campos enviados pelo dispositivo na página de gráfico/detalhes (há muitos se incluir todas as flags), então o Tinamous foi configurado para mostrar apenas peso e contagem de itens. Rótulos e unidades amigáveis também foram aplicados.
O campo Item Count é 100x a contagem real, então uma calibração é aplicada a esse campo para reduzi-lo 100 vezes.
Alguns dados downlink são definidos para causar a Really Smart Box a zerar e aplicar limites de faixa de temperatura e umidade quando solicitar próxima mensagem downlink (2 minutos após ligar, depois a cada 12 horas).
Visualizando Informações da Really Smart Box
Agora os campos do dispositivo estão configurados, podemos monitorá-los através da página de detalhes do dispositivo (Nota: não tinha zerado a plataforma neste momento, por isso acha que 1/2 unidade está presente. Também troquei a tampa de acrílico por uma versão de 5mm, mais pesada, mas que aguenta filamento melhor).
Também podemos ver as interações de callback Sigfox na seção Sigfox. Note aqui que os dados downlink estão sendo enviados e acked, porém o Arduino reporta um erro. Mais sobre isso no final.
Na aba Localização, também podemos ver onde nossa Really Smart Box está, útil se esquecer em qual cliente está, ou se está em uma van.
E naturalmente queremos uma visão de dashboard bonita da nossa Really Smart Box. A abaixo mostra o peso do conteúdo da caixa, as unidades estimadas nela, e uma contagem de dispositivos que não estão reportando para saber se um quebrou.
Recebendo Notificações com Tinamous
Em seguida, configurei o Tinamous para enviar email e SMS quando a contagem de itens está baixa. Fiz isso especificando uma faixa de trabalho de 3-300 para o campo item count. Se o valor estiver fora desta faixa, um evento de medição fora de faixa é acionado.
Adicionando uma Notificação ao Tinamous, podemos ser notificados quando isso acontece.
Poderíamos especificar apenas o campo de interesse, mas deixando vazio, obtemos notificações para qualquer campo fora de faixa.
Da mesma forma para dispositivos, deixe em branco para todos (ex: o único que temos no momento!)
Defina Repetir Notificações para acionar apenas uma vez, até ser resetado (todos os dias). Caso contrário, notificações a cada 15 minutos ficam irritantes muito rápido!
Então selecione como ser notificado. Configurei para email e SMS e criei a notificação:
Conclusão
Agora posso implantar a Really Smart Box e (esperançosamente) esquecer sobre ela. Quando o nível de estoque ficar baixo, serei notificado e posso verificar no dashboard como está indo. Usando Sigfox, não preciso me preocupar em alimentar o dispositivo além de uma troca ocasional de bateria e nenhuma configuração WiFi no local é necessária, tornando a implantação extremamente simples.
Planejo implantar esta unidade em uma de nossas caixas de armazenamento de filamento no Cambridge Makespace para monitorar níveis de estoque de filamento.
Problemas a Resolver
Nem preciso dizer que isso não é um projeto de qualidade de produção finalizado. Alguns problemas ainda precisam ser resolvidos:
Sigfox Downlink: Sigfox permite 4 mensagens por dia em resposta a uma mensagem uplink. A Really Smart Box usa estas para permitir re-zero das balanças, definir limites superiores e inferiores de temperatura/umidade e peso do item. Porém, ao tentar fazer funcionar, mesmo a mensagem downlink parecendo ser enviada e acked (como mostrado no backend Sigfox), o Arduino reporta erro de status 62, que não mapeia para nenhuma condição de flag de erro listada para o chip ATA8520. Investigando os drivers, o comando usa uma solicitação de downlink também não listada na datasheet, então mais investigação é necessária.
Modo Debug Apenas: Executar comunicações Sigfox com debug desabilitado causa ativação da configuração de baixa potência do Arduino, que mata a porta serial USB.
Modo Baixa Potência: Como descrito para configuração debug Sigfox, usar a biblioteca Low Power do Arduino causa queda da porta serial USB, então não está habilitado no momento.
Deriva: Nenhuma compensação para deriva foi adicionada. Sem dúvida, as células de carga derivarão quando mantidas sob carga constante.
Ruído / Medições Não Verticais: É possível que a Really Smart Box esteja na traseira de uma van (ex: limpador móvel, carpinteiro, encanador). Seria bom adicionar um acelerômetro à plataforma e pular ciclos de medição quando a caixa não estiver estável. Da mesma forma, se a caixa não estiver vertical, o peso não passará pelas células de carga como esperado.
Código
Código Arduino Really Smart Box
Adicione bibliotecas para Arduino MKR FOX 1200, HX711, AdaFruit BME280, Arduino Low Power. Use a Arduino IDE para programar normalmente.
// Really Smart Box
// Mede o peso do conteúdo de uma really smart box
// Feita por duas chapas de acrílico com 2 células de carga entre elas
// colocadas em uma really useful box.
// Também inclui um BME280 para medir temperatura e pressão dentro da caixa.
// Autor: Stephen Harrison
// Licença: MIT
#include <Adafruit_BME280.h>
#include <HX711.h>
#include <SigFox.h>
#include <SigFox.h>
#include <ArduinoLowPower.h>
// --------------------------------------
// BME280 na porta I2C.
Adafruit_BME280 bme;
// --------------------------------------
// Amplificador de célula de carga HX711.
// 0: D0 - DOUT
// 1: D1 - CLK
// ganho inicial 128.
HX711 scales(0, 1, 128);
// Arrays para células de carga. Índice 0 == Canal A, Índice 1 == Canal B.
float gain[] = {128, 32};
// Fatores de calibração.
// usamos y = mx + c (c = offset, m = scaleFactor).
// para converter o valor medido em peso.
// Defina isto para o offset reportado pelas células de carga sem peso nelas.
float offset[] = {0, 54940};
// Defina isto para o fator calculado quando um peso é colocado na balança.
// Defina o offset primeiro, re-flash o arduino para efeito
// coloque um peso na balança e divida o valor bruto medido pelo peso.
// usando scaleFactor = valor medido / peso.
float scaleFactor[] = {378.f, 260.9f};
// --------------------------------------
// Sigfox
// Esta é a estrutura de dados que publicamos no Sigfox.
// Separe os bits como flags bool do primeiro byte de status mas o byte ainda precisa ser
// incluído caso contrário humidity vira o status
// firstRun::bool:7 hx711Fault::bool:6 bmeFault::bool:5 temperatureAlarm::bool:4 humidityAlarm::bool:3 weightAlarm::bool:2 lowStock::bool:1 b0::bool:0
// status::int:8 humidity::int:8 temperature::int:8 zeroWeight::int:16:little-endian weight::int:16:little-endian itemCount::int:16:little-endian
typedef struct __attribute__ ((packed)) sigfox_message {
uint8_t status; // status::uint:8 -> Dividido em 8 bits
// B7 - Primeira execução, B6 - Falha HX711, B5 Falha BME280, B4 Alarme temp, B3 - Alarme umidade, B2 - alarme peso, B1 - Estoque baixo, B0 - reserva
int8_t humidity; // humidity::int:8 (sim alguns sensores HTU21D leem umidade negativa)
int8_t temperature; // temperature::int:8 (sem casas decimais).
int16_t zeroWeight; // zeroWeight::int:16:little-endian
int16_t weight; // weight::int:16:little-endian
int16_t itemCount; // itemCount::int:16:little-endian (100x contagem real para permitir 2.01)
int8_t driftCorrection;// Correção de deriva para mudanças no peso zero aplicado às balanças.
int8_t filler;
int8_t lastStatus; // Último status sigfox
} SigfoxMessage;
// Tempo da última mensagem Sigfox publicada
long lastPublish = 0;
// Tempo do último downlink Sigfox solicitado.
// Máximo 4 por dia permitidos.
long lastDownlink = 0;
uint8_t lastSigfoxStatus = 0;
// --------------------------------------
// Aplicação/estado
// Se o primeiro ciclo (após reset) para o ciclo medir/publicar
// (usado para solicitar mensagem downlink do Sigfox).
// Note que apenas 4 são permitidos por dia, então cuidado ao implantar código.
bool isFirstCycle = true;
// Modo da aplicação
// 0: Normal
// 1: Calibração
int mode = 0;
// Qual canal deve ser lido durante calibração.
int calibrate_channel = 0;
// Último valor médio medido para cada canal.
float lastAverage[] = {0, 0};
// Peso atual do conteúdo da caixa
float currentWeight = 0;
// Peso das unidades que a caixa vai conter.
// Atualizável via mensagem downlink Sigfox.
float unitWeight = 238;
// Diferente de tara, seria um zero manual em uma leitura definida das balanças
// Isso provavelmente mudará com deriva (tempo/temperatura/etc)
// e deve ser definido uma vez que a balança está no lugar mas não carregada.
// Atualizável via mensagem downlink Sigfox.
float zeroWeight = 0;
bool bmeOk = true;
bool hx711Ok = true;
// Alarmes e faixas de alarme
float minTemperature = 5.f;
float maxTemperature = 60.f;
float minHumidity = 0.f;
float maxHumidity = 60.f;
float maxWeight = 10000; // 10kg
bool temperatureAlarm = false;
bool humidityAlarm = false;
bool weightAlarm = false;
float currentTemperature = 0;
float currentHumidity = 0;
float stockLevel = 0;
bool lowStock = false;
float minStock = 5;
// Configura o Arduino.
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
// Inicializa serial:
Serial.begin(9600);
// NB: O sensor que estou usando (de vendedor aleatório eBay)
// não usa o endereço padrão.
bmeOk = bme.begin(0x76);
if (!bmeOk) {
Serial.println("Não foi possível encontrar sensor BME280 válido!");
}
// Atraso para conexão USB Serial e primeira leitura do BME.
delay(5000);
Serial.println("Really Smart Box...");
printHeader();
}
int delayCounter = 0;
void loop() {
switch (mode) {
case 0:
measureAndPublish();
// Sleep por 1 minuto
// Causando problemas com USB conectado.
// LowPower.sleep(1 * 60 * 1000);
delay(60 * 1000);
break;
case 1:
calibrate();
delay(1000);
break;
}
// Verifica entrada do usuário via porta serial.
checkSerial();
// Medição feita no tick do timer RTC (uma vez por minuto)
delay(100);
}
void measureAndPublish() {
// liga LED para indicar medição
digitalWrite(LED_BUILTIN, HIGH);
printBmeValues();
measureTemperatureAndHumidity();
measureWeight(true);
// Peso, temperatura e umidade lidos a cada minuto
// porém publicamos ocasionalmente.
if (shouldPublish()) {
publishMeasurements();
}
digitalWrite(LED_BUILTIN, LOW);
}
// Loop principal de medição. Lê peso das células de carga
// e armazena se sem ruído da leitura anterior.
void measureWeight(bool printDetails) {
scales.power_up();
delay(500);
float delta = readDelta(printDetails);
if (printDetails) {
Serial.print("\t");
Serial.print(delta, 2);
}
// Se delta entre -1 e 1 (sem ruído)
// atualiza mudança no peso total e unidades contidas
// caso contrário ignora e tenta novamente depois.
// Isso garante que usamos apenas leituras estáveis quando ambos canais não mudaram por duas medições.
if (delta < 1.f && delta > -1.f) {
float lastWeight = currentWeight;
// Calcula peso. Pega peso de ambas células somadas e subtrai peso zerado.
currentWeight = lastAverage[0] + lastAverage[1] - zeroWeight;
// Calcula diferença de peso dos itens na caixa comparado à última leitura estável.
float itemsWeightDelta = currentWeight - lastWeight;
updateStockLevels();
if (printDetails) {
Serial.print("\t\t");
Serial.print(currentWeight, 2);
Serial.print("\t");
Serial.print(currentWeight / unitWeight, 2);
Serial.print("\t");
Serial.print(itemsWeightDelta, 2);
Serial.print("\t");
Serial.print(itemsWeightDelta / unitWeight, 2);
}
}
checkWeightLimits();
if (printDetails) {
Serial.println();
}
// Coloca ADC em modo sleep e desliga LED
scales.power_down();
}
void updateStockLevels() {
stockLevel = currentWeight / unitWeight;
// Diferente de outros alarmes, o nível baixo de estoque
// é resetado se o estoque for reposto.
lowStock = stockLevel < minStock;
}
// Verifica se peso total atual ou peso de uma célula está fora de faixa.
void checkWeightLimits() {
if (currentWeight > maxWeight) weightAlarm = true;
if (lastAverage[0] > (maxWeight / 2)) weightAlarm = true;
if (lastAverage[1] > (maxWeight / 2)) weightAlarm = true;
}
// Lê diferença de peso da última média para esta vez em ambas células.
// Valor médio armazenado no array lastAverage.
float readDelta(bool printDetails) {
float aDelta = readChannel(0, true);
if (printDetails) Serial.print("\t");
float bDelta = readChannel(1, true);
return aDelta + bDelta;
}
// Lê peso de um canal. Armazena valor medido no array lastAverage
// e retorna delta do valor medido do lastAverage anterior.
// canal 0 = A, canal 1 = B
float readChannel(int channel, bool printDetails) {
// Ganho: Canal A suporta 128 ou 64. Padrão 128. Canal B suporta 32.
// Seleciona Canal B usando ganho 32.
scales.set_gain(gain[channel]);
// Biblioteca HX711 só tem um conjunto de offset/fatores de escala
// que não funciona para nós pois usamos ambos canais com ganhos diferentes.
scales.set_offset(offset[channel]);
scales.set_scale(scaleFactor[channel]);
// força leitura para trocar ganho
scales.read();
scales.read();
float singleRead = scales.get_units();
float average = scales.get_units(10);
float delta = average - lastAverage[channel];
if (printDetails) {
Serial.print(singleRead, 1);
Serial.print("\t");
Serial.print(average, 1);
Serial.print("\t");
Serial.print(delta, 1);
Serial.print("\t");
}
lastAverage[channel] = average;
return delta;
}
// Imprime cabeçalho para dados de debug durante medição.
void printHeader() {
Serial.print("BME280\t\t\t\t\t");
Serial.print("Canal A\t\t\t");
Serial.print("Canal B\t\t\t");
Serial.print("\t\t");
Serial.print("Totais \t\t\t");
Serial.println("");
Serial.print("Temp\t");
Serial.print("Pressao\t");
Serial.print("Umidade\t");
Serial.print("leitura\t");
Serial.print("media\t");
Serial.print("delta\t");
Serial.print("\t");
Serial.print("leitura\t");
Serial.print("media\t");
Serial.print("delta\t");
Serial.print("\t");
Serial.print("soma\t");
Serial.print("\t");
Serial.print("peso\t");
Serial.print("itens\t");
Serial.print("mudanca\t");
Serial.print("itens add");
Serial.println("");
}
// Calibração - lê/imprime valores do canal selecionado.
void calibrate() {
digitalWrite(LED_BUILTIN, HIGH);
scales.set_gain(gain[calibrate_channel]);
scales.set_offset(offset[calibrate_channel]);
scales.set_scale(scaleFactor[calibrate_channel]);
// força leitura para trocar ganho
Serial.print("\t|CH:\t");
Serial.print(calibrate_channel, 1);
Serial.print("\traw:\t");
Serial.print(scales.read(), 1);
Serial.print("\t| raw:\t");
Serial.print(scales.read(), 1);
Serial.print("\t| unidades:\t");
Serial.print(scales.get_units(), 1);
Serial.print("\t| ganho:\t");
Serial.print(gain[calibrate_channel], 1);
Serial.print("\t| fator:\t");
Serial.println(scaleFactor[calibrate_channel], 1);
digitalWrite(LED_BUILTIN, LOW);
}
// Verifica porta serial para entrada do console para alterar modo etc.
void checkSerial() {
if (Serial.available()) {
char instruction = Serial.read();
switch (instruction) {
case '0': calibrate_channel = 0; Serial.println("Canal 0 (A) Selecionado"); break;
case '1': calibrate_channel = 1; Serial.println("Canal 1 (B) Selecionado"); break;
case 'm': mode = 0; Serial.println("Modo Medição"); printHeader(); break;
case 'c': mode = 1; Serial.println("Modo Calibração"); break;
case 't': // Tara. Reset escala para 0
Serial.println("Tara");
scales.power_up();
delay(500);
scales.tare(5); // Precisa fazer para cada canal e atualizar offset armazenado.
Serial.println("Não Tara propriamente!");
break;
case 'h': printHeader(); break;
case 'z': zeroScales(); break;
case 's': printSigfoxModelDetails(); break;
default:
Serial.println("Instrução desconhecida. Selecione: 0, 1, m, c, t, h, z, ou s");
Serial.println("m - modo medição");
Serial.println("c - modo calibração");
Serial.println("0 - Canal 0 (A) Calibração");
Serial.println("1 - Canal 1 (B) Calibração");
Serial.println("t - Tara (escala)");
Serial.println("z - Zero (Peso)");
Serial.println("h - imprimir Cabeçalho");
Serial.println("s - imprimir detalhes modelo Sigfox");
break;
}
}
}
// Mede (e registra) níveis de temperatura e umidade
// Define alarmes se fora de faixa (não podemos usar limites no lado serviço
// pois mensagens podem ser enviadas poucas vezes ao dia e uma condição
// fora de faixa breve (talvez horas) poderia facilmente ser perdida entre publicações.
void measureTemperatureAndHumidity() {
if (!bmeOk) return;
currentTemperature = bme.readTemperature();
if (currentTemperature < minTemperature) temperatureAlarm = true;
if (currentTemperature > maxTemperature) temperatureAlarm = true;
currentHumidity = bme.readHumidity();
if (currentHumidity < minHumidity) humidityAlarm = true;
if (currentHumidity > maxHumidity) humidityAlarm = true;
}
// Imprime valores lidos do sensor BME280
void printBmeValues() {
Serial.print(bme.readTemperature(), 1);
Serial.print("\t");
Serial.print(bme.readPressure() / 100.0F, 0);
Serial.print("\t\t");
Serial.print(bme.readHumidity(), 1);
Serial.print("\t\t");
}
// =============================================================
// Manipulação Sigfox
// =============================================================
// Determina se devemos publicar mensagem Sigfox.
bool shouldPublish() {
int messageIntervalMinutes = 15;
if (isFirstCycle) {
messageIntervalMinutes = 2;
Serial.println("Primeiro ciclo");
}
long millisAgo = millis() - lastPublish;
return millisAgo > (messageIntervalMinutes * 60 * 1000);
}
// Publica nossas medições (peso, temperatura, umidade etc) no Sigfox.
void publishMeasurements() {
Serial.println("Enviando via Sigfox...");
bool useDownlink = shouldUseDownlink();
if (useDownlink) Serial.println("Usando Sigfox downlink...");
SigfoxMessage msg = buildMessage();
SigFox.begin();
SigFox.debug();
delay(100);
SigFox.status();
delay(1);
SigFox.beginPacket();
SigFox.write((uint8_t*)&msg, 12);
uint8_t statusCode = SigFox.endPacket(useDownlink);
printSigfoxStatus(statusCode);
if (statusCode == 0) resetAlarms();
lastPublish = millis();
isFirstCycle = false;
if (useDownlink) {
parseDownlinkData(statusCode);
lastDownlink = lastPublish;
}
lastSigfoxStatus = statusCode;
SigFox.end();
}
void printSigfoxStatus(uint8_t statusCode) {
Serial.print("Código status resposta: 0x");
Serial.println(statusCode, HEX);
if (statusCode != 0) {
Serial.print("Status Sigfox:");
Serial1.println(SigFox.status(SIGFOX));
Serial1.println();
Serial.print("Status Atmel:");
Serial1.println(SigFox.status(ATMEL));
Serial1.println();
}
}
// Cria mensagem para publicar no Sigfox.
SigfoxMessage buildMessage() {
SigfoxMessage message;
message.status = getStatusFlags();
message.humidity = (int8_t)currentHumidity;
message.temperature = (int8_t)currentTemperature;
message.zeroWeight = (int16_t)zeroWeight;
message.weight = (int16_t)currentWeight;
message.itemCount = (int16_t)(stockLevel * 100);
message.driftCorrection = 0; // TODO
message.filler = 0;
message.lastStatus = lastSigfoxStatus;
return message;
}
// Obtém flags de status para mensagem Sigfox.
byte getStatusFlags() {
byte status = 0;
if (isFirstCycle) status |= 0x80;
if (!hx711Ok) status |= 0x40;
if (!bmeOk) status |= 0x20;
if (temperatureAlarm) status |= 0x10;
if (humidityAlarm) status |= 0x08;
if (weightAlarm) status |= 0x04;
if (lowStock) status |= 0x02;
return status;
}
// Determina se solicitamos mensagem downlink.
bool shouldUseDownlink() {
if (isFirstCycle) return true;
long millisAgo = millis() - lastDownlink;
return millisAgo > (12 * 60 * 60 * 1000);
}
// Parseia dados downlink.
void parseDownlinkData(uint8_t statusMessage) {
if (statusMessage > 0) {
Serial.println("Sem transmissão. Status: " + String(statusMessage));
return;
}
uint8_t response[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// Espera...
// Byte 0: Flags
// B7: Zero balanças
// B6: Definir faixa temperatura (ignorar min/max se 0)
// B5: Definir faixa umidade (ignorar min/max se 0)
// B4: Definir tolerância?
// B3: Definir ???
// B2: Atualizar peso unitário (ignorar se 0)
// B1:
// B0:
// Byte 1: Min T, Byte 2: Max T, Byte 3: Min Umidade, Byte 4: Max Umidade
// Byte 5: Tolerância de leitura??? (+/- x)
// Byte 6 e 7: Peso unitário (Little Endian)
if (SigFox.parsePacket()) {
Serial.println("Resposta do servidor:");
int i = 0;
while (SigFox.available()) {
Serial.print("0x");
int readValue = SigFox.read();
Serial.println(readValue, HEX);
response[i] = (uint8_t)readValue;
i++;
}
if (response[0] & 0x80 == 0x80) zeroScales();
if (response[0] & 0x40 == 0x40) updateTemperatureAlarm(response[1], response[2]);
if (response[0] & 0x20 == 0x20) updateHumidityAlarm(response[3], response[4]);
if (response[0] & 0x04 == 0x04) {
uint16_t weight = response[7] << 8 & response[6];
updateUnitWeight(weight);
}
} else {
Serial.println("Sem resposta do servidor");
}
Serial.println();
}
void printSigfoxModelDetails() {
if (!SigFox.begin()) {
Serial.println("Erro no shield ou não presente!");
return;
}
String version = SigFox.SigVersion();
String ID = SigFox.ID();
String PAC = SigFox.PAC();
Serial.println("Configuração Sigfox MKRFox1200");
Serial.println("Versão FW SigFox " + version);
Serial.println("ID = " + ID);
Serial.println("PAC = " + PAC);
Serial.println("");
Serial.print("Temperatura do módulo: ");
Serial.println(SigFox.internalTemperature());
Serial.println("Registre sua placa em https://backend.sigfox.com/activate com ID e PAC fornecidos");
delay(100);
SigFox.end();
}
// =============================================================
// Métodos auxiliares gerais
// =============================================================
void resetAlarms() {
temperatureAlarm = false;
humidityAlarm = false;
weightAlarm = false;
}
void zeroScales() {
zeroWeight = lastAverage[0] + lastAverage[1];
Serial.print("Zerado: ");
Serial.print(zeroWeight, 1);
Serial.println();
}
void updateTemperatureAlarm(int8_t lower, int8_t upper) {
Serial.print("Definindo alarme temperatura. Min: ");
Serial.print(lower);
Serial.print(", Max: ");
Serial.println(upper);
minTemperature = lower;
maxTemperature = upper;
}
void updateHumidityAlarm(int8_t lower, int8_t upper) {
Serial.print("Definindo alarme umidade. Min: ");
Serial.print(lower);
Serial.print(", Max: ");
Serial.println(upper);
minHumidity = lower;
maxHumidity = upper;
}
void updateUnitWeight(uint16_t weight) {
Serial.print("Definindo peso unitário: ");
Serial.println(weight);
unitWeight = weight;
}
Repositório GitHub Really Smart Box: https://github.com/Tinamous/ReallySmartBox
Peças Personalizadas e Gabinetes
Use isto para cortar as chapas superior e inferior de acrílico.
cuttingguide_e7GNHf980M.svg
Fica entre a chapa inferior de acrílico e a célula de carga para elevá-la um pouco e fornecer uma borda à plataforma. Imprima 4 destes para cada canto da chapa inferior se necessário.
Esquemáticos
Processo de manufatura
- A Revolução da Caixa de Suco: História, Design e Impacto Ambiental
- Gravadores de Voo (Caixas Pretas): Dos Primeiros Protótipos aos Designs Integrados Futuros
- Células Solares: Dos Primeiros Experimentos à Fotovoltaica Moderna – Tecnologia, Produção e Perspectivas Futuras
- Bartender Inteligente: Construa sua Própria Estação de Coquetéis com Raspberry Pi
- Semáforo Inteligente: Iluminação Pública Adaptativa com IoT
- Lixeira Inteligente: Otimizando Coleta de Resíduos com Sensores IoT
- Robô DIY Inspirado no Wall‑E com Caixa de CD e Raspberry Pi
- Kit Esterilizador UVC DIY – Caixa UV de Baixo Custo e Fácil Montagem para Itens Médicos e Pessoais
- Planta Inteligente IoT: Estufa Conectada com Arduino e Azure
- Persianas Inteligentes: Controle Automático de Luz com Arduino e Energia Solar