Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Manufacturing Technology >> Производственный процесс

Really Smart Box: умная коробка для IoT-мониторинга запасов

Компоненты и материалы

Акриловый лист (например, 3 мм плексиглас) 1 шт.
Arduino MKR FOX 1200 1 шт.
Усилитель тензодатчика SparkFun HX711 1 шт.
Тензодатчик 5 кг 2 шт.
Датчик BME280 (Adafruit) 1 шт.

Инструменты и станки

Приложения и сервисы

О проекте

Платформа Really Smart Box превращает обычную коробку Really Useful Box в интеллектуальный интернет-хранилище для мониторинга запасов. На базе Arduino MKR FOX 1200 она измеряет вес содержимого, температуру и влажность, передавая данные через низкоэнергетическую сеть Sigfox.

Вариант использования — хранение филамента для 3D-принтера

Владельцы 3D-принтеров знают, как важно правильное хранение филамента. То же относится к краскам, клеям и другим материалам, чувствительным к температуре и влажности (например, герметик непригоден после заморозки).

Как ответственный за 3D-принтеры в локальном makerspace, мне нужно знать, достаточно ли филамента и сухо ли он (нужно ли менять силикагель).

Really Smart Box позволяет отслеживать вес катушки (закончится ли филамент) и влажность внутри коробки.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Вариант использования — контроль расходников у клиента

Клининговая компания может хранить расходники (освежители, мыло) на объекте клиента. Клиент не даст доступ к Wi-Fi или розетке, но компании нужно знать, когда привозить новые запасы — это лишние поездки и бумажная работа.

Really Smart Box просто кладётся в коробку: Sigfox не требует настройки Wi-Fi, низкое потребление позволяет работать от батарей месяцами. Arduino большую часть времени спит, просыпаясь редко для измерений.

Платформе можно задать вес одной единицы товара (например, баллона освежителя) — она сама вычислит количество и отправит уведомление при снижении запаса.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Изготовление платформы

Конструкция проста: две лазерно вырезанные акриловые пластины (3 мм, для тяжелых грузов лучше толще) с парой тензодатчиков между ними.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Отверстия под винты тензодатчиков зашлифованы вручную для посадки головок (лазер не умеет делать зафаски). Верхняя пластина чуть меньше нижней, чтобы не застревать в стенках коробки. В нижней пластине вырез для электроники без Abstand-ов, PCB прикручивается прямо в вырез (M3 резьба). 3D-напечатанные ножки в углах гарантируют, что недокрученные винты тензодатчиков не влияют на баланс.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Вес измеряют два тензодатчика на 5 кг. В напольных весах обычно ставят 4, но я не нашёл удобного способа зафиксировать четверку на акриле с нужным расстоянием для электроники.

Тензодатчики нуждаются в прокладках сверху и снизу для небольшого прогиба. Датчик деформации (белая часть) толще монтажного блока. Под тензодатчиком — 3D-напечатанные пластины с выступами, сверху — акриловые прокладки.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Тензодатчики подключены к усилителю HX711. У него два канала (A и B) с выбором усиления — идеально для двух датчиков.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Каждый тензодатчик — мост Уитстона из тензорезисторов. Под нагрузкой сопротивление меняется, создавая разность потенциалов, которую усиливает и оцифровывает HX711.

Используются два датчика на 5 кг (есть и 1 кг, и 10 кг — работают так же, меняется чувствительность).

Стрелка на конце тензодатчика должна смотреть вниз (по направлению нагрузки). М5 — закрепление, М4 — нагрузка. Красный/чёрный — питание (общее для двух датчиков), зелёный/белый — сигнальные (каналы A и B HX711).

У HX711 три коэффициента усиления, которые также выбирают канал: 128 и 64 — канал A, 32 — канал B. Значит, второй канал менее чувствителен, что допустимо для нашей задачи.

HX711 подключается к любым цифровым пинам Arduino (я использовал D0 — Data, D1 — Clock), питание — 3,3 В от Arduino.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Подробнее о тензодатчиках и HX711 — в туториале SparkFun.

BME280 подключён к I2C для измерения температуры и влажности внутри коробки (давление тоже умеет, но в 12 байтах Sigfox не помещается).

