Manufactura industrial
Internet industrial de las cosas | Materiales industriales | Mantenimiento y reparación de equipos | Programación industrial |
home  MfgRobots >> Manufactura industrial >  >> Manufacturing Technology >> Proceso de manufactura

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Componentes y materiales

Lámina de acrílico (ej. 3mm perspex) × 1
Arduino MKR FOX 1200 × 1
Amplificador de celda de carga SparkFun HX711 × 1
Célula de carga 5kg × 2
Adafruit BME280 × 1

Herramientas y máquinas necesarias

Cortadora láser (genérica)
Impresora 3D (genérica)

Aplicaciones y servicios en línea

Tinamous
Sigfox

Sobre el proyecto

La plataforma Really Smart Box convierte una Really Useful Box (tm) en una caja de almacenamiento inteligente, conectada a internet, para monitorización de stock. Basada en el Arduino MKR FOX 1200 con Sigfox, detecta el peso de lo que se almacena en la caja, junto con la temperatura y la humedad, y usa la radio Sigfox de bajo consumo para transmitir esta información.

Caso de uso – Almacenamiento de filamento para impresora 3D

Si tienes una impresora 3D, probablemente te importe cómo se almacena tu filamento. Esto es cierto no solo para el filamento, sino para muchas otras cosas que deben almacenarse dentro de rangos aceptables de temperatura y humedad (por ejemplo, el sellador de pintor puede volverse inutilizable si se expone a temperaturas bajo cero).

Como uno de los responsables de mantener las impresoras 3D en mi makerspace local, necesito asegurar que tengamos suficiente stock de filamento y que se mantenga seco.

Con la Really Smart Box puedo monitorizar el peso del filamento y saber si estamos bajos, además de monitorizar el nivel de humedad en la caja para saber cuándo hay que reemplazar la gel de sílice.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Caso de uso – Control de stock de consumibles

Una empresa de limpieza puede querer mantener un stock de ambientadores, jabón de manos u otros consumibles en el sitio del cliente. El cliente probablemente no permitirá acceso WiFi al contratista ni alimentar un dispositivo como este cuando no esté presente. Sin embargo, la empresa del contratista necesita saber cuándo enviar nuevo stock, añadiendo trabajo extra al personal de limpieza y papeleo que a nadie gusta.

La plataforma Really Smart Box simplemente se coloca dentro de una caja de almacenamiento. Al usar Sigfox, no necesita conectarse a la red del cliente y es de bajo consumo, funcionando con pilas. Como el contenido de la caja cambia muy infrecuentemente, el Arduino puede mantenerse en estado de bajo consumo la mayor parte del tiempo, alargando la vida de la batería.

La plataforma puede configurarse con el peso del tipo de artículo almacenado (ej. ambientadores) y así calcular cuántos hay en la caja. Esto se envía a la empresa del contratista para alertarles cuando necesiten más stock en el sitio del cliente.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Construcción de la plataforma

La plataforma es de construcción simple: el cuerpo principal está hecho de dos piezas de acrílico cortado por láser (usé 3mm, mejor más grueso para objetos pesados como filamento de impresora) con un par de celdas de carga entre ellas.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Hice manualmente los avellanados para los tornillos de la celda de carga para un mejor acabado; aún no he encontrado una cortadora láser que haga avellanados.

El acrílico puede cortarse a cualquier tamaño para adaptarse a la caja que prefieras, pero ten en cuenta la posición de las celdas de carga y la longitud de sus cables, que es bastante corta. La lámina superior es ligeramente más pequeña que la inferior para que no roce con los laterales de la caja.

La lámina inferior tiene un recorte para permitir montar la electrónica en el acrílico sin separadores y que las patas soldadas sobresalgan. Usé un macho M3 en las esquinas del recorte para atornillar directamente el PCB. También se añadieron patas impresas en 3D en las esquinas para asegurar que los tornillos de la celda de carga que no queden a ras no afecten el equilibrio.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

El peso se detecta usando dos celdas de carga de 5kg. Un set de 4 se usa normalmente en básculas de baño y podría ser mejor, pero no encontré una forma bonita de fijarlas al acrílico y dar el espaciado necesario para la electrónica.

Las celdas de carga necesitan algo de acolchado arriba y abajo para permitir una pequeña flexión. Los sensores de extensómetro reales (la parte blanca en la foto) son más gruesos que el bloque de montaje. Esto se logra debajo de la celda con dos placas finales "Really Smart Box" impresas en 3D que tienen un pequeño bloque para elevar la celda; encima de la celda hay bloques de acrílico cortado por láser como acolchado.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Las celdas de carga se conectan a un amplificador HX711. Este tiene dos canales (A y B) que se pueden seleccionar, perfecto para este uso.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Cada celda de carga está construida con extensómetros en configuración de puente de Wheatstone, creando un par de divisores de tensión desequilibrados. Cuando la celda se somete a carga, la resistencia de los extensómetros cambia y se crea una diferencia entre los dos divisores, que es amplificada y medida por el HX711, que realiza la conversión analógico-digital.

