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!
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.
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 below.
Oh, and you can find the source code for all of the above in my GitHub repo.
Let's get started!
"Assemble" the HardwareMy 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 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.
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 NotecardWith 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.
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.
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 SvelteThe 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.
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?
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.
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!
Comments