Twitter icon
Facebook icon
LinkedIn icon
Google icon
Reddit icon
StumbleUpon icon
Del.icio.us icon

Serenity Now! Build your own Bluetooth WFH Stress Monitor

Added to IoTplaybook or last updated on: 11/04/2021
Serenity Now! Build your own Bluetooth WFH Stress Monitor

Story

I'm fortunate that, for the last 12 years of my 22 year career, I have been able to work from home. Many of the jobs I've had in that time have required some travel, but I've never minded too much because, when I'm home, I can make my morning coffee, clothe myself from the waist up, and head into my well-apportioned home office for a day of maximum productivity.

At least, that's how I hope my days will go. And sometimes they do. But most of the time, I'm peppered by the constant barrage of Zoom calls, email, text notifications, and the unholy creation that is the Slack woodblock sound effect.

What's more, in a still COVID-present world, the lines between home and work--between life and career--are blurrier than ever. Increasingly, those sounds don't just dominate my workdays, but every waking hour of life.

It's enough to drive one's anxiety through the roof!

It's not just me...

It's not just me...

And I don't know about you, but sometimes I need an external reminder to take a deep breath or two when I'm feeling stressed, anxious or overwhelmed. That's why I built the WFH Stress Monitor, a Bluetooth and Cellular IoT solution that pays attention to my heart rate, the temperature, and noise in my office, shows me a live dashboard of how I'm doing, and sends me alerts when I need to take a break, breathe deep, or walk away.

Things used in this project

Hardware components

Notecard
Blues Wireless Notecard
 
× 1

Blues wireless

Feather Starter Kit
Blues Wireless Feather Starter Kit
 
× 1

Blues wireless

Feather Bluefruit LE Board
Adafruit Feather Bluefruit LE Board
 
× 1

Adafruit

 
Polar Verity Sense Heart Rate Monitor
 
× 1

Polar

Software apps and online services

Notehub.io
Blues Wireless Notehub.io
 
 

Notebun.io

SMS Messaging API
Twilio SMS Messaging API
 
  Twilio
Microsoft Azure
Microsoft Azure
 
  Microsoft Azure

In this article, I'll share what I created and show you how to:

  • Add external Bluetooth sensors to any project with the help of the Adafruit feather nRF52840 Sense and CircuitPython.
  • Use the Blues Wireless Notecard to encrypt my sensitive health and environment data before sending that data to Notehub.io.
  • Create Routes in Notehub.io to send encrypted data to Azure Serverless Functions, and alerts to Twilio's SMS service.
  • Decrypt health data and store it in an Azure CosmosDB database.
  • Build a simple Svelte-based web application for showing health data, and alerts in real-time.
  • Use Environment Variables to dynamically update alert thresholds for heart rate, temperature and sound level in my office.

This project really has something for everyone! If you want a brief overview of the project, check out the episode of Blues Wireless TV.

Oh, and you can find the source code for all of the above in my GitHub repo.

Let's get started!

"Assemble" the Hardware

My hardware setup for this project was pretty simple. I started with a Polar Verity Sense Heart Rate Monitor, a simple, screen-less, Bluetooth-enabled device that captures heart rate readings. To capture those readings, I needed a Bluetooth-capable MCU, and decided on the Adafruit Feather nRF52840 Sense. This fancy little device not only has Bluetooth courtesy of the Nordic nRF52840 on board, but it also includes a 9-DoF motion sensor, an accelerometer, magnetometer, temp, pressure, humidity, proximity, light, color, and gesture sensors, and a PDM microphone and sound sensor. Best of all, it supports CircuitPython!

Finally, for cloud connectivity, I added the Blues Wireless Notecard. If you've not yet heard of the Notecard, it's a cellular and GPS-enabled device-to-cloud data-pump that comes with 500 MB of data and 10 years of cellular for $49 dollars.

The Blues Wireless Notecard for Cellular communication

The Blues Wireless Notecard for Cellular communication

The Notecard itself is a tiny 30 x 35 SoM with an m.2 connector. To make integration into an existing prototype or project easier, Blues Wireless provides host boards called Notecarriers. I used the Notecarrier AF for this project because it includes a handy set of headers ready with any Feather-compatible device.