He usado dos celdas de 5kg; existen otras capacidades (1kg, 10kg) que funcionan igual pero con distinta sensibilidad.

Al colocar las celdas, asegúrate de que la flecha en el extremo de la celda apunte hacia abajo (en la dirección de la carga). Las celdas tienen roscas M5 en un extremo (generalmente el fijo) y M4 en el otro (el lado donde se aplica la carga).

Los cables rojo/negro son alimentación, compartida entre las dos celdas. Verde y blanco son los cables de sentido desde el centro de los divisores, conectados a los canales A y B del HX711.

El HX711 soporta 3 factores de ganancia, que también se usan para selección de canal. Ganancias de 128 y 64 en canal A; ganancia de 32 selecciona canal B. Esto significa que nuestro segundo canal será menos sensible, lo cual está bien para esta aplicación.

El HX711 puede conectarse a cualquier pin digital del Arduino; usé D0 (Data) y D1 (Clock), y el amplificador solo necesita conectarse a la alimentación 3.3V del Arduino.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Puedes leer más sobre celdas de carga y el HX711 en el excelente tutorial de SparkFun.

Finalmente, un BME280 se conecta al bus I2C para detectar temperatura y humedad dentro de la caja; también puede medir presión, pero probablemente sea de poco interés y solo tenemos 12 bytes de datos Sigfox, así que no se reporta.

La electrónica va montada en una placa de prototipado ThingySticks Arduino, añadí un portapilas (pegado con cola caliente a la lámina inferior) y conecté la antena, de diseño plano, que funcionó perfectamente para la plataforma.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Calibración de celdas de carga

Antes de usar la plataforma, las celdas de carga necesitan calibración. Cada celda es única; se fabrica pegando extensómetros a un bloque de metal y taladrando agujeros para proporcionar la flexión necesaria sin romperse. Por eso necesitamos calibrar cada celda para su respuesta al peso.

Una vez calibrada, aplicamos la ecuación y = mx + c al valor ADC medido (x) para obtener el peso real (y). Necesitamos encontrar c (el offset) y m (la pendiente) para nuestra celda.

Quité la tapa superior de la plataforma y coloqué un cuadrado pequeño de acrílico en cada celda por turnos, monitorizando los valores medidos (hay una rutina en el firmware para hacer esto, que se inicia enviando una "c" al puerto serial).

Primero se midió la lectura con la plataforma vacía, lo que da el offset (c). Luego se colocó una carga de peso conocido en la celda; la diferencia en lecturas nos da la pendiente.

Pendiente = (valor_medido - offset) / peso (g).

Usé tanto una lata pequeña de ambientador (~230g) como una bobina de filamento (~1.5kg) para verificar los valores; ambas dieron aproximadamente la misma pendiente, lo que fue tranquilizador.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Naturalmente, el offset medido con el pequeño cojín de acrílico difiere del que se experimenta con la tapa completa; también hay una pequeña diferencia de pendiente al usar ambas celdas, por lo que se necesita una calibración secundaria. Por ahora se usa un offset de cero en un punto (tara), que se establece en el firmware pero también puede configurarse vía conexión serial USB o mediante un mensaje downlink Sigfox una vez desplegado.

Conexión Sigfox

Con la Really Smart Box cableada, usé inicialmente el puerto serial USB para monitorizar la salida y depurar/ajustar el sistema. Así se pueden ver las celdas de carga individuales, cambios y ruido. Pero esto no funcionará para una caja desplegada, que debe ser totalmente inalámbrica.

Con Sigfox podemos enviar 12 bytes de datos hasta 140 veces al día a nuestro servicio en línea, más que suficiente para la Really Smart Box. La siguiente estructura de datos se usa en el Arduino para describir cómo usamos los 12 bytes.

typedef struct __attribute__ ((packed)) sigfox_message {
  uint8_t status;        // flags de estado
  int8_t humidity;       // humedad::int:8 (algunos sensores leen humedad negativa)
  int8_t temperature;    // temperatura::int:8 (sin decimales)
  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 cuenta real para permitir 2.01)
  int8_t driftCorrection;// Corrección deriva para cambios en peso cero
  int8_t filler;         // Relleno
  int8_t lastStatus;     // Último estado Sigfox
} SigfoxMessage;

