本网页已闲置超过3分钟,按键盘任意键或点击空白处,即可回到网页

基于蓝牙和蜂窝 IoT 的工作压力监测器

发布时间:2021-08-21
分享到:

基于蓝牙和蜂窝 IoT 的工作压力监测器

发布时间:2021-08-21
分享到:

该项目将介绍如何基于蓝牙和蜂窝的监控系统来管理您的 WFH 压力。

很幸运,在近期由于一些原因,我能够在家工作。在这段时间里,我做过的许多工作都需要出差,但我并不太介意,因为当我回家时,我可以冲早上的咖啡,穿起腰部以上的衣服,然后走进布局合理的家庭办公室,以最高的效率度过一天。

刚开始我很享受这样的方式工作。但大多数时候,我都被Zoom的电话、电子邮件、短信通知以及其他外界因素干扰等不受欢迎的东西包围着。此外,在covid - 19仍然存在的世界里,家庭和工作、生活和事业之间的界线比以往任何时候都更加模糊。渐渐地,这些干扰不仅影响了我的工作时间,还影响了我生活中的时时刻刻。这足以使一个人焦虑到极点!

我不知道大家对于这种情况会怎么解决,但有时当我感到压力、焦虑或不知所措时,我需要外界的提醒来做一两次深呼吸。这就是为什么我建立了工作压力监测(蓝牙和手机物联网解决方案)可以观察我在工作时的的心率,温度,噪音等,通过现场仪表盘我展示对应的数据,并发送我提醒我需要休息,深呼吸,或远离干扰。

在本文中,我将分享我创建的内容并向您展示如何:

  • 借助Adafruit Fee nRF52840 Sense和 CircuitPython,将外部蓝牙传感器添加到任何项目中。
  • 在将数据发送到Notehub.io之前,使用 Blues Wireless Notecard 加密我敏感的健康和环境数据。
  • Notehub.io 中创建路由以将加密数据发送到 Azure Serverless Functions,并向Twilio的 SMS 服务发送警报。
  • 解密健康数据并将其存储在 Azure CosmosDB 数据库中。
  • 构建一个简单的基于 Svelte 的 Web 应用程序,用于实时显示健康数据和警报。
  • 使用环境变量动态更新我办公室的心率、温度和声音级别的警报阈值。

这个项目真的适合每个人!如果您想简要了解该项目,请查看下面的视频:

视频演示:

硬件组件:    

  • 蓝调无线记事卡×1    
  • Blues Wireless Feather 入门套件×1      
  • Adafruit Feather Bluefruit LE 板×1    
  • Polar Verity Sense 心率监测器×1    
  • 源代码

组装硬件
我为这个项目设置的硬件非常简单。我从Polar Verity Sense 心率监测器开始,这是一款简单、无屏幕、支持蓝牙的设备,可捕获心率读数。为了捕捉这些读数,我需要一个支持蓝牙的 MCU,并决定使用Adafruit Feather nRF52840 Sense 。这款精美的小设备不仅具有板载 Nordic nRF52840 的蓝牙功能,而且还包括一个 9-DoF 运动传感器、一个加速度计、磁力计、温度、压力、湿度、接近度、光线、颜色和手势传感器,以及一个PDM 麦克风和声音传感器。最重要的是,它支持 CircuitPython!

最后,对于云连接,我添加了 Blues Wireless Notecard 。如果您还没有听说过 Notecard,它是一款支持蜂窝和 GPS 的设备到云数据泵,以 49 美元的价格提供 500 MB 的数据和 10 年的蜂窝数据。

Notecard 本身是一个带有 m.2 连接器的小型 30 x 35 SoM。为了更轻松地集成到现有原型或项目中,Blues Wireless 提供了称为Notecarriers 的主机板。我在这个项目中使用了 Notecarrier AF,因为它包含一组方便的标头,可与任何 Feather 兼容设备配合使用。

使用 CircuitPython 读取 BLE 心率和环境数据
Sense Feather 包含很多传感器,但对于这个项目,我决定将自己限制在几个选择中:

  • Polar 手环的心率和电池电量;
  • 来自板载 BMP280 的温度和压力;
  • SHT31D 的湿度;
  • 来自 PDM 麦克风的声级

为了使用板载传感器,我首先将每个这些的 CircuitPython 库添加到lib我的设备目录中,import为每个添加了语句,以及note-python 库,它提供了一组简单的 API 用于使用 Notecard .

import adafruit_bmp280
import adafruit_sht31d
import notecard

由于我也在从蓝牙设备读取数据,因此我需要包含用于捕获心率和其他设备信息的adafruit_ble库和相关服务。

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

接下来,我通过 I2C 配置了与我的 Notecard 的连接,初始化了 Feather 上的 BMP280、SHT31D、PDM 麦克风和 BLE 无线电。

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()

在我的程序的主要部分,我从板载传感器读取数据并每 60 秒扫描一次 BLE 心率数据。BLE 部分是最复杂的部分,所以我将它封装在一个辅助函数中,该函数返回dict来自 Polar 传感器的数据,包括制造商、型号、心率和设备的电池电量。

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