An easy hardware setup for this project

An easy hardware setup for this project

Read BLE Heart Rate and Environment Data with CircuitPython

The Sense Feather includes a lot of sensors, but for this project I decided to limit myself to a select few:

  • Heart rate and battery level from the Polar band;
  • Temp and pressure from the onboard BMP280;
  • Humidity from the SHT31D;
  • Sound Level from the PDM microphone

To work with the onboard sensors, I first added the CircuitPython libraries for each of these to the lib directory of my device, added import statements for each, as well as the note-python library, which provides an easy set of APIs for working with the Notecard.

import adafruit_bmp280
import adafruit_sht31d
import notecard

Since I'm also reading from a Bluetooth device, I needed to include the adafruit_ble library and related services for capturing heart rate and other device info.

import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService
from adafruit_ble.services.standard import BatteryService
from adafruit_ble_heart_rate import HeartRateService

Next, I configured the connection to my Notecard over I2C, initialized the BMP280, SHT31D, PDM mic, and the BLE radio on the Feather.

productUID = "com.blues.bsatrom:wfh_stress_detector"

i2c = board.I2C()

card = notecard.OpenI2C(i2c, 0, 0, debug=True)

bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)
sht31d = adafruit_sht31d.SHT31D(i2c)
microphone = audiobusio.PDMIn(
    board.MICROPHONE_CLOCK,
    board.MICROPHONE_DATA,
    sample_rate=16000,
    bit_depth=16
)

bmp280.sea_level_pressure = 1013.25

ble = adafruit_ble.BLERadio()

In the main part of my program, I read from the onboard sensors and scan for BLE heart rate data every sixty seconds. The BLE piece is the most complex part, so I wrapped it in a helper function that returns a dict of data from the Polar sensor, including the manufacturer, model number, heart rate, and the battery level of the device.

def get_heart_rate_data(hr_connection, notify_hr):
    heart_rate = {}

    print("Scanning for Heart Rate Service...")
    red_led.value = True
    blue_led.value = False
    time.sleep(1)

    for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
        if HeartRateService in adv.services:
            print("found a HeartRateService advertisement")
            hr_connection = ble.connect(adv)
            time.sleep(2)
            print("Connected to service")
            blue_led.value = True
            red_led.value = False
            break

    # Stop scanning whether or not we are connected.
    ble.stop_scan()
    print("Stopped BLE scan")
    red_led.value = False
    blue_led.value = True

    if hr_connection and hr_connection.connected:
        print("Fetch HR connection")
        if DeviceInfoService in hr_connection:
            dis = hr_connection[DeviceInfoService]
            try:
                manufacturer = dis.manufacturer
            except AttributeError:
                manufacturer = "(Manufacturer Not specified)"
            try:
                model_number = dis.model_number
            except AttributeError:
                model_number = "(Model number not specified)"
            heart_rate["manufacturer"] = manufacturer.replace("\u0000", "")
            heart_rate["model_number"] = model_number
        else:
            print("No device information")

        if BatteryService in hr_connection:
            batt_svc = hr_connection[BatteryService]
            batt = batt_svc.level
            heart_rate["battery_level"] = batt

        hr_service = hr_connection[HeartRateService]

        while hr_connection.connected:
            values = hr_service.measurement_values
            if values:
                bpm = values.heart_rate
                if bpm is not 0:
                    pct_notify = round(100 * (bpm / notify_hr))
                if values.heart_rate is 0:
                    print("Heart Rate not found...")
                    break
                else:
                    heart_rate["bpm"] = bpm
                    heart_rate["pct_notify"] = pct_notify
                    break
    return heart_rate, hr_connection

When I run the program, with a bit of logging code not included above, I'll see something like the following on the serial terminal.

Readings
---------------------------------------------
Temperature: 27.9 C
Barometric pressure: 995.314
Humidity: 56.7 %
Sound level: 6
Heart Rate:  64
% of Ceiling Rage:  53
---------------------------------------------