El primer byte (status) se divide en flags de bits:

// status::uint:8 -> Split a 8 bits
// B7 - Primera ejecución
// B6 - Fallo HX711
// B5 - Fallo BME280
// B4 - Alarma temperatura
// B3 - Alarma humedad
// B2 - Alarma peso
// B1 - Stock bajo
// B0 - reserva

Esta estructura se compacta a 12 bytes, pero hay que desempaquetarla en el lado de Sigfox para enviarla a Tinamous. Usamos una Configuración de Payload Personalizada para eso, y es mejor definirla junto con la estructura de datos. La nuestra es:

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

El payload personalizado divide nuestros 12 bytes según se parsean.

Nota que debemos especificar la naturaleza little-endian de cualquier cosa mayor a 1 byte, ya que Sigfox usa big-endian por defecto y Arduino usa little-endian (el byte menos significativo va primero en palabras multi-byte).

También notar que separar los flags booleanos en el primer byte no avanza el marcador de byte como hace con las demás lecturas, por lo que el byte de estado que contiene todos los flags también se lee para saltar el primer byte.

Incluidos en los flags están alarmas de rango de temperatura, humedad y peso. Podríamos usar un servicio en línea (ej. Tinamous) para monitorizar valores fuera de rango, pero estos pueden ser breves (unas horas) y nuestra caja puede enviar datos infrecuentemente (una o dos veces al día), por lo que una condición ambiental dañina podría pasarse por alto fácilmente. Por eso se marcan en el dispositivo y se envían (y se reinician tras un envío exitoso).

El contador de artículos (itemCount) se establece a 100 veces la cuenta real. Quería permitir valores como 2.2 artículos (por error de peso u otros objetos en la caja) sin forzar redondeo; 2.95 podría redondearse a 2 si no tenemos cuidado, sugiriendo 3 artículos y un pequeño error. Tampoco quería usar un float que requeriría más espacio, así que usé una palabra de 16 bits con un factor para conversión fácil (también firmado para permitir error de cero, que podría dar nivel de stock -1 o -2).

Muy poco se necesita para habilitar la comunicación Sigfox. En el Arduino se añade la librería Sigfox y se llaman las funciones apropiadas para enviar los datos según los ejemplos de Arduino para la librería Sigfox, pero hay que registrar el dispositivo en Sigfox.

Enviando una "s" al puerto serial de la Really Smart Box se imprime el ID y código PAC de Sigfox, que se usan para activar el dispositivo en el backend de Sigfox. Luego vamos al servicio de activación de Sigfox backend y seguimos el asistente: primero seleccionamos nuestro dispositivo, luego país/proveedor y luego algunos detalles.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Y finalmente nuestro dispositivo está activado y listado:

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Sigfox asigna dispositivos a agrupaciones de Tipo de Dispositivo, lo cual es sensato ya que normalmente se tienen muchos (cientos, miles) del mismo tipo que se quieren tratar como grupo. Con el Tipo de Dispositivo definido, podemos configurar un callback personalizado para enviar los datos recibidos a nuestro servicio en línea. Uso Tinamous para esto (pista: mira mi nombre de perfil – puedo ser parcial en mi elección).

Usando la Really Smart Box

Una vez cableada, atornillada, firmware flasheado y pilas puestas, la plataforma simplemente se deja caer dentro de una Really Useful Box (tm) y está lista.

La alimentación debe aplicarse lo más tarde posible, ya que una vez encendido el dispositivo enviará el primer mensaje Sigfox tras 2 minutos y solicitará datos downlink. Estos datos pueden incluir un comando "Zero" para poner a cero el peso de la plataforma. Si no, se necesitará una conexión serial USB o esperar a la siguiente solicitud downlink – se hacen cada 12 horas.

Una vez en marcha, la plataforma publicará mensajes Sigfox cada 15 minutos para enviar peso, cuenta de artículos, temperatura, humedad y estados de alarma. Temperatura, humedad y peso se miden cada minuto para asegurar que no están fuera de rango, marcando alarmas para la siguiente transmisión si se han activado.

Monitorización con Tinamous

Tinamous soporta callbacks personalizados de Sigfox añadiendo un "Sigfox Bot" a nuestra cuenta; para instrucciones ver mi tutorial "Get Your Sigfox On" en Hackster.io.

Al añadir un Sigfox Bot a tu cuenta de Tinamous, si incluyes la configuración API, el bot buscará tus dispositivos y los añadirá a tu cuenta, aunque no es necesario ya que el dispositivo se añadirá automáticamente cuando publique datos.