Электроника смонтирована на макетной плате ThingySticks, держатель батареи (клей-пистолет) и плоская антенна Sigfox — на нижней акриловой пластине.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Калибровка тензодатчиков

Каждый тензодатчик уникален — нужно откалибровать. Применяем уравнение y = mx + c (x — сырое значение АЦП, y — вес в граммах). Находим c (смещение) и m (наклон).

Снимаю верхнюю пластину, приклеиваю маленький акриловый квадратик к каждому тензодатчику по очереди. В прошивке есть режим калибровки (команда 'c' в Serial Monitor).

  1. Измеряю значение пустой платформы — это offset (c).
  2. Кладу известный вес, разница показаний даёт slope (m): Slope = (measured - offset) / weight (g).

Проверял на баллончике освежителя (~230 г) и катушке филамента (~1,5 кг) — наклон получился одинаковый, что успокаивает.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Естественно, offset с маленькой прокладкой отличается от полной верхней пластины, и наклон немного меняется при работе двух датчиков — нужна вторичная калибровка. Пока использую однократную тару (zero) при установке, задаваемую прошивкой, через USB или downlink Sigfox.

Подключение к Sigfox

Сначала отлаживал через USB Serial. Для деплоя нужна беспроводная связь. Sigfox позволяет отправлять 12 байт до 140 раз в сутки — достаточно.

Структура данных (12 байт):

typedef struct __attribute__ ((packed)) sigfox_message {
  uint8_t status;        // флаги состояния
  int8_t humidity;       // влажность (int8, некоторые датчики дают отрицательные)
  int8_t temperature;    // температура (int8, без дробной части)
  int16_t zeroWeight;    // нулевой вес (little-endian)
  int16_t weight;        // текущий вес (little-endian)
  int16_t itemCount;     // количество предметов ×100 (little-endian)
  int8_t driftCorrection; // коррекция дрейфа нуля
  int8_t filler;         // резерв
  int8_t lastStatus;     // последний статус Sigfox
} SigfoxMessage;

Первый байт (status) — битовые флаги:

// B7: первый запуск
// B6: ошибка HX711
// B5: ошибка BME280
// B4: тревога по температуре
// B3: тревога по влажности
// B2: тревога по весу
// B1: низкий запас
// B0: резерв

Для разбора на стороне Sigfox используется Custom Payload Configuration:

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

Важно: little-endian для много байтовых полей (Sigfox по умолчанию big-endian, Arduino — little-endian). Флаги в первом байте не сдвигают указатель байта, поэтому status читается отдельно.

Включены флаги тревог по Т, Вл, весу — так как сообщения могут быть редкими (1–2 в день), кратковременные перегревы могут быть пропущены облаком, поэтому флаги выставляются на устройстве и сбрасываются после успешной отправки.

itemCount = реальное_кол-во × 100. Это позволяет передавать дробные значения (2.2, 2.95) без float, используя int16. Знаковое — для коррекции нуля (может быть -1, -2).

Для активации Sigfox: отправьте 's' в Serial Monitor — выведется ID и PAC код. Зарегистрируйте устройство на backend.sigfox.com.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Настройка Callback в Sigfox для отправки данных в Tinamous (Device Type → Callbacks → Custom Callback, тип DATA → BIDIR). Включите Downlink (по умолчанию выключен даже для BIDIR). Добавьте SERVICE → ACKNOWLEDGE callback для подтверждения доставки downlink.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Использование Really Smart Box

После сборки, прошивки и установки батарей платформу просто кладут в коробку Really Useful Box — готово.

Питание включайте как можно позже: через 2 мин после включения уйдёт первое Sigfox-сообщение с запросом downlink (можно передать команду тарировки). Если не сработало — либо USB, либо ждать следующего downlink (каждые 12 часов).

В рабочем режиме платформа шлёт данные каждые 15 мин: вес, количество единиц, Т, Вл, тревоги. Измерения Т, Вл, веса — каждую минуту для детекции тревог.

Мониторинг в Tinamous

Tinamous поддерживает Sigfox callbacks через «Sigfox Bot». Инструкция: мой туториал «Get Your Sigfox On» на Hackster.io.