Encrypt Data with the Notecard

With readings in hand, I was ready to send data to the Notecard, but for this project, I decided to try out a new feature of the product: end-to-end data encryption. My plan was to encrypt my health data before it is synched to the cloud, and then decrypt it once it arrives at my serverless function for storage. That way, I can have a complete chain of custody over my data for the entire time it's out of my direct control.

As detailed in this guide at dev.blues.io, the Notecard supports AES 256 encryption of Notes, and I was able to leverage this capability by creating an RSA key-pair and saving the public key as an environment variable on my Notehub.io project.

Then, when adding readings to the Notecard via the note.add api, I used the key argument to provide the name of the environment variable containing my public key.

req = {"req": "note.add"}
req["file"] = "sensors.qo"
req["key"] = "encryption_key"
req["body"] = {
  "temp": bmp280.temperature,
  "humidity": sht31d.relative_humidity,
  "pressure": bmp280.pressure,
  "sound_level": sound_level,
  "heart_rate": heart_rate
}
card.Transaction(req)

When the Notecard sees the key argument, it uses my public key to generate a random 64-bit AES key, encrypts the Note body with that key, encrypts the RSA public key with the AES key, base64-encodes both, and queues both values on the Notecard for the next sync.

And on the other end, the data I've passed to the Notecard shows up in Notehub safely encrypted.

My encrypted data in Notehub.io

My encrypted data in Notehub.io

Route Encrypted Data to Azure

Once I got my health date into Notehub, I was ready to route readings to my cloud application for decryption, and storage. Using this guide at dev.blues.io as a reference, I created a new Notehub.io Route, pointed it to an Azure Function I set-up for receiving data from Notehub, and made sure to send along both the encrypted data and the encrypted AES key in the JSONata transformation.

Decrypt and Store Data in CosmosDB

Since the health data is coming into my saveHealthData Azure function, I need to decrypt it first before saving it to CosmosDB. To do that, I'll extract the data and encrypted AES key from the request body and send both to a decryption helper function I created.

const encText = msgBody.data;
const encAES = msgBody.key;

const decryptedPayload = await aes_decrypt(encAES, encText);

To decrypt the data I sent to the Notecard, I need the private key from the RSA key-pair I generated earlier. Since I am using Azure for this solution, I stored my private key PEM in Azure KeyVault and used the key and cryptography client objects in the Azure NodeJS library to retrieve the key from the vault.

const crypto = require('crypto');
const { DefaultAzureCredential } = require("@azure/identity");
const { KeyClient, CryptographyClient } = require("@azure/keyvault-keys");

const keyVaultName = process.env["KEY_VAULT_NAME"];
const keyName = process.env["KEY_NAME"];

const ENC_ALGORITHM = 'AES-256-CBC';
const IV_LENGTH = 16;

const KVUri = "https://" + keyVaultName + ".vault.azure.net";

const credential = new DefaultAzureCredential();
const client = new KeyClient(KVUri, credential);

Once I have my private key, the process for decrypting my health data is encryption in reverse: Using the built-in Crypto library in Node, I decrypt the random AES key with my private key, then decrypt the Note body using that AES key.

const aes_decrypt = async function (encryptedAES, cipherText) {
  const key = await client.getKey(keyName);
  const cryptographyClient = new CryptographyClient(key, credential);

  // decrypt the random aes with RSA private key (RSA)
  const aes = await cryptographyClient.decrypt({
    algorithm: "RSA1_5",
    ciphertext: Buffer.from(encryptedAES, "base64")
  });

  const text = Buffer.from(cipherText, 'base64');
  const iv = Buffer.alloc(IV_LENGTH, 0);

  // Create a decipher object using the decrypted AES key
  var decipher = crypto.createDecipheriv(ENC_ALGORITHM, aes.result, iv);
  decipher.setAutoPadding(false);
  
  // Decrypt the cipher text using the AES key
  let dec = decipher.update(text, 'base64', 'utf-8');
  dec += decipher.final('utf-8');

  return dec.replace(/[\u0000-\u0010+\f]/gu,"");
}