Cuando hayas añadido el Bot, se presenta una pantalla de configuración de callback para ayudar a configurar los callbacks de Sigfox.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Luego crea un callback personalizado en Sigfox; ten en cuenta que la Really Smart Box usa callback DATA → BIDIR que maneja el callback UPLINK normal y los callbacks BIDIR (subida y bajada).

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Aquí es donde el payload personalizado de antes resulta útil; pégalo desde el código fuente en payload personalizado y actualiza la sección de campos para reflejar esto.

Lat y Lng se especifican en este callback para dar una ubicación aproximada; sin embargo, Sigfox en Arduino soporta ubicación mejorada, pero necesita un segundo callback. Si usas la geolocalización no especifiques Lat/Lng en este mensaje o la Really Smart Box parecerá moverse entre ubicaciones.

Una vez configurado, también hay que habilitarlo para Downlink, que está deshabilitado por defecto aunque se haya puesto BIDIR.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Con un callback Downlink también queremos especificar un callback SERVICE → ACKNOWLEDGE para saber que nuestro dispositivo recibió los datos downlink. Al hacer clic en el Sigfox Bot en Tinamous se muestran otras configuraciones de callback; sigue las instrucciones para ACKNOWLEDGE y GEOLOC.

Nota que necesitas copiar la cabecera de autorización del primer callback uplink/bidir ya que es una contraseña que se cifra de forma unidireccional en Tinamous y ya no está disponible para mostrar.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Con nuestros callbacks en su lugar, los datos publicados por nuestro dispositivo deberían enviarse ahora a Tinamous. También podríamos añadir callbacks de email en Sigfox que ayuden a confirmar que los datos llegan (pero pueden volverse muy ruidosos muy rápido).

Configuración del dispositivo en Tinamous

Una vez que el dispositivo Sigfox ha sido visto en Tinamous (ya sea por búsqueda API o un callback), aparecerá en la página de Dispositivos, desde donde podemos editar sus propiedades. Los campos se añaden automáticamente según vienen del callback Sigfox, así que es mejor esperar a que el dispositivo publique datos.

Establecí el tiempo "Not Reporting After" a 1 hora (publica cada 15 minutos) para saber si el dispositivo se ha roto y opcionalmente recibir notificación.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

No quería ver todos los campos enviados por el dispositivo en la página de gráficos/detalles (hay muchos si incluyes todos los flags), así que Tinamous se configura para mostrar solo peso y cuenta de artículos. Etiquetas y unidades amigables también se aplicaron aquí.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

El campo Item Count es 100x la cuenta real, así que se aplica una calibración a ese campo para reducirlo 100 veces.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Se establecen algunos datos downlink que harán que la Really Smart Box ponga a cero y aplique límites de rango de temperatura y humedad cuando solicite el próximo mensaje downlink (2 minutos tras encender, luego cada 12 horas).

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Visualizando la información de la Really Smart Box

Ahora que los campos del dispositivo están configurados, podemos monitorizarlos a través de la página de detalles del dispositivo (Nota: no había puesto a cero la plataforma en este momento, por eso cree que hay 1/2 unidad presente – también reemplacé la tapa de acrílico por una de 5mm, más pesada pero mejor para filamento de impresora).

Really Smart Box: Solución de Almacenamiento Inteligente IoT

También podemos ver las interacciones de callback Sigfox desde la sección Sigfox. Observa aquí que los datos downlink se envían y se ack, pero el Arduino reporta un error. Más sobre eso al final.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

En la pestaña Ubicación también podemos ver dónde está nuestra Really Smart Box, útil si olvidas en qué cliente está o si está en una furgoneta.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Y naturalmente queremos una vista de panel bonita de nuestra Really Smart Box; la de abajo muestra el peso del contenido, las unidades estimadas y un contador de dispositivos que no reportan para saber si uno falla.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Recibiendo notificaciones con Tinamous

Configuré Tinamous para enviar email y SMS cuando la cuenta de artículos es baja. Lo hice especificando un rango de trabajo de 3–300 para el campo item count. Si el valor está fuera de este rango se genera un evento "measurement out of range".

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Añadiendo una notificación en Tinamous podemos ser notificados cuando eso ocurra.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Podríamos especificar solo el campo que nos interesa, pero dejándolo en blanco recibimos notificaciones por cualquier campo fuera de rango.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Igual para dispositivos, déjalo en blanco para todos (ej. el único que tenemos ahora).

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Establece Repeat Notifications para que se active una sola vez hasta que se reinicie (cada día), ¡si no las notificaciones cada 15 minutos se vuelven molestas muy rápido!

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Luego selecciona cómo ser notificado; lo puse para email y SMS y creo la notificación:

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Conclusión

