# Building an IoT-powered Fridge

Imagine the scenario: guests are about to arrive, and you realize your fridge is running low on beer. Your **BoA (Beverage on Arrival)** SLA is in danger, and panic sets in, but fear not! In this blog post, we'll walk you through a solution we've devised to never end up in this situation again. By integrating AWS services, IoT technology, Lambda functions, and even a touch of Rust programming, **Elva** created a system that not only alerts us when our beverage stock is running low but also has the potential to automatically place orders to replenish our supplies in the future.

> Using AWS, IoT core, Lambda, Cloudwatch, TS, Rust
> 
> ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700580328879/802556e4-7174-40c4-ba31-8daad15f42c1.png align="left")

Requirements (total cost: ~31$)

* 4 [Weight sensors](https://www.sparkfun.com/products/10245) & 1 [Amplifier/ADC](https://www.sparkfun.com/products/13879)
    
* 1 [RPI zero w](https://www.raspberrypi.com/products/raspberry-pi-zero-w/)
    
* [AWS account](https://aws.amazon.com/free)
    
* *A few hours of your lifetime*
    

TLDR; Here are the projects

1. [Cloud implementation](https://github.com/elva-labs/byra/tree/main/byra-watcher)
    
2. [On-board processes](https://github.com/elva-labs/byra/tree/main/rpi)
    

## The Setup

The overall setup looks like this.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700668459569/27082876-1648-4687-a679-5342bbc27225.png align="center")

The image displays an abstract view of the data flow in the finalized system.

1. The sensors output an analog value depending on the applied pressure (weight). The reading is transformed into a digital output using an analog-to-digital converter (**ADC**).
    
2. That resulting data is sent to the scale-reading-process using serial communication (via Raspberry’s **GPIO**).
    
3. Next, the first (scale worker) process collects and transforms the digital values into something more understandable to us. The second (IoT worker) pushes that data to AWS IoT core and, in turn, other upstream services like Slack.
    

### The Cloud

> AWS IoT Core, Lambda, and Cloudwatch Metrics

As the infrastructure diagram notes, the IoT worker running on the Raspberry is pushing the translated scale readings to IoT core (using MQTT). Once we're in the cloud, we can do whatever we like. The main gist is that we have two handlers and some certificates.

We pass the final readings to Cloudwatch using lambda in this specific setup.

```typescript
// ~/byra-watcher/src/lambda.ts
const metrics = new Metrics({ namespace: "elva-labs", serviceName: "byra" });

export const handler = middy(async (
  event: {
    grams: number
  }) => {
  metrics.addMetric("beerWeight", MetricUnits.Count, event.grams);
}).use(logMetrics(metrics));
```

In Cloudwach, we can follow the readings and create alarms if the weight is below a specific threshold value. If this threshold value is breached, another lambda is triggered, which sends a message to Slack so that everyone can see that we have a critical problem to resolve.

```typescript
// ~/byra-watcher/src/slack.ts
export const handler: EventBridgeHandler<'_', CloudWatchAlarmDetail, void> = async (event) => {
  console.info(`Received event: ${JSON.stringify(event)}`);

  await new IncomingWebhook(Config.SLACK_URL).send({
    text:
      event.detail.state.value === "ALARM"
        ? `${WARN_EMOJI} CRITICAL: ${BEER_EMOJI} Beer count is low`
        : `${HAPPY_EMOJI} ALL GOOD: ${BEER_EMOJI} We have beer!`,
  });
};
```

It's time to deploy our handlers and acquire our "thing"-certificates so the RPI can push data to the cloud.

```typescript
// ~/byra-watcher/stacks/ByraStack.ts
// ...
const { thingArn, certId, certPem, privKey } = new ThingWithCert(stack, 'ByraScale01', {
  thingName: 'byra-01',
  saveToParamStore: true,
  paramPrefix: 'devices',
});
// ...
```

```bash
cd ~/byra-watcher && npm run deploy

SST v2.36.1

➜  App:     byra-watcher
   Stage:   dev
   Region:  eu-north-1
   Account: ...

|  ByraStack PUBLISH_ASSETS_COMPLETE 

✔  Deployed:
   ByraStack
   Byra01Thing: arn:aws:iot:eu-north-1:...:thing/byra-01
   CertId: ...
   CertPem: -----BEGIN CERTIFICATE-----
            ...
            -----END CERTIFICATE-----    

   PrivKey: -----BEGIN RSA PRIVATE KEY-----
            ...
            -----END RSA PRIVATE KEY-----
```

Who could have guessed? **Super simple**. Now, we have our infrastructure deployed and our certificates generated.

### The Hardware

The objective of the Raspberry is to read data from the sensors, transform the reading into something we can understand, and then push that data to the cloud for further action.

* The scale worker code can be found [here](https://github.com/elva-labs/byra/tree/main/rpi/elva-byra-scale).
    
* The IoT worker code can be found [here](https://github.com/elva-labs/byra/tree/main/rpi/elva-byra-iot-worker).
    
* The wiring guide for the cells can be found [here](https://learn.sparkfun.com/tutorials/load-cell-amplifier-hx711-breakout-hookup-guide/all).
    

To read data, we needed to know how to communicate with the analog-to-digital (ADC) component. Reading through the [documentation](https://cdn.sparkfun.com/datasheets/Sensors/ForceFlex/hx711_english.pdf), we found that it uses serial communication and a 24-bit data protocol (+gain bits) to send data over the wire.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700724566655/d096f067-a4cf-44f8-a7ba-e3e0eb3fc243.jpeg align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1687091305154/d7552b56-62e6-49c2-893c-bbef1777c15c.png align="center")

The following protocol is implemented like the following image. In short, we ensure we can read data from the ADC and dump each bit into a temporary buffer, which finally translates to an actual value in grams.

```rust
// ~/rpi/elva-byra-scale/src/hx711.rs#L107
fn read(&mut self) -> Result<f32, HX711Error> {
        if !self.dout.is_low() {
            return Err(HX711Error::new(HX711ErrorType::DoutNotReady));
        }

        let mut buff = 0;

        for _ in 0..24 {
            self.send_pulse()?;
            thread::sleep(Duration::from_nanos(100));
            buff <<= 1;
            buff |= match self.dout.read() {
                Level::Low => 0b0,
                Level::High => 0b1,
            };
        }

        // Sets gain for following reads...
        for _ in 0..match self.gain {
            Gain::G32 => 3,
            Gain::G64 => 2,
            Gain::G128 => 1,
        } {
            self.send_pulse()?;
        }

        Ok(self.translate(buff))
}
```

### Debugging

We can debug the communication over the wires using a [logic analyzer](https://www.amazon.se/AZDelivery-Analyzer-USB-Kabel-kompatibel-inklusive/dp/B01MUFRHQ2/ref=asc_df_B01MUFRHQ2/?tag=shpngadsglede-21&linkCode=df0&hvadid=476458787949&hvpos=&hvnetw=g&hvrand=8825784032754899236&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1012511&hvtargid=pla-404738525278&psc=1) and [this helpful program](https://www.saleae.com/downloads/). A typical information exchange looked like this.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700644212318/3a51a8f5-d19b-4dc4-9b1b-1a9a483a62c6.png align="center")

The upper channel is the data channel (sent from the ADC), and the lower is the pulses sent from the Raspberry over the SCK channel (which dictates the read-timings).

We read the expected bytes into our process using the documented timings. Then convert the 24 bits to an integer value (that doesn't mean anything to us for now). After we have ensured that the readings are stable, i.e., not bouncing all over the place and giving us random values, we can move on to translating the actual output to a representation we can understand.

### Calibration & Output

Next, we placed the fridge on the scale, ensured all sensors were actively engaged, and noted a few readings. Then, place one kilogram on the scale and do the same thing. We should be able to determine two averages, which will help us translate pressure changes to actual grams.

We now have two reference values, one when the scale is empty and one with one kilogram on the scale.

`points_per_gram = (one_kg_reading_avg - empty_scale_reading_avg) / 1000`

```rust
// ~/elva-byra-scale/src/hx711.rs#L153
fn translate(&self, read: i32) -> f32 {
    (read as f32 - self.offset) / self.points_per_gram
}
```

Great, we now use this function to translate the reading from the scale to something we can understand and reason about in the upstream services.

Using an offset (empty\_scale) of 1076761 in our case and 1099180 (one\_kg\_reading). We get the following output while having a few things in the fridge that would be ~10 kg of weight.

| 0b | 10 | Grams |
| --- | --- | --- |
| 000101000001101000000001 | 1317377 | 10732.68 |
| 000101000001100110111100 | 1317308 | 10729.60 |
| 000101000001101001000100 | 1317444 | 10735.67 |

The scaling process reads the scale value at a fixed interval and outputs a more readable JSON structure to the `/tmp/byra.sock` so that other processes may act on the changes.

```bash
$ netcat -U /tmp/byra.sock
# {"datetime":"2023-06-18T12:11:48.759024680Z","grams":3660.4785}
```

The secondary process listens to this socket and pushes each new sample to **AWS IoT core** using **MQTT** and the credentials we received after deploying the infrastructure.

### The Result

Finally, we can ensure we're always prepared to supply our guests with cold beverages on arrival and our **BoA** metric remains in the safe zone.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700580328879/802556e4-7174-40c4-ba31-8daad15f42c1.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700580425921/0da974f2-060d-4a95-9801-3f45e8a6e512.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700723892321/af7a2062-bbac-4ac3-b8fd-0500b97a7078.jpeg align="center")

---

If you enjoyed this post, follow me on [GitHub](https://github.com/JoelRoxell). I dabble with everything related to fully managed serverless solutions on AWS, embedded stuff, and Rust.

---

%%[leadfeeder] [Elva](https://elva-group.com) is a serverless-first consulting company that can help you transform or begin your AWS journey for the future
