Project tutorial

Really Smart Box © MIT

Turn a really useful box into a really smart box with this drop in platform to help manage stock levels.

  • 7,723 views
  • 4 comments
  • 19 respects

Components and supplies

Necessary tools and machines

Lasercutter
Laser cutter (generic)
3drag
3D Printer (generic)

Apps and online services

About this project

The Really Smart Box platform turns a Really Useful Box (tm) into an intelligent, internet connected storage box for stock monitoring. Based on the Sigfox Arduino MKR FOX 1200 it senses the weight of things stored in the box, along with temperature and humidity and uses the low power Sigfox radio to relay this information.

Introduction

Use Case - 3D Printer Filament Storage:

If you own a 3D printer you will most likely care about how your filament is stored, this is true not only of printer filament but many other things need to be stored within acceptable temperature and humidity ranges (for example painters caulk may become unusable if exposed to below freezing temperatures).

As one of the people responsible for maintaining the 3D printers in my local maker space, I need to ensure we have sufficient stock of filament and that it is kept dry.

With the Really Smart Box I am able to monitor the weight of the filament and so know if we were getting low, along with the monitoring the humidity level in the box to tell if the silica gel needs replacing.

Use Case - Consumables Stock Control:

A cleaning contractor may wish to keep a stock of air freshener, hand soap or other consumables at a clients site, the client would likely not allow the contractor WiFi access or to power a device like this when not present, however the contractors company needs to know when to send in new stock, adding overhead to the cleaners time and extra paperwork that nobody likes.

The Really Smart Box platform simply drops into a storage box, because it uses Sigfox it does not need to connect to the clients network and is low power so runs from a set of batteries. As the contents of the box would change very infrequently the Arduino can be held in a low power state for most of the time helping to extend the battery lifetime.

The platform can be told the weight of the item type stored in the box (i.e. the air fresheners) and hence calculate how many are in the box. This can then be sent to the cleaning contractors company to alert them when they need more stoke to be delivered to the clients site.

Making The Platform:

The platform is of simple construction, the main body is made from two pieces of laser cut acrylic (I used 3mm, thicker would be better for heavy items like printer filament) with a pair of load cells between them.

I manually counter sunk the screw holes for the load cell to give a nicer finish, I have as yet to find a laser cutter that does countersinking!

The acrylic can be cut to any size to match the box you like, however watch of positioning of the load cells and the length of wire on these as it's fairly short. The upper acrylic sheet is slightly smaller than the lower to ensure it doesn't snag on the box sides.

The lower sheet has a cutout to allow the electronics to be mounted to the acrylic without spacers and have the soldered device legs poking through. I used a M3 tap on the corner sections of the cutout to directly bolt the PCB to it. 3D printed feet were also fitted at the corners to sure that any screws from the load cell that were not flush with the acrylic did not affect the balance.

The weight is sensed using two 5Kg load cells. A set of 4 is normally used in bathroom scales and this might have be better, however I couldn't find a nice way to fix them to the acrylic as well as provide the spacing required for the electronics.

The load cells need some padding top and bottom to allow for a little flexing and the actual strain gauge sensors (the white bit in the picture) of the load cell are fatter than the mounting block. This is achieved under the load cell with the two "Really Smart Box" 3D Printed end plates which have a small block to raise the load cell up, on the top of the load cell are some laser cut acrylic padding blocks.

The load cells are connected to a HX711 load cell amplifier. This has two channels (A and B) that can be selected which is perfect for this use.

Each load cell is constructed of strain gauges in a Wheatstone bridge configuration, this creates an unbalanced pair of potential dividers, when the load cell is placed under load, the resistance of the strain gauges change and hence a difference between the two potential dividers is created, this is amplified and measured by the HX711 which performs the analog to digital conversion for us.

I've used two 5kg load cells for this project, you can get different ratings (e.g. 1kg and 10kg) which work exactly the same but with different sensitivity.