Ahora puedo desplegar la Really Smart Box y (esperemos) olvidarme de ella. Cuando el nivel de stock baje, seré notificado y podré ver en el panel cómo va. Usando Sigfox no me preocupo de alimentar el dispositivo salvo un cambio ocasional de pilas y no se necesita configuración WiFi en el sitio, haciendo el despliegue extremadamente simple.

Planeo desplegar esta unidad en una de nuestras cajas de almacenamiento de filamento en Cambridge Makespace para monitorizar niveles de stock.

Really Smart Box: Solución de Almacenamiento Inteligente IoT
Really Smart Box: Solución de Almacenamiento Inteligente IoT

Problemas por resolver

Como es lógico, esto no es un proyecto terminado de calidad de producción; quedan algunos problemas:

Sigfox Downlink: Sigfox permite 4 mensajes al día en respuesta a un mensaje uplink. La Really Smart Box los usa para permitir re-poner a cero la báscula, establecer rangos superiores/inferiores de temperatura/humedad y el peso del artículo. Sin embargo, al intentar hacer que funcione, aunque el mensaje downlink parece enviarse y ser ack (como se ve en el backend Sigfox), el Arduino reporta un error de estado 62, que no mapea a ninguna condición de error listada para el chip ATA8520. Investigando los drivers, el comando usa una solicitud de downlink que tampoco está en la hoja de datos, así que se necesita más investigación.

Solo depuración: Ejecutar las comunicaciones Sigfox con depuración desactivada activa el modo de bajo consumo del Arduino, que mata el puerto serial USB.

Modo bajo consumo: Como se describe para la depuración Sigfox, usar la librería Low Power de Arduino hace que el puerto serial USB se caiga, así que no está activado por ahora.

Deriva: No se ha añadido compensación por deriva; sin duda las celdas de carga derivarán bajo carga constante.

Ruido / Mediciones no verticales: La Really Smart Box puede estar en la parte trasera de una furgoneta (ej. limpiador móvil, carpintero, fontanero). Sería bueno añadir un acelerómetro a la plataforma y saltar ciclos de medición cuando la caja no está estable; igualmente, si la caja no está vertical, el peso no pasará por las celdas de carga como se espera.

Código

Código Arduino Really Smart Box Arduino

Añade librerías para Arduino MKR FOX 1200, HX711, AdaFruit BME280, Arduino Low Power. Usa el Arduino IDE para programar normalmente.

// Really Smart Box
// Mide el peso del contenido de una really smart box
// Hecha con dos láminas de acrílico con 2 celdas de carga entre ellas
// colocadas en una really smart box.
// También incluye un BME280 para medir temperatura y presión dentro de la caja.
// Autor: Stephen Harrison
// Licencia: MIT

#include <Adafruit_BME280.h>
#include <HX711.h>
#include <SigFox.h>
#include <SigFox.h>
#include <ArduinoLowPower.h>

// --------------------------------------
// BME280 en el puerto I2C.
Adafruit_BME280 bme;

// --------------------------------------
// HX711 amplificador de celda de carga.
// 0: D0 - DOUT
// 1: D1 - CLK
// ganancia inicial 128.
HX711 scales(0, 1, 128);

// Arrays para celdas de carga. Índice 0 == Canal A, Índice 1 == Canal B.
float gain[] = {128, 32};

// Factores de calibración.
// Usamos y = mx + c (c = offset, m = scaleFactor).
// para convertir el valor medido en peso.
// Establece esto al offset reportado por las celdas de carga sin peso.
float offset[] = {0, 54940};

// Establece esto al factor calculado cuando se pone un peso en la báscula.
// Primero pon el offset, reflasha el Arduino para que surta efecto,
// pon un peso en la báscula y divide el valor medido en bruto por el peso.
// usando scaleFactor = valor medido / peso.
float scaleFactor[] = {378.f, 260.9f};

// --------------------------------------
// Sigfox
// Esta es la estructura de datos que publicamos en Sigfox.
// Separamos los bits como flags bool del primer byte de estado pero el byte
// sigue necesitando incluirse sino humidity se convierte en 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 -> Split a 8 bits
                         // B7 - Primera ejecución, B6 - Fallo HX711, B5 - Fallo BME280,
                         // B4 - Alarma temperatura, B3 - Alarma humedad, B2 - Alarma peso,
                         // B1 - Stock bajo, B0 - reserva
  int8_t humidity;       // humidity::int:8 (sí, algunos sensores (HTU21D leen humedad negativa)
  int8_t temperature;    // temperature::int:8 (sin decimales).
  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 cuenta real)
  int8_t driftCorrection;// Corrección deriva para cambios en peso cero aplicados a la báscula.
  int8_t filler;         // Nada que ver aquí, sigue adelante....
  int8_t lastStatus;     // Último estado sigfox
} SigfoxMessage;