Once I've decrypted the sensor data, the final steps are: 1) append the event_created timestamp to the body for storage, and 2) save the object to CosmosDB. And just like that, I was successfully transferring sensitive health data from my app to my cloud using encryption, and saving the original data in my own database on the other end.

const jsonPayload = JSON.parse(decryptedPayload);
jsonPayload["event_created"] = msgBody.event_created;

context.bindings.healthDataStorage = jsonPayload;

Build a Command Center Web App With Svelte

The next step was to create a pretty dashboard and command-center application. For this app, I built a simple dashboard using Svelte, Bootstrap, and Nivo. The end-result, depicted below, is a nice live view of my heart rate, the temperature in my office, sound level, and a historical view of the last few dozen heart rate and temp readings. The full source code for my dashboard app is in the GitHub repo for this project.

Send Alerts with Notehub and Twilio

But wait, there's more! Capturing heart rate readings from a BLE sensor and configuring end-to-end encryption were fun exercises, but the real point of this project was to help me find some WFH Zen during my day. To that end, I needed a way to send notifications if my heart rate was too high or it was too noisy in my office. On the firmware side, I added a send_notification function to my CircuitPython application that adds an Alert note to my Notecard.

def send_notification(message):
    req = {"req": "note.add"}
    req["file"] = "sensor_alert.qo"
    req["sync"] = True
    req["body"] = {
        "message": message
    }
    card.Transaction(req)

Then, after I get all of the sensor values, I can check the current values against thresholds I've set and, if they are out of bounds, send the Alert note.

if heart_rate["bpm"] > notify_hr:
  send_notification("Your heart rate is high. Take a few deep breaths, buddy.")

if sound_level > sound_max:
  send_notification("It's pretty loud in there. Maybe stop yelling and you'll feel better.")

On the Notehub side, I followed this guide to configure a Route to Twilio SMS, so that my alerts come through as text messages. Because nothing says WFH Zen like a text telling me to calm down, right?

I'm not stressed, you're stresed!

I'm not stressed, you're stressed!

Use Environment Variables to Configure Alert Thresholds

Having alerts is great, but to really take this project to 11, I decided I wanted to make the alerts configurable, meaning I wanted the ability to adjust the alert thresholds from my web dashboard and have a way for my CircuitPython application to pick up changes without needing to update firmware.

Updating alert thresholds from my web dashboard

Updating alert thresholds from my web dashboard

Thankfully, Blues makes this easy with a featured called Environment Variables. Environment Variables can be set at the device, project or fleet level, and changes propagate to the device upon sync, meaning that I can make changes to these variables using my dashboard app and the CircuitPython application will see these changes via an env.get request after the next sync.

notify_hr = int(get_env_var("notify_hr"))
sound_max = int(get_env_var("sound_max"))

def get_env_var(name):
    req = {"req": "env.get"}
    req["name"] = name
    rsp = card.Transaction(req)
    if "text" in rsp:
        return rsp["text"]
    else:
        return 0

This was a fun project to build, and even though there are a lot of moving pieces from the hardware through firmware, cloud, and a web app, I was able to complete the end-to-end project in just a few weeks of part-time work. If you've not yet, I encourage you to check out the Notecard and see what it's like to prototype without fear using no-fees Cellular IoT.

Have fun, and I can't wait to see what you deploy!

Code - WFH Stress Monitor

A Notecard and Bluetooth Solution for monitoring my stress levels while working from home. Contains firmware, serverless functions, and a web-based dashboard app.

bsatrom / wfh_stress_monitor

A Notecard and BLE Solution for monitoring my stress levels while working from home. — Read More

Latest commit to the master branch on 10-6-2021

Download as zip

Credits

Brandon Satrom

Brandon Satrom

Making & breaking things in the IoT | DevX @blueswireless | Formerly @particle @telerik @microsoft | speaker, maker & optimistic nay-sayer

 

Hackster.io

This content is provided by our content partner Hackster.io, an Avnet developer community for learning, programming, and building hardware. Visit them online for more great content like this.

This article was originally published at Hackster.io. It was added to IoTplaybook or last modified on 11/04/2021.