When placing the load cells ensure that the arrow on the end of the cell is pointing down (in the direction of the load). The cells have M5 tapped holes at one end (generally the fixed end) and M4 the other (the side you place the load on.

The red/black wires are power, this feeds top and bottom of the potential dividers and is shared between the two load cells. Green and white are the sense wires from the middle of the potential dividers, these are connected to Channel A and B on the HX711.

The HX711 supports 3 gain factors, however these are also used for channel selection. Gains of 128 and 64 are available on the A channel, where as selecting a gain of 32 selects the B channel. this means our second channel will not be as sensitive as the main channel, this is fine for this application.

The HX711 can be connected to any digital pins on the Arduino, I've used D0 (Data) and D1 (Clock), then the amplifier just needs to be connected to the Arduino's 3v3 supply.

You can read more about load cells and the HX711 at SparkFuns excellent load cell tutorial.

Finally a BME280 is connected to the I2C bus and used to sense the temperature and humidity inside the box, this can also be used to sense pressure however this is probably of little interest and we only have 12 bytes of sigfox data to play with so it's not reported.

The electronics is mounted on a ThingySticks Arduino Prototype board, I added a battery holder (hot melt glued to the lower acrylic sheet) and connected the aerial which is of a nice flat design so worked perfectly for the platform.

Load Cell Calibration:

Before we can use the platform the load cells need calibrating. Each load cell is unique, it is made by attaching strain gauge to a block of metal and holes drilled in it to provide sufficient flexing that can be sensed without it snapping, because of this we need to calibrate each load cell for its response to weight.

Once calibrated we apply the y=mx+c equation to the measured ADC value (x) to get the actual weight (y). So we need to find c (the offset) and m (the slope) for our load cell.

I removed the main platform top and attached a small acrylic square to each load cell in turn, and monitored the measured values (theirs a routing in the firmware to do this which can be started by sending a "c" to the serial port.

Initially the reading for an empty platform was measured, this gives the offset (c) value, then placing a load of known weight on the cell, the difference in readings gives us the slope.

Slope = (measured - offset) / weight (g). 

I used both a small air freshener can (ca. 230g) and a spool of printer filament (ca. 1.5kg) to check the values, both gave about the same slope which was reassuring.

Naturally the offset measured with the small acrylic pad is different to that experienced with the full top sheet, likewise their is also a small slope difference when using both load cells so a secondary calibration is needed. For now a one point zero offset (tare) is used, this is set in the firmware but can also be set using the USB serial connection or through a Sigfox downlink message once deployed.

Sigfox Connection:

With the Really Smart Box wire up I initially used the USB serial port to monitor the output to help debug and tune the system. This way you can see the individual load cells, changes and noise. However this isn't going to work for a deployed box as it needs to be totally wireless.

With Sigfox we can send 12 bytes of data upto 140 times a day to our online service, this is more than enough for the Really Smart Box. The below data structure is used in the Arduino describes how we use the 12 bytes.

typedef struct __attribute__ ((packed)) sigfox_message { 
 uint8_t status;       // status flags
 int8_t humidity;      // humidity::int:8 - 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;        // Nothing to see here, move along....
 int8_t lastStatus;    // Last sigfox status 
} SigfoxMessage; 

The first byte (status) is split into bit flags to indicated issues:

// 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  

This structure compacts down to 12 bytes, however we need to unpack it at the Sigfox end to push onto Tinamous. We use a Custom Payload Configuration for that and it's best to work this out as the data structure is defined. Ours is:

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  

The custom payload splits our 12 bytes down as it is parsed.

Note that we need to specify the little-endian nature of anything larger than 1 byte as Sigfox defaults to big-endian and the Arduino uses little-endian (i.e. the least significant byte is first in multi-byte words).

Also note that splitting out the boolean flags in the first byte doesn't progress the byte marker as it does with all other reads so the status byte which holds all the flags is also read to skip over the first byte.

Included in the flags are temperature, humidity and weight range alarm flags, we could use an online service (i.e. Tinamous) to monitor for out of range temperature,humidity and weight, however these may be short lived (a few hours) and our box may be sending infrequently (once or twice a day) the resulting possible damaging environmental condition could easily be missed, so they are flagged on the device and sent (and reset after a successful send).

The item count is actually set to 100 times the actual item count. I wanted to allow for values such as 2.2 items (due to weight error or other items in the box) without forcing a rounding, likewise 2.95 might be rounded down to 2 if we are not careful and it would be more suggestive of 3 items in the box and a small error. I also didn't want to use a float which would require more space so I've used a 16 bit word and applied a factor to allow easy conversion (it's also signed to allow for a zero error, which might result in a stock level of -1 or -2 etc.).

Very little needs to be done to enable Sigfox communication. Within the Arduino the Sigfox library is added and appropriate functions called to push the data out as per the Arduino examples for the Sigfox library, however we need to register our device with Sigfox.

Sending an "s" to the serial port of the Really Smart Box prints the Sigfox ID and PAC code, these are used to activate the device at the Sigfox backend. We then head over to the Sigfox backend activation service and follow the wizard, first selecting our device, then country/provider and then some details.

And finally our device is activated and listed:

Sigfox assigns devices to Device Type grouping which is sensible as you may normally have many (hundreds, thousands, etc.) of the same device type which you wish to act on as a group. With the Device Type defined we can configure a custom callback to push the data we've received to our online service. I'm using Tinamous for this (hint: see my profile name - I might be biased in my selection).

Using the Really Smart Box:

Once wired up, bolted together, firmware flashed and batteries fitted the platform is simply dropped into a Really Useful Box (tm) and ready to go.

Power should be applied as late as possible as once the device is powered up it will send the first Sigfox message after 2 minutes and request downlink data with it. This data can include a "Zero" command to zero the platforms weight. Failing this either a USB serial connection is needed or waiting for the next downlink request - these are made every 12 hours.

Once up and running the platform will publish Sigfox messages every 15 minutes to send the weight, item count, temperature, humidity and alarm states. Temperature, humidity and weight are measured every minute to ensure these are not out of range with alarms being flagged for the next transmission if they have been raised.

Monitoring With Tinamous:

Tinamous supports Sigfox custom callbacks by adding a "Sigfox Bot" to our account, for instructions on how to do that please see my "Get Your Sigfox On" Hackster.io tutorial.

When add a Sigfox Bot to your Tinamous account, if you include the API settings, the Sigfox Bot will lookup your devices and add them to your Tinamous account, however you don't need to do this as the device will be automatically added when data is published.

When you've added the Bot we are be presented with a Callback configuration screen to help set-up the Sigfox Callbacks.

You can then create a custom callback at Sigfox, note that the Really Smart Box uses DATA -> BIDIR callback which handles the normal UPLINK callback and the BIDIR (up and downlink) callbacks.

This is where the custom payload from before comes in handy, paste this from the source code into custom payload and update the fields section to reflect this.

Lat and Lng are specified in this callback which gives an approximate location, however Sigfox on the Arduino supports improved location setting, but this needs a second callback. If you use the Geo location facility don't specify the Lat/Lng in this message as the Really Smart Box will appear to be moving between locations.

Once this is configured it also needs to be enabled for Downlink, this is disabled by default even though the BIDIR was set.

Note the screenshot below the Downlink option is "checked", this has to be done manually, and may not be available if the Device Type has not been set to "CALLBACK" for downlink data (Device Type -> Edit -> Downlink Data).

With a Downlink callback we also want to specify a SERVICE -> ACKNOWLEDGE callback to know our device has received the downlink data. By clicking on the Sigfox Bot in Tinamous other callback configurations are shown, follow the instructions for the ACKNOWLEDGE and GEOLOC callbacks.

Note that you need to copy the authorization header from the first uplink/bidir callback as this is a password that is one-way encrypted in Tinamous an no longer available for display.

With our callbacks in place the data published by our device should now be being sent to Tinamous. We could also add email callbacks at Sigfox which may help confirm the data is coming through (but can also get very noisy very quickly).

Configuring the Tinamous Device:

Once the Sigfox device has been seen at Tinamous (either through the api lookup or a callback), it will be shown on the Devices page, from here we can edit the properties. Fields are automatically added as they come from the Sigfox callback so it's best to wait until the device is publishing data.

I set the "Not Reporting After" time to 1 hour (It currently publishing every 15 minutes) so I can tell if the device has is broken and optionally get notified about this.

I didn't want to see all the fields sent by the device on the chart/details page (their's a lot if you include all the flags), so Tinamous is configured only to show weight and item count. Human friendly labels and units were also applied here.

The Item Count field is 100x the actual item count, so a calibration is applied to that field to reduce it 100 fold.

Some downlink data is set that will cause the Really Smart Box to zero and apply temperature and humidity range limits when it next requests a downlink message (2 minutes after power on, then once every 12 hours).

Viewing The Really Smart Box Information:

Now the device fields are configured we can monitor them through the device details page (Note I'd not zero'd the platform at this time, hence it thinks 1/2 a unit is present - I also replaced the acrylic top with a 5mm version which is heavier but will handle printer filament better).

We can also see the Sigfox callback interactions from the Sigfox section. Notice here that the downlink data is being sent and acked however the Arduino is reporting an error. More on that at the end.

On the Location tab we can also see where our Really Smart Box is, which might be useful if you forget which customer it's at, or if it's in a van.

And naturally we want a nice dashboard view of our Really Smart Box, the one below shows the box contents weight, the estimated units in it, and a count of devices that are not reporting so we can tell if one is broken.

Receiving Notifications With Tinamous:

Next I set-up Tinamous to send an email and sms when the item count is low. I did this by specify a working range of 3 - 300 for the item count field. If the value is outside of this range a measurement out of range even is raised.

Adding a Notification to Tinamous we can be notified when that happens.

We could specify just the field we're interested in, but leaving this empty gives us notifications for any field that is out of range.

Likewise for devices, leave it blank for all devices (i.e. the only one we've got at present!)

Set the Repeat Notifications to trigger once only, until it is reset (every day), otherwise notifications every 15 minutes gets annoying very quickly!

Then select how to be notified, I set it for email and sms then create the notification:

Conclusion:

I can now deploy the Really Smart Box and (hopefully) forget about it. When the stock level gets low I'll be notified and I can check on the dashboard to see how it is doing. Using Sigfox I don't have to worry about powering the device other than an occasional battery change and no WiFi set-up on site is required making deployment extremely simple.

I'm planning to deploy this unit into one of our filament storage boxes in Cambridge Makespace to monitor filament stock levels.

Issues To Resolve:

Needless to say this isn't a production quality finished project, a few issue still need to be resolved:

Sigfox Downlink:

Sigfox allows for 4 messages a day to be send in response to a uplink message. The Really Smart Box uses these to allow for re-zeroing of the scales, setting upper and lower temperature and humidity ranges and the item weight. However whilst trying to get this to work, even though the downlink message appears to be getting sent and is being acked (as shown in the Sigfox backend) the Arduino is reporting a status error of 62, which doesn't map to any error flag conditions listed for the ATA8520 chip, digging into the drivers the command uses a request for downlink that is also not listed in the data sheet, so some more investigation needs to be done.

Debug Only:

Running the Sigfox communications with debug disabled causes the low power setting of the Arduino to be activated with kills the USB serial port.

Low Power Mode:

As described for the Sigfox debug setting, using the Arduino's Low Power library causes the USB serial to drop off so this is not enabled at this time.

Drift:

No compensation for drift has been added, no doubt the load cells will drift when held under constant load.

Noise / Non Vertical Measurements:

It's possible the Really Smart Box may be in the back of a van (e.g. mobile cleaner, carpenter, plumber etc). It would be good to add an accelerometer to the platform and skip measurement cycles when the box is not steady, likewise if the box is not vertical the weight will not go through the load cells as expected.

Custom parts and enclosures

Laser cutting guide
Use this to cut the top and bottom acrylic sheets.
cuttingguide_e7GNHf980M.svg
Load cell base / platform end
This sits between the lower acrylic sheet and load cell to raise it up a little and provide a edge to the platforms
Print 4 of these for each corner of the lower sheet if needed

Schematics

Wiring
Nothing to complex.
2017 11 09 22 32 29 voe9fa1k65

Code

Really Smart Box Arduino CodeArduino
Add Libraries for Arduino MKR FOX 1200, HX711, AdaFruit BME280, Arduino Low Power. Use the Arduino IDE to program as normal.
// 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;
}
Really Smart Box Github Repository

Comments

Similar projects you might like

Pavlov's Cat

Project tutorial by Arduino

  • 141 views
  • 0 comments
  • 1 respect

Arduino Obstacle Avoidance Robot with Ultrasonic HC-SR04

Project tutorial by Jorge Rancé

  • 752 views
  • 1 comment
  • 10 respects

Using Finite State Machines

by Gustavo Gonnet

  • 6,948 views
  • 2 comments
  • 18 respects

Visual Accelerometer

Project tutorial by Reid Paulhus

  • 211 views
  • 1 comment
  • 5 respects

Alexa: "Your Clothes Are Dry"

Project in progress by TNunnster

  • 1,681 views
  • 0 comments
  • 6 respects

How to Configure NeoPixels Using Vixen Lights and Arduino

Project tutorial by Victor Aguilar

  • 460 views
  • 1 comment
  • 4 respects
Add projectSign up / Login