// Tiempo del último mensaje Sigfox publicado
long lastPublish = 0;
// Tiempo del último downlink Sigfox solicitado.
// Máximo 4 permitidos por día.
long lastDownlink = 0;
uint8_t lastSigfoxStatus = 0;

// --------------------------------------
// Aplicación/estado
// Si es el primer ciclo (tras reset) para el ciclo medir/publicar
// (esto se usa para solicitar un mensaje downlink de Sigfox).
// Nota que solo 4 permitidos por día, ten cuidado al desplegar código.
bool isFirstCycle = true;

// Modo aplicación
// 0: Normal
// 1: Calibración
int mode = 0;

// Qué canal leer durante calibración.
int calibrate_channel = 0;

// Último valor promedio medido para cada canal.
float lastAverage[] = {0, 0};

// Peso actual del contenido de la caja
float currentWeight = 0;

// Peso de las unidades que la caja contendrá.
// Actualizable vía mensaje downlink Sigfox.
float unitWeight = 238;

// Diferente a tara, sería un cero manual en una lectura fija de la báscula.
// Probablemente cambie con deriva (tiempo/temperatura/etc) y debe establecerse
// una vez que la báscula está en su sitio pero sin cargar.
// Actualizable vía mensaje downlink Sigfox.
float zeroWeight = 0;

bool bmeOk = true;
bool hx711Ok = true;

// Alarmas y rangos de alarma
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 el Arduino.
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
  // NB: El sensor que uso (de vendedor aleatorio eBay)
  // no usa la dirección por defecto.
  bmeOk = bme.begin(0x76);
  if (!bmeOk) {
    Serial.println("Could not find a valid BME280 sensor!");
  }
  // Retardo para conexión USB Serial y primera lectura del BME.
  delay(5000);
  Serial.println("Really Smart Box...");
  printHeader();
}

int delayCounter = 0;

void loop() {
  switch (mode) {
    case 0:
      measureAndPublish();
      // Sleep 1 minuto
      // Causa problemas con USB conectado.
      // LowPower.sleep(1 * 60 * 1000);
      delay(60 * 1000);
      break;
    case 1:
      calibrate();
      delay(1000);
      break;
  }
  // Comprueba entrada de usuario por puerto serial para alterar modo, etc.
  checkSerial();
  // La medición se hace en tick de RTC (una vez por minuto)
  delay(100);
}

void measureAndPublish() {
  digitalWrite(LED_BUILTIN, HIGH); // LED encendido = midiendo
  printBmeValues();
  measureTemperatureAndHumidity();
  measureWeight(true);
  // Peso, temperatura y humedad se leen cada minuto
  // pero solo publicamos ocasionalmente.
  if (shouldPublish()) {
    publishMeasurements();
  }
  digitalWrite(LED_BUILTIN, LOW);
}

// Bucle principal de medición. Lee peso de celdas de carga
// y almacena si no hay ruido de la lectura anterior.
void measureWeight(bool printDetails) {
  scales.power_up();
  delay(500);
  float delta = readDelta(printDetails);
  if (printDetails) {
    Serial.print("\t");
    Serial.print(delta, 2);
  }
  // Si delta entre -1 y 1 (sin ruido), actualiza cambio de peso total y unidades
  // sino ignora e intenta más tarde.
  // Esto asegura que usamos solo lecturas estables cuando ambos canales no han cambiado
  // en dos series de mediciones.
  if (delta < 1.f && delta > -1.f) {
    float lastWeight = currentWeight;
    // Calcula peso. Suma peso de ambas celdas y resta peso cero.
    currentWeight = lastAverage[0] + lastAverage[1] - zeroWeight;
    // Calcula diferencia de peso de artículos en la caja comparado con última lectura estable.
    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();
  }
  scales.power_down();
}

void updateStockLevels() {
  stockLevel = currentWeight / unitWeight;
  // A diferencia de otras alarmas, el nivel bajo de stock
  // se reinicia si se repone stock.
  lowStock = stockLevel < minStock;
}

void checkWeightLimits() {
  if (currentWeight > maxWeight) weightAlarm = true;
  if (lastAverage[0] > (maxWeight / 2)) weightAlarm = true;
  if (lastAverage[1] > (maxWeight / 2)) weightAlarm = true;
}