当我运行程序时,使用上面未包含的一些日志记录代码,我将在串行终端上看到类似以下内容。

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

使用 Notecard 加密数据
有了读数,我准备将数据发送到 Notecard,但对于这个项目,我决定尝试该产品的一个新功能:端到端数据加密。我的计划是在我的健康数据同步到云之前对其进行加密,然后在它到达我的无服务器功能进行存储时对其进行解密。这样,我就可以在数据不受我直接控制的整个过程中对其进行完整的监管。

正如dev.blues.io 上的本指南中所述,Notecard 支持 Notes 的 AES 256 加密,我能够通过创建 RSA 密钥对并将公钥保存为我的Notehub.io上的环境变量来利用此功能项目。

然后,当通过note.addapi向 Notecard 添加读数时,我使用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)

当 Notecard 看到key参数时,它使用我的公钥生成一个随机的 64 位 AES 密钥,使用该密钥加密 Note 正文,使用 AES 密钥加密 RSA 公钥,对两者进行 base64 编码,并将两个值排队下一次同步的记事卡。

在另一端,我传递给 Notecard 的数据显示在安全加密的Notehub 中

将加密数据路由到 Azure
一旦我将健康日期输入 Notehub,我就准备将读数路由到我的云应用程序以进行解密和存储。使用dev.blues.io 上的本指南作为参考,我创建了一个新的Notehub.io路由,将其指向我为从 Notehub 接收数据而设置的 Azure 函数,并确保同时发送加密数据和JSONata 转换中的加密 AES 密钥。

在 CosmosDB 中解密和存储数据
由于健康数据正在进入我的saveHealthDataAzure 函数,因此我需要先对其进行解密,然后再将其保存到 CosmosDB。为此,我将从请求正文中提取数据和加密的 AES 密钥,并将两者发送到我创建的解密辅助函数。

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

const decryptedPayload = await aes_decrypt(encAES, encText);

要解密我发送到 Notecard 的数据,我需要我之前生成的 RSA 密钥对中的私钥。由于我将 Azure 用于此解决方案,因此我将私钥 PEM 存储在Azure KeyVault 中,并使用Azure NodeJS库中的密钥和加密客户端对象从保管库中检索密钥。

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);

一旦我有了我的私钥,解密我的健康数据的过程就是反向加密:使用 Node 中的内置 Crypto 库,我用我的私钥解密随机 AES 密钥,然后使用该 AES 密钥解密 Note 正文。

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,"");
}

一旦我解密了传感器数据,最后的步骤是:1) 将时间戳附加event_created到正文以进行存储,以及 2) 将对象保存到 CosmosDB。就这样,我成功地使用加密将敏感的健康数据从我的应用程序传输到我的云,并将原始数据保存在我自己的另一端数据库中。

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

context.bindings.healthDataStorage = jsonPayload;

使用 Svelte 构建命令中心 Web 应用程序
下一步是创建一个漂亮的仪表板和命令中心应用程序。对于这个应用程序,我使用Svelte 、Bootstrap 和Nivo构建了一个简单的仪表板。最终结果如下图所示,是我的心率、办公室温度、声级的实时视图,以及最近几十个心率和温度读数的历史视图。我的仪表板应用程序的完整源代码位于此项目GitHub 存储库中。

使用 Notehub 和 Twilio 发送警报
但是等等,还有更多!从 BLE 传感器捕获心率读数并配置端到端加密是一项有趣的练习,但该项目的真正意义在于帮助我在白天找到一些 WFH Zen。为此,如果我的心率过高或办公室太吵,我需要一种方法来发送通知。在固件方面,我向我的 CircuitPython 应用程序添加了一个 send_notification 函数,该函数向我的 Notecard 添加了一个警报注释。

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

然后,在获得所有传感器值后,我可以根据我设置的阈值检查当前值,如果它们超出范围,则发送警报注释。

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.")

在 Notehub 方面,我按照本指南配置了到 Twilio SMS 的路由,以便我的警报以文本消息的形式发送。因为没有什么比文字更加直观了。

使用环境变量配置警报阈值
有警报很棒,但要真正使这个项目达到 完美,我决定让警报可配置,这意味着我希望能够从我的 Web 仪表板调整警报阈值,并让我的 CircuitPython 应用程序能够获取更改无需更新固件。

值得庆幸的是,Blues 通过名为Environment Variables的功能使这变得容易。环境变量可以在设备、项目或车队级别设置,并且更改在同步时传播到设备,这意味着我可以使用我的仪表板应用程序更改这些变量,并且 CircuitPython 应用程序将在下一个之后通过请求看到这些更改env.get同步。

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

这是一个有趣的项目,即使有很多从硬件到固件、云和 Web 应用程序的移动部分,我还是能够在短短几周内完成端到端项目-兼职工作。如果您还没有制作,我鼓励您查看上述内容,看看使用免费蜂窝 IoT 进行原型设计是什么感觉。

代码:WFH 压力监测器

以上就是关于这个项目的全部内容了,欢迎讨论交流。

加入微信技术交流群

技术交流,职业进阶

关注与非网服务号

获取电子工程师福利

加入电路城 QQ 交流群

与技术大牛交朋友

讨论