После добавления бота появится экран настройки Callback. Создайте Custom Callback в Sigfox (используйте Custom Payload из кода). Lat/Lng в callback дают приблизительную локацию, но лучше использовать встроенную геолокацию Sigfox (нужен второй callback). Не указывайте Lat/Lng в основном сообщении, иначе коробка будет «прыгать» между координатами.

Downlink callback нужно включить вручную (галочка Downlink), доступно только если Device Type настроен на CALLBACK для downlink.

Настройка устройства в Tinamous

Когда данные начнут приходить, устройство появится в списке. Я установил «Not Reporting After» = 1 час (период отправки 15 мин) — так узнаю, если устройство сломалось.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Скрыл лишние поля (флаги) на графике, оставил вес и количество единиц. Добавил понятные подписи и единицы измерения.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Поле Item Count хранит значение ×100 — добавил калибровку деления на 100.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Downlink-данные: команда тарировки, пороги Т и Вл. Отправляются при следующем запросе downlink (через 2 мин после включения, потом каждые 12 часов).

Really Smart Box: умная коробка для IoT-мониторинга запасов

Просмотр данных

На странице устройства видно вес, расчётное количество единиц, Т, Вл. (На скриншоте тара не сброшена — показывает 0.5 единицы; верхнюю пластину заменил на 5 мм для тяжелого филамента).

Really Smart Box: умная коробка для IoT-мониторинга запасов

Вкладка Sigfox показывает историю callbacks, включая downlink и ACK. (Arduino сообщает ошибку 62 — см. «Проблемы»).

Really Smart Box: умная коробка для IoT-мониторинга запасов

Вкладка Location — где коробка (полезно, если забыли у какого клиента или в каком фуре).

Really Smart Box: умная коробка для IoT-мониторинга запасов

Дашборд: вес содержимого, расчётные единицы, счётчик нерепортящихся устройств.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Уведомления в Tinamous

Настроил email/SMS при низком запасе. Задал рабочий диапазон Item Count = 3–300. Выход за границы → событие «measurement out of range».

Really Smart Box: умная коробка для IoT-мониторинга запасов

Создал Notification на это событие.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Поле можно не указывать — сработает для любого out-of-range. Устройства — тоже все (пока одно).

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Repeat Notifications = Once per day (иначе каждые 15 мин будет раздражать).

Really Smart Box: умная коробка для IoT-мониторинга запасов

Каналы: email + SMS.

Really Smart Box: умная коробка для IoT-мониторинга запасов

Заключение

Теперь можно развернуть Really Smart Box и забыть. При низком запасе придет уведомление, дашборд покажет состояние. Sigfox избавляет от настройки Wi-Fi и частой смены батарей — деплой максимально прост.

Планирую установить в коробку с филаментом в Cambridge Makespace.

Really Smart Box: умная коробка для IoT-мониторинга запасов
Really Smart Box: умная коробка для IoT-мониторинга запасов

Проблемы к решению

Код

Really Smart Box Arduino Code

Подключите библиотеки: Arduino MKR FOX 1200, HX711, Adafruit BME280, ArduinoLowPower. Программируйте через Arduino IDE.