float readDelta(bool printDetails) {
  float aDelta = readChannel(0, true);
  if (printDetails) Serial.print("\t");
  float bDelta = readChannel(1, true);
  return aDelta + bDelta;
}

float readChannel(int channel, bool printDetails) {
  scales.set_gain(gain[channel]);
  scales.set_offset(offset[channel]);
  scales.set_scale(scaleFactor[channel]);
  scales.read(); scales.read(); // fuerza lectura para cambiar ganancia
  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;
}

void printHeader() {
  Serial.print("BME280\t\t\t\t\t");
  Serial.print("Channel A\t\t\t");
  Serial.print("Channel B\t\t\t");
  Serial.print("\t\t");
  Serial.print("Totals \t\t\t");
  Serial.println("");
  Serial.print("Temp\t");
  Serial.print("Pressure\t");
  Serial.print("Humidity\t");
  Serial.print("read\t");
  Serial.print("average\t");
  Serial.print("delta\t");
  Serial.print("\t");
  Serial.print("read\t");
  Serial.print("average\t");
  Serial.print("delta\t");
  Serial.print("\t");
  Serial.print("sum\t");
  Serial.print("\t");
  Serial.print("weight\t");
  Serial.print("items\t");
  Serial.print("change\t");
  Serial.print("items added");
  Serial.println("");
}

void calibrate() {
  digitalWrite(LED_BUILTIN, HIGH);
  scales.set_gain(gain[calibrate_channel]);
  scales.set_offset(offset[calibrate_channel]);
  scales.set_scale(scaleFactor[calibrate_channel]);
  scales.read(); // fuerza lectura para cambiar ganancia
  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| units:\t");
  Serial.print(scales.get_units(), 1);
  Serial.print("\t| gain:\t");
  Serial.print(gain[calibrate_channel], 1);
  Serial.print("\t| factor:\t");
  Serial.println(scaleFactor[calibrate_channel], 1);
  digitalWrite(LED_BUILTIN, LOW);
}

void checkSerial() {
  if (Serial.available()) {
    char instruction = Serial.read();
    switch (instruction) {
      case '0': calibrate_channel = 0; Serial.println("Channel 0 (A) Selected"); break;
      case '1': calibrate_channel = 1; Serial.println("Channel 1 (B) Selected"); break;
      case 'm': mode = 0; Serial.println("Measurement Mode"); printHeader(); break;
      case 'c': mode = 1; Serial.println("Calibration Mode"); break;
      case 't': // Tara. Reinicia la báscula a 0
        Serial.println("Taring");
        scales.power_up(); delay(500);
        scales.tare(5); // Hacer para cada canal y actualizar offset almacenado.
        Serial.println("Not properly Tared!");
        break;
      case 'h': printHeader(); break;
      case 'z': zeroScales(); break;
      case 's': printSigfoxModelDetails(); break;
      default:
        Serial.println("Unknown instruction. Select: 0, 1, m, c, t, h, z, or s");
        Serial.println("m - measurement mode");
        Serial.println("c - Calibration mode");
        Serial.println("0 - Channel 0 (A) Calibration");
        Serial.println("1 - Channel 1 (B) Calibration");
        Serial.println("t - Tare (scale)");
        Serial.println("z - Zero (Weight)");
        Serial.println("h - print Header");
        Serial.println("s - print Sigfox model details");
        break;
    }
  }
}

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;
}

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");
}

// =============================================================
// Manejo Sigfox
// =============================================================

bool shouldPublish() {
  int messageIntervalMinutes = 15;
  if (isFirstCycle) {
    messageIntervalMinutes = 2;
    Serial.println("First cycle");
  }
  long millisAgo = millis() - lastPublish;
  return millisAgo > (messageIntervalMinutes * 60 * 1000);
}

void publishMeasurements() {
  Serial.println("Sending via Sigfox...");
  bool useDownlink = shouldUseDownlink();
  if (useDownlink) Serial.println("Using 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("Response status code : 0x");
  Serial.println(statusCode, HEX);
  if (statusCode != 0) {
    Serial.print("Sigfox Status:");
    Serial1.println(SigFox.status(SIGFOX)); Serial1.println();
    Serial.print("Atmel Status:");
    Serial1.println(SigFox.status(ATMEL)); Serial1.println();
  }
}

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;
}

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;
}

bool shouldUseDownlink() {
  // return false; // descomentar para debug sin downlink
  if (isFirstCycle) return true;
  long millisAgo = millis() - lastDownlink;
  return millisAgo > (12 * 60 * 60 * 1000); // cada 12 horas
}