// Really Smart Box 
// Measures the weight of the contents of a really smart box 
// Made by two sheets of acrylic with 2 load cells between them 
// placed in a really smart box. 
// Also includes a BME280 to measure temperature and pressure inside the box. 
// Author: Stephen Harrison 
// License: MIT 
#include <Adafruit_BME280.h> 
#include <HX711.h> 
#include <SigFox.h> 
#include <SigFox.h> 
#include <ArduinoLowPower.h> 
// -------------------------------------- 
// BME280 on the I2C port. 
Adafruit_BME280 bme; 
// -------------------------------------- 
// HX711 load cell amplifier. 
// 0: D0 - DOUT 
// 1: D1 - CLK 
// initial gain of 128. 
HX711 scales(0, 1, 128); 
// Arrays for load cells. Index 0 == Channel A, Index 1 == Channel B. 
float gain[] = {128,32}; 
// Calibration factors. 
// we use y = mx + c (c = offset, m = scaleFactor). 
// to convert the measured value into a weight. 
// Set this to the offset reported by the load cells. 
// with no weight on them. 
float offset[] = {0,54940}; 
// Set this to the factor computed when a weight is placed on the scale. 
// Set the offset first, re-flash the arduiono for this to take effect 
// place a weight on the scale and divide the raw measured value by the weight. 
// using scaleFactor = measured value / weight. 
float scaleFactor[] = {378.f,260.9f}; 
// -------------------------------------- 
// Sigfox 
// This is the data structure we publish to Sigfox. 
// Split out the bits as bool flags from the first status byte but the byte still needs to be 
// included otherwise humidity becomes the 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 to 8 bits 
  // B7 - First run, B6 - HX711 fault, B5 BME280 fault, B4 Temperature alarm, B3 - Humidity alarm, B2 - weight alarm, B1 - Low stock, B0 - spare 
  int8_t humidity; // humidity::int:8 (yes some sensors (HTU21D read -ve humidity) 
  int8_t temperature; // temperature::int:8 (no decimal places). 
  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 actual item count to allow for 2.01 (as weight won't match exactly) 
  int8_t driftCorrection; // Drift Correction for changes in zero weight applied to the scales. 
  int8_t filler; 
  int8_t lastStatus; // Last sigfox status 
} SigfoxMessage; 
// Time the last Sigfox message was published at 
long lastPublish = 0; 
// Time the last Sigfox downlink was requested. 
// Allowed max 4 per day of these. 
long lastDownlink = 0; 
uint8_t lastSigfoxStatus = 0; 
// -------------------------------------- 
// Application/state 
// If the fist cycle (after a reset) for the measure/publish 
// cycle (this is used to request a downlink message from Sigfox). 
// Note that only 4 of them are allowed per day so becareful 
// when deploying code. 
bool isFirstCycle = true; 
// Application mode 
// 0: Normal 
// 1: Calibration 
int mode = 0; 
// Which channel should be read during calibration. 
int calibrate_channel = 0; 
// The last average value measured for each channel. 
float lastAverage[] = {0,0}; 
// The current weight of the contents of the box 
float currentWeight = 0; 
// The weight of the units the box will hold. 
// Updatable via Sigfox downlink message. 
float unitWeight = 238; 
// Different to tare as it would be a manual 
// zero'd at a set reading from scales 
// This will most likely change with drift (time/temperature/etc) 
// and should be set once the scale is in place but not loaded. 
// Updatable via Sigfox downlink message. 
float zeroWeight = 0; 
bool bmeOk = true; 
bool hx711Ok = true; 
// Alarms and alarm ranges 
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; 
// Setup the Arduino. 
void setup() { 
  pinMode(LED_BUILTIN, OUTPUT); 
  //Initialize serial: 
  Serial.begin(9600); 
  // NB: The sensor I'm using (from random eBay seller) 
  // does not use the default address. 
  bmeOk = bme.begin(0x76); 
  if (!bmeOk) { 
    Serial.println("Could not find a valid BME280 sensor!"); 
  } 
  // Delay for USB Serial connect and for the BME's first reading. 
  delay(5000); 
  Serial.println("Really Smart Box..."); 
  printHeader(); 
} 
int delayCounter = 0; 
void loop() { 
  switch (mode) { 
    case 0: 
      measureAndPublish(); 
      //Sleep for 1 minutes 
      // Causing problems with USB connected. 
      //LowPower.sleep(1 * 60 * 1000); 
      delay(60 * 1000); 
      break; 
    case 1: 
      calibrate(); 
      delay(1000); 
      break; 
  } 
  // Check for user input via the serial port. 
  checkSerial(); 
  // measure is done on RTC timer tick (once per minute) 
  delay(100); 
} 
void measureAndPublish() { 
  // turn the LED on to indicate measuring. 
  digitalWrite(LED_BUILTIN, HIGH); 
  printBmeValues(); 
  measureTemperatureAndHumidity(); 
  measureWeight(true); 
  // Weight, temperature and humidity are read every minute 
  // however we only publish occasionally. 
  if (shouldPublish()) { 
    publishMeasurements(); 
  } 
  digitalWrite(LED_BUILTIN, LOW); 
} 
// Main measurement loop. Reads the weight from the load cells 
// and stores if no noise from the previous read. 
void measureWeight(bool printDetails) { 
  scales.power_up(); 
  delay(500); 
  float delta = readDelta(printDetails); 
  if (printDetails) { 
    Serial.print("\t"); 
    Serial.print(delta, 2); 
  } 
  // If the delta is between -1 and 1 (i.e. no noise) 
  // update the change in overall weight and units contained 
  // otherwise ignore and try again later on. 
  // This ensures we use only stable readings when both channels have not changed for 
  // two sets of measurements. 
  if (delta < 1.f && delta > -1.f) { 
    // Remember the previous measured weight so we can get a delta. 
    float lastWeight = currentWeight; 
    // Compute the weight. Take the weight of both load cells 
    // added together then subtract the zero'd weight. 
    currentWeight = lastAverage[0] + lastAverage[1] - zeroWeight; 
    // Compute the difference in weight of the items in the box 
    // compated to the last time we had a stable reading. 
    float itemsWeightDelta = currentWeight - lastWeight; 
    updateStockLevels(); 
    if (printDetails) { 
      Serial.print("\t"); 
      Serial.print("\t"); 
      Serial.print(currentWeight, 2); 
      Serial.print("\t"); 
      // divide by unit weight to estimate the stock level in the box 
      Serial.print(currentWeight / unitWeight, 2); 
      Serial.print("\t"); 
      // the change in weight, (i.e. the weight if the items added) 
      Serial.print(itemsWeightDelta, 2); 
      Serial.print("\t"); 
      // divide by unit weight to estimate the units removed/added 
      Serial.print(itemsWeightDelta / unitWeight, 2); 
    } 
  } 
  checkWeightLimits(); 
  if (printDetails) { 
    Serial.println(); 
  } 
  // put the ADC in sleep mode and switch 
  // off the LED now we're done measuring. 
  scales.power_down(); 
} 
void updateStockLevels() { 
  stockLevel = currentWeight / unitWeight; 
  // Unlike other alarms the low stock level 
  // is reset if the stock is re-stocked. 
  lowStock = stockLevel < minStock; 
} 
// Check if the current total weight 
// or a single load cell weight is out of range. 
void checkWeightLimits() { 
  if (currentWeight > maxWeight ) { 
    weightAlarm = true; 
  } 
  if (lastAverage[0] > (maxWeight /2)) { 
    weightAlarm = true; 
  } 
  if (lastAverage[1]> (maxWeight /2)) { 
    weightAlarm = true; 
  } 
} 
// Read the difference in weight from the last 
// average to this time across both load cells. 
// average value is stored in the lastAverage array. 
float readDelta(bool printDetails) { 
  float aDelta = readChannel(0, true); 
  if (printDetails) { 
    Serial.print("\t"); 
  } 
  float bDelta = readChannel(1, true); 
  return aDelta + bDelta; 
} 
// Read the weight from a channel. Stores the measured value in 
// the lastAverage array and retuns the delta of the measured value 
// from the previous lastAverage. This allows us to know if the weight 
// has changed. 
// channel 0 = A 
// channel 1 = B 
float readChannel(int channel, bool printDetails) { 
  // Gain: 
  // Channel A supports 128 or 64. Default 128 
  // Channel B supports 32 
  // Select Channel B by using gain of 32. 
  scales.set_gain(gain[channel]); 
  // HX711 library only has one set of offset/scale factors 
  // which won't work for use as we use both channels and they 
  // have different gains, so each needs to have it's offset/scale set 
  // before reading the value. 
  scales.set_offset(offset[channel]); 
  scales.set_scale(scaleFactor[channel]); 
  // force read to switch to gain. 
  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; 
} 
// print the header for the debug data pushed out when measuring. 
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(""); 
} 
// Calibration - reads/prints selected channel values. 
void calibrate() { 
  digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) 
  scales.set_gain(gain[calibrate_channel]); 
  scales.set_offset(offset[calibrate_channel]); 
  scales.set_scale(scaleFactor[calibrate_channel]); 
  // force read to switch to gain 
  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); 
} 
// check the serial port for input from a console to allow us to alter 
// the device mode etc. 
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': 
        // Measurement mode 
        mode = 0; 
        Serial.println("Measurement Mode"); 
        printHeader(); 
        break; 
      case 'c': 
        // Calibration mode 
        mode = 1; 
        Serial.println("Calibration Mode"); 
        break; 
      case 't': 
        // Tare. Teset the scale to 0 
        Serial.println("Taring"); 
        scales.power_up(); 
        delay(500); 
        scales.tare(5); 
        // Need to do this for each channel 
        // and update our stored offset. 
        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; 
    } 
  } 
} 
// Measure (and record) the temperature and humidity levels 
// Sets alarms if out of rage (we can't use limits on Internet service side 
// as the messages may only be sent a few times a day and a brief (maybe hours) 
// out of range temperature/humidity could easily be missed between 
// message publishing. 
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; 
  } 
} 
// Print the values read from the BME280 sensor 
void printBmeValues() { 
  //Serial.print("Temperature = "); 
  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"); 
} 
// ============================================================= 
// Sigfox handing 
// ============================================================= 
// Determine if we should publish the Sigfox message. 
// We may also wish to publish if the stock level has 
// changed (or a significant weight level has changed) 
// but we would need to be careful of exceeding the 
// 140 messages per day for a noisy system. 
bool shouldPublish() { 
  // Publish every 15 minutes 
  // this doesn't really need to be this often 
  // but whilst developing it helps keep an eye on the system. 
  int messageIntervalMinutes = 15; 
  // On first run after reset 
  // allow a 2 minute delay for the platform to be placed into 
  // the box and stabalise before doing first publish 
  // which is also expected to include a check for zeroing the platform. 
  if (isFirstCycle) { 
    messageIntervalMinutes = 2; 
    Serial.println("First cycle"); 
  } 
  // How long ago we last publish a Sigfox message 
  long millisAgo = millis() - lastPublish; 
  return millisAgo > (messageIntervalMinutes * 60 * 1000); 
} 
// Publish our measurements (weight, temperature, humidity etc) 
// to Sigfox. 
void publishMeasurements() { 
  Serial.println("Sending via Sigfox..."); 
  bool useDownlink = shouldUseDownlink(); 
  if (useDownlink) { 
    Serial.println("Using Sigfox downlink..."); 
  } 
  // stub for message which will be sent 
  SigfoxMessage msg = buildMessage(); 
  SigFox.begin(); 
  SigFox.debug(); 
  // Wait at least 30mS after first configuration (100mS before) 
  delay(100); 
  // Clears all pending interrupts 
  SigFox.status(); 
  delay(1); 
  SigFox.beginPacket(); 
  SigFox.write((uint8_t*)&msg, 12); 
  // endPacket actually sends the data. 
  uint8_t statusCode = SigFox.endPacket(useDownlink); 
  printSigfoxStatus(statusCode); 
  // Status = 0 for a successful send, otherwise indicates 
  // a failure. 
  // Store when we last published a Sigfox message 
  // to allow for timed message sending. 
  if (statusCode == 0) { 
    resetAlarms(); 
  } 
  // Update the last publish/downlink times 
  // even if an error resonse was received to prevent 
  // repeated publishing 
  lastPublish = millis(); 
  isFirstCycle = false; 
  if (useDownlink) { 
    parseDownlinkData(statusCode); 
    lastDownlink = lastPublish; 
  } 
  // Store the status value 
  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(); 
  } 
} 
// Create the message to be publish to 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; 
} 
// Get the status flags for the Sigfox message. 
byte getStatusFlags() { 
  byte status = 0; 
  // B7 - First run, 
  // B6 - HX711 fault 
  // B5 - BME280 fault 
  // B4 - Temperature alarm 
  // B3 - Humidity alarm 
  // B2 - weight alarm 
  // B1 - Low stock 
  // B0 - spare 
  // Upper Nibble (Charging/Battery) 
  // Battery flat 
  if (isFirstCycle) { 
    status |= 0x80; // 1000 0000 
  } 
  // HX711 fault. 
  // we don't have a way to check this yet. 
  if (!hx711Ok) { 
    status |= 0x40; // 0100 0000 
  } 
  // BME280 fault 
  if (!bmeOk) { 
    status |= 0x20; // 0010 0000 
  } 
  // Over/Under temperature alarm 
  if (temperatureAlarm > 0) { 
    status |= 0x10; // 0001 0000 
  } 
  // Over/Under humidity alarm 
  if (humidityAlarm) { 
    status |= 0x08; // 0000 1000 
  } 
  // Over/under? weight alarm 
  if (weightAlarm) { 
    status |= 0x04; // 0000 0100 
  } 
  // if computed stock level low. 
  if (lowStock) { 
    status |= 0x02; // 0000 0010 
  } 
  return status; 
} 
// Determine if we are requesting a downlink message. 
bool shouldUseDownlink() { 
  // When debugging uncomment this so as to not keep requesting 
  // downlink 
  //return false; 
  // On first run we want to request a downlink 
  // message to help with zero'ing and setup. 
  if (isFirstCycle) { 
    return true; 
  } 
  // How long ago we last did a downlink message. 
  long millisAgo = millis() - lastDownlink; 
  // try every 12 hours, this keeps us under the 
  // maximum 4 per day. 
  return millisAgo > (12 * 60 * 60 * 1000); 
} 
// Parse downlinked data. 
void parseDownlinkData(uint8_t statusMessage) { 
  if (statusMessage > 0) { 
    Serial.println("No transmission. Status: " + String(statusMessage)); 
    return; 
  } 
  // Max response size is 8 bytes 
  // set-up a empty buffer to store this. (0x00 == no action for us.) 
  uint8_t response[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 
  // Expect... 
  // 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 
  // Parse the response packet from Sigfox 
  if (SigFox.parsePacket()) { 
    Serial.println("Response from server:"); 
    // Move the response into local buffer. 
    int i = 0; 
    while (SigFox.available()) { 
      Serial.print("0x"); 
      int readValue = SigFox.read(); 
      Serial.println(readValue, HEX); 
      response[i] = (uint8_t)readValue; 
      i++; 
    } 
    // byte 0 - flags. 
    // 0b 1000 0000 
    if (response[0] & 0x80 == 0x80) { 
      zeroScales(); 
    } 
    // 0b 0100 0000 
    if (response[0] & 0x40 == 0x40) { 
      updateTemperatureAlarm(response[1], response[2]); 
    } 
    // 0b 0010 0000 
    if (response[0] & 0x20 == 0x20) { 
      updateHumidityAlarm(response[3], response[4]); 
    } 
    // 0b 0000 0100 
    if (response[0] & 0x04 == 0x04) { 
      // Little Endian format. (ff dd -> 0xddff 
      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; 
  } 
  // Output the ID and PAC needed to register the 
  // device at the Sigfox backend. 
  String version = SigFox.SigVersion(); 
  String ID = SigFox.ID(); 
  String PAC = SigFox.PAC(); 
  // Display module informations 
  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); 
  // Send the module to the deepest sleep 
  SigFox.end(); 
} 
// ============================================================= 
// General helper methods 
// ============================================================= 
// Reset the alarms after they have been published. 
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; 
}

GitHub репозиторий

https://github.com/Tinamous/ReallySmartBox

Пользовательские детали и корпуса

Схемы

Really Smart Box: умная коробка для IoT-мониторинга запасов

Производственный процесс

  1. Революция соковых коробок: история, дизайн и экологический след
  2. Черные ящики самолетов: от ранних прототипов к будущим интегрированным дизайнам
  3. Солнечные ячейки: от ранних экспериментов к современной фотовольтаике — технологии, производство и перспекти…
  4. Умный бармен: создайте свою Raspberry Pi-станцию для коктейлей
  5. Умный светофор: адаптивное уличное освещение на базе IoT
  6. Интеллектуальное умное мусорное ведро: оптимизация сбора отходов с IoT-датчиками
  7. DIY робот в стиле Wall‑E на Raspberry Pi из коробки от CD
  8. DIY UVC-стерулизатор: бюджетный UV-коробок для медицинских и бытовых предметов
  9. Умная теплица IoT на Arduino и Azure: датчики, автополив и удалённый контроль
  10. Умные шторы: автоматическое управление светом на Arduino с солнечной энергией