void parseDownlinkData(uint8_t statusMessage) {
  if (statusMessage > 0) {
    Serial.println("No transmission. Status: " + String(statusMessage));
    return;
  }
  uint8_t response[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  // Byte 0: Flags
  // B7: Zero scales
  // B6: Set Temperature range (ignore min/max temp if 0)
  // B5: Set Humidity range (ignore min/max humidity if 0)
  // B4: Set tolerance?
  // B3: Set ???
  // B2: Update unit weight (ignore update if 0)
  // B1: 
  // B0: 
  // Byte 1: Min T
  // Byte 2: Max T
  // Byte 3: Min Humidity
  // Byte 4: Max Humidity
  // Byte 5: Read tolerence?? (+/- x)
  // Byte 6 & 7: Unit weight (Little Endian)
  if (SigFox.parsePacket()) {
    Serial.println("Response from server:");
    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) zeroScales();
    if (response[0] & 0x40) updateTemperatureAlarm(response[1], response[2]);
    if (response[0] & 0x20) updateHumidityAlarm(response[3], response[4]);
    if (response[0] & 0x04) {
      uint16_t weight = (response[7] << 8) | response[6];
      updateUnitWeight(weight);
    }
  } else {
    Serial.println("No response from server");
  }
  Serial.println();
}

void printSigfoxModelDetails() {
  if (!SigFox.begin()) {
    Serial.println("Shield error or not present!");
    return;
  }
  String version = SigFox.SigVersion();
  String ID = SigFox.ID();
  String PAC = SigFox.PAC();
  Serial.println("MKRFox1200 Sigfox configuration");
  Serial.println("SigFox FW version " + version);
  Serial.println("ID = " + ID);
  Serial.println("PAC = " + PAC);
  Serial.println("");
  Serial.print("Module temperature: ");
  Serial.println(SigFox.internalTemperature());
  Serial.println("Register your board on https://backend.sigfox.com/activate with provided ID and PAC");
  delay(100);
  SigFox.end();
}

// =============================================================
// Métodos auxiliares generales
// =============================================================

void resetAlarms() {
  temperatureAlarm = false;
  humidityAlarm = false;
  weightAlarm = false;
}

void zeroScales() {
  zeroWeight = lastAverage[0] + lastAverage[1];
  Serial.print("Zero'd: ");
  Serial.print(zeroWeight, 1);
  Serial.println();
}

void updateTemperatureAlarm(int8_t lower, int8_t upper) {
  Serial.print("Setting temperature alarm. Min: ");
  Serial.print(lower);
  Serial.print(", Max: ");
  Serial.println(upper);
  minTemperature = lower;
  maxTemperature = upper;
}

void updateHumidityAlarm(int8_t lower, int8_t upper) {
  Serial.print("Setting humidity alarm. Min: ");
  Serial.print(lower);
  Serial.print(", Max: ");
  Serial.println(upper);
  minHumidity = lower;
  maxHumidity = upper;
}

void updateUnitWeight(uint16_t weight) {
  Serial.print("Setting unit weight: ");
  Serial.println(weight);
  unitWeight = weight;
}

Repositorio GitHub Really Smart Box

https://github.com/Tinamous/ReallySmartBox

Piezas personalizadas y cajas

Úsalo para cortar las láminas superior e inferior de acrílico.
cuttingguide_e7GNHf980M.svg
Esta pieza va entre la lámina inferior y la celda de carga para elevarla un poco y dar un borde a la plataforma.
Imprime 4 de estas para cada esquina de la lámina inferior si es necesario.

Esquemáticos

Nada demasiado complejo.

Really Smart Box: Solución de Almacenamiento Inteligente IoT

Proceso de manufactura

  1. La Revolución del Brick de Jugo: Historia, Diseño e Impacto Ambiental
  2. Cajas negras de vuelo: de los primeros prototipos a diseños integrados futuros
  3. Células solares: de los primeros experimentos a la fotovoltaica moderna – tecnología, producción y perspectivas
  4. Barman Inteligente: Construye tu Propia Estación de Cócteles con Raspberry Pi
  5. Semáforo Inteligente: Iluminación Vial Adaptativa con IoT
  6. Papelera Inteligente Avanzada: Optimización de Recogida de Residuos con Sensores IoT
  7. Robot DIY inspirado en Wall‑E con Raspberry Pi y caja de CD
  8. Kit esterilizador UVC DIY: caja UV de bajo costo y fácil construcción para artículos médicos y personales
  9. Planta Inteligente IoT: Construye un Invernadero Conectado con Arduino y Azure
  10. Persianas Inteligentes: Control Automático de Luz con Arduino y Energía Solar
  11. Moderniza tu Hogar: Interruptor Táctil Inteligente con Monitor de Temperatura en Arduino