亲,“电路城”已合并升级到更全、更大、更强的「新与非网」。点击查看「新与非网」

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

基于Blues Wireless Notecard的跌倒检测系统

发布时间:2022-10-25
分享到:

基于Blues Wireless Notecard的跌倒检测系统

发布时间:2022-10-25
分享到:

这是一种专为老年人设计的智能设备,它能探测到跌倒,并通过蜂窝网络发送带有位置信息的紧急警报信息。

概述
在日本,那里65岁及以上的人口估计超过3600万,创历史新高,是全球201个国家和地区中最多的。我们很容易在街上或商店里找到80到90岁以上的老人。近年来,日本与跌倒有关的死亡人数显著增加,其中约80%的人年龄≥65岁。摔倒会导致身体和心理上的创伤,尤其是对老年人来说。

为了提高老年人的生活质量,本项目提出了一种跌倒检测和报警系统的开发。大多数老年人(75岁以上)使用Keitai Denwa,这是一种基本功能的手机,没有任何通讯设备。没有智能手机/智能手表和智能应用程序,很难跟踪他们的活动。在这个项目中,我构建了一个独立的、便携的解决方案,需要一个低维护和低功率的跌倒检测设备来监视他们的活动,以便在紧急情况下及时获得帮助。TensorFlow Lite模型使用Edge Impulse Studio进行训练,并部署到树莓派Pico上,它使用Blues无线Notecard发送注释(消息)。

硬件选择
该项目需要一种低功率、可靠、广泛使用且具有成本效益的蜂窝网络无线电向手机发送警报信息。我将使用一个蓝色无线笔记卡(用于GPS和蜂窝连接)和一个蓝色无线笔记卡a带AA电池套,一个笔记卡的载体板,因为锂聚合物电池可能不是一个安全的选择作为老年人的可穿戴设备。

虽然Notecard可以作为一个独立的设备用于跟踪目的,但我们需要使用Edge Impulse运行模型推理,所以我将使用树莓派Pico作为主机MCU。我使用KiCad设计了一个PCB,它将树莓派Pico集成为一个模块,仅作为nocarrier上的一个组件使用。这个屏蔽(piconokto)被连接到nocarrier上,使它成为一个没有任何突出的电线的小巧便携的设备。piconoto这个名字是pico(微小的)和noto(日语中的便条或信息)两个词的组合。下图中还使用了许多其他组件。

PCB设计
这是我的第一个PCB设计,但结果是令人满意的,工作没有任何明显的缺陷。该设计允许电源输入从主电源nocarrier (VMAIN)或3倍AA电池供应(VBAT)使用焊料跳线焊垫。有一个保护二极管,以确保RPi Pico不能提供电力到nocarrier。该设计包括一个Grove连接器,一个Stemma/Qwiic连接器和两个通孔头封装以连接传感器。有一个焊料跳线垫连接到nocarrier的ATTN引脚或RPi Pico 3V3_EN引脚(启用/禁用电源)或GPIO引脚7(设置中断)。

如果需要的话,我们还可以附加一个按钮和一个LED。大多数的Notecard引脚路由到屏蔽,可以与树莓派Pico一起使用。AUX调试引脚被路由为单独的头引脚,以防我们需要它们来调试Notecard。

三维视图

制作PCB(前/后)

nocarrier和树莓派Pico引脚映射打印在PCB的背面,方便和容易查找。

组装
RPi Pico直接焊接在SMD板上,以保持其安全,并降低整体高度。当RPi Pico连接到PC进行编程或监视串行日志时,肖特基二极管用于防止电流从RPi Pico到nocarrier。红色按钮用于紧急情况下的SOS按钮。当长按超过3秒时,就会添加一条提示,并通过Notehub发送一条短信警告消息到Twilio。

此外,我们需要在noteccarrier ATTN到RPi Pico GP7和noteccarrier VBAT到RPi Pico VSYS之间做一个焊锡桥(下图中红色突出显示)。通过这种方式,RPi Pico从noteccarrier 3x AA电池获得电源,GPIO 7引脚配置为中断引脚,ATTN引脚配置为每当Notecard GPS模块进行位置固定时触发。主机MCU (RPi Pico)存储最近的GPS位置,以跟踪事件的位置。

在附加屏蔽和Grove ADXL345加速度计到noteccarrier与Notecard的最终产品看起来如下所示。

开发环境
我使用Edge Impulse Studio进行特征生成和TensorFlow Lite模型创建和训练。您需要在https://studio.edgeimpulse.com上注册一个免费帐户,并创建一个项目开始。我正在使用macOS进行本地开发工作。

训练数据集
收集各种日常生活活动(ADL)和跌倒的数据是一项费时费力的工作。它需要来自不同年龄段的许多人,需要大量的工时来管理数据集。

幸运的是,有许多高质量的公共数据集可用于类似类型的数据。我使用了SisFall: A Fall and Movement Dataset,这是一个用加速度计获取的Fall和ADL数据集。数据集包含19种adl类型和15种fall类型。它包括38名志愿者的加速和旋转数据,志愿者被分为两组:23名19到30岁的成年人,以及15名60到75岁的老年人。用三个传感器(2个加速度计和1个陀螺仪)在200hz的频率采样下获取数据。对于这个项目,我使用的加速度数据来自其中一个传感器。此外,我使用相同的加速度计ADXL345具有相同的配置,用于数据收集。这些数据集以原始格式提供,可以从下面给出的链接下载。

https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5298771/

数据示例如下所示。只使用前3列,这是来自ADXL345传感器的3轴加速度计数据。

34,-259, -74, -38, 20, -3, 50,-987,-132;
36,-258, -73, -36, 19, -2, 52,-986,-130;
32,-257, -68, -37, 21, -1, 54,-993,-130;
37,-260, -73, -36, 22, -1, 51,-992,-131;
34,-259, -74, -34, 20,  0, 51,-992,-128;
35,-264, -79, -33, 21, -1, 52,-991,-133;
35,-261, -68, -30, 21,  0, 52,-992,-128;
33,-257, -67, -27, 21, -1, 51,-993,-126;
34,-263, -70, -26, 21,  0, 50,-993,-127;
35,-261, -76, -24, 21,  1, 51,-994,-130;
33,-261, -70, -24, 21,  0, 55,-992,-124;
36,-260, -68, -23, 20, -1, 51,-994,-126;
34,-260, -70, -23, 20, -2, 52,-993,-127;
34,-258, -72, -23, 19,  0, 52,-995,-127;
35,-260, -69, -23, 19, -1, 56,-996,-124;
37,-262, -73, -23, 18,  0, 51,-988,-124;
37,-262, -72, -26, 17, -1, 49,-996,-127;
33,-257, -68, -28, 18, -3, 51,-991,-127;
34,-262, -70, -30, 19, -2, 52,-990,-121;
35,-259, -71, -30, 17, -1, 55,-991,-125;
33,-259, -69, -30, 16, -2, 50,-989,-122;
35,-259, -69, -33, 16, -2, 52,-993,-129;
34,-259, -69, -33, 18, -2, 53,-984,-124;

每个3轴加速度计数据(x, y, z)使用以下转换方程转换为重力。

Resolution: 13 (13 bits)
Range: 16 (+-16g)

Acceleration [g]: [ ( 2 * Range ) / ( 2 ^ Resolution ) ] * raw_acceleration

正在向边缘冲动工作室上传数据
我们需要创建一个新项目上传数据到边缘冲动工作室。

此外,我们还需要Edge Impulse Studio项目的HMAC密钥来为数据采集格式生成签名。我们可以复制HMAC键从仪表板>键[选项卡]在边缘脉冲工作室仪表板。

加速度计数据分为ADL和FALL两类,并在上传到Edge Impulse Studio之前转换为m/s^2。我已经编写了一个Python脚本(如下),将原始加速度计数据转换为Edge Impulse工作室生成功能所需的数据采集JSON格式。

import json
import time, hmac, hashlib
import glob
import os
import time

HMAC_KEY = "YOUR_HMAC_KEY"

# Empty signature (all zeros). HS256 gives 32 byte signature, and we encode in hex, so we need 64 characters here
emptySignature = ''.join(['0'] * 64)

def get_x_filename(filename):
    m_codes = ['D01', 'D02', 'D03', 'D04', 'D05', 'D06', 'D07', 'D08', 'D09', 'D10', 'D11', 'D12', 'D13', 'D14', 'D15', 'D16', 'D17', 'D18', 'D19']
f_codes = ['F01', 'F02', 'F03', 'F04', 'F05', 'F06', 'F07', 'F08', 'F09', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15']
    code = filename.split('_')[0]
    label = ''
    if code in m_codes:
        label = 'ADL'
    if code in f_codes:
        label = 'FALL'
    if label == '':
        raise Exception('label not found')
    x_filename = './data/{}.{}.json'.format(label, os.path.splitext(filename)[0])
    return x_filename

if __name__ == "__main__":
    files = glob.glob("SisFall_dataset/*/*.txt")
    CONVERT_G_TO_MS2 = 9.80665
    for index, path in enumerate(files):
        filename = os.path.basename(path)
        values = []
        with open(path) as infile:
        for line in infile:
            line = line.strip()
            if line:
                row = line.replace(" ", "")
                cols = row.split(',')
                ax = ((2 * 16) / (2 ** 13)) * float(cols[0]) * CONVERT_G_TO_MS2
                ay = ((2 * 16) / (2 ** 13)) * float(cols[1]) * CONVERT_G_TO_MS2
                az = ((2 * 16) / (2 ** 13)) * float(cols[2]) * CONVERT_G_TO_MS2
                values.append([ax, ay, az])
        if (len(values) == 0):
            continue
        data = {
            "protected": {
                "ver": "v1",
                "alg": "HS256",
                "iat": time.time() # epoch time, seconds since 1970
             },
             "signature": emptySignature,
             "payload": {
             "device_name": "aa:bb:ee:ee:cc:ff",
             "device_type": "generic",
             "interval_ms": 5,
             "sensors": [
                 { "name": "accX", "units": "m/s2" },
                 { "name": "accY", "units": "m/s2" },
                 { "name": "accZ", "units": "m/s2" }
             ],
             "values": values
             }
        }
        # encode in JSON
        encoded = json.dumps(data)
        # sign message
        signature = hmac.new(bytes(HMAC_KEY, 'utf-8'), msg = encoded.encode('utf-8'), digestmod = hashlib.sha256).hexdigest()
        # set the signature again in the message, and encode again
        data['signature'] = signature
        encoded = json.dumps(data)
        x_filename = get_x_filename(filename)
    with open(x_filename, 'w') as fout:
        fout.write(encoded)

要执行上面的脚本,请将其保存为format.py并运行下面的命令。假设SisFall数据集已经下载到SisFall_dataset目录。

$ mkdir data
$ python3 format.py

转换后的数据采集JSON如下所示。采样速率为200hz,因此将interval_ms设置为5 (ms)。

{
  "protected": {
    "ver": "v1",
    "alg": "HS256",
    "iat": 1646227572.4969049
  },
  "signature": "3a411ca804ff73ed07d41faf7fb16a8174a58a0bef9adc5cee346f0bc3261e90",
  "payload": {
  "device_name": "aa:bb:ee:ee:cc:ff",
  "device_type": "generic",
  "interval_ms": 5,
  "sensors": [
    {
      "name": "accX",
      "units": "m/s2"
    },
    {
      "name": "accY",
      "units": "m/s2"
    },
    {
      "name": "accZ",
      "units": "m/s2"
    }
  ],
  "values": [
      [
        0.0383072265625,
        -9.95987890625,
        -2.3367408203125
      ],
      [
        -0.0383072265625,
        -9.9215716796875,
        -2.4516625
      ],
      [
        -0.1149216796875,
        -9.95987890625,
        -2.375048046875
      ],
      ...
    ]
  }
}

通过Edge Impulse CLI上传数据。请按照说明安装命令行:https://docs.edgeimpulse.com/docs/cli-installation。

上面的脚本会给JSON文件加上标签名的前缀(例如FALL.F10_SA07_R01.json),这样CLI就可以自动推断出标签名。下面的命令用于上传所有JSON文件到训练数据集。

$ edge-impulse-uploader --category training data/*.json

我们本可以使用类别分割来自动将数据分割为训练和测试数据集,但我们需要分割样本,以便将其上载到那里。我们可以在Edge Impulse Studio的数据采集页面中看到上传的数据集。

上传的FALL事件数据在FALL事件之前和之后有混合运动事件,通过分割片段删除。使用ADL类别数据时不做任何修改。

我们可以通过选择每个样本并从下拉菜单中单击Split sample来进行拆分,但这是一项耗时且繁琐的工作。幸运的是,有一个Edge Impulse SDK API可以用于自动化整个过程。经过一些实验,我们选择了4000毫秒的段长度,这是检测跌倒的最佳长度。

import json
import requests
import logging
import threading

API_KEY = "YOUR_API_KEY"
projectId = "YOUR_PROJECT_ID"
headers = {
    "Accept": "application/json",
    "x-api-key": API_KEY
}

def get_sample_len(sampleId):
    url = f'https://studio.edgeimpulse.com/v1/api/{projectId}/raw-data/{sampleId}'
    response = requests.request("GET", url, headers=headers)
    resp = json.loads(response.text)
    return resp['sample']['totalLengthMs']

def get_segments(sampleId):
    url = f'https://studio.edgeimpulse.com/v1/api/{projectId}/raw-data/{sampleId}/find-segments'
    payload = {
        "shiftSegments": False,
        "segmentLengthMs": 4000
    }
    response = requests.request("POST", url, json=payload, headers=headers)
    return json.loads(response.text)["segments"]

def crop_sample(sampleId):
    sample_len = get_sample_len(sampleId)
    cropStart = 200
    cropEnd = int(sample_len/5)
    payload = {"cropStart": cropStart, "cropEnd": cropEnd}
    #print(payload)
    url = f'https://studio.edgeimpulse.com/v1/api/{projectId}/raw-data/{sampleId}/crop'
    response  = requests.request("POST", url, json=payload, headers=headers)
    resp = json.loads(response.text)
    if resp['success']:
        logging.info(f'Crop: {sampleId}')
    else:
        logging.error(f'Crop: {sampleId} {resp["error"]}')
  
def segment(tid, ids):
    for sampleId in ids:
        try:
            crop_sample(sampleId)
            segments = get_segments(sampleId)
            if len(segments) > 0:
                payload = {"segments": segments}
                url = f'https://studio.edgeimpulse.com/v1/api/{projectId}/raw-data/{sampleId}/segment'
                response = requests.request("POST", url, json=payload, headers=headers)
                resp = json.loads(response.text)
                if resp['success']:
                    logging.info(f'Segment: {tid} {sampleId}')
                else:
                    logging.error(f'Segment: {tid} {sampleId} {resp["error"]}')
        except Exception as e:
            logging.error(f'Segment: exception {sampleId}')
            continue

def get_id_list():
    querystring = {"category":"training", "excludeSensors":"true", "labels": '["FALL"]'}
    url = f'https://studio.edgeimpulse.com/v1/api/{projectId}/raw-data'
    response = requests.request("GET", url, headers=headers, params=querystring)
    resp = json.loads(response.text)
    id_list = list(map(lambda s: s["id"], resp["samples"]))
    return id_list

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
    datefmt="%H:%M:%S")
    id_list = get_id_list()
    logging.info('Sample Count: {}'.format(len(id_list)))
    div = 8
    n = int(len(id_list) / div)
    threads = list()
    for i in range(div):
        if i == (div - 1):
            ids = id_list[n*i: ]
        else:
            ids = id_list[n*i: n*(i+1)]
        x = threading.Thread(target=segment, args=(i, ids))
        threads.append(x)
        x.start()
    for thread in threads:
        thread.join()
    logging.info("Finished")

要执行上面的脚本,请将其保存到一个segments.py文件中,并运行下面的命令。

$ python3 segments.py

在分割数据集之后,我们可以通过点击Edge Impulse Studio仪表板上的Perform train / test split按钮将其分割为训练和测试集。

训练
进入脉冲设计>创建脉冲页面,单击添加处理块,然后选择频谱分析,这对于分析重复运动非常有用,例如来自加速度计的数据。另外,在同一页面上,单击Add学习块,并选择Classification (Keras),它可以从数据中学习模式,并将这些模式应用到新数据中。我们选择了4000ms的窗口大小和4000ms的窗口增加,这意味着我们正在使用单帧。现在点击Save Impulse按钮。

接下来进入脉冲设计>光谱分析页面,更改如下图所示的参数,并单击保存参数按钮。

点击Save parameters按钮会重定向到另一个页面,在那里我们应该点击Generate Feature按钮。完成功能生成通常需要几分钟的时间。我们可以在Feature Explorer中看到生成的特征的3D可视化。

现在转到Impulse Design >神经网络(Keras)页面并定义神经网络架构。我已经创建了2个密集(完全连接)层的模型。训练周期数选择为30个。由于ADL和FALL类数据集不是平衡的,因此选择了一个Auto-balance数据集选项,它混合了来自不常见类的更多数据副本,这可能有助于使模型在过拟合时更健壮。

现在点击开始训练按钮,等待几分钟直到训练完成。我们可以看到下面的Training输出。量化(int8)模型具有97.5%的精度。

测试
我们可以通过进入模型测试页面并点击分类所有按钮,在测试数据集上测试模型。该模型对测试数据集具有97.11%的准确性,因此我们有信心该模型能够在真实环境中工作。

部署
Edge Impulse Studio支持树莓派Pico c++ SDK,但布鲁斯无线Notecard还不支持它。幸运的是,两者都支持Arduino库,所以在部署页面中,我们将选择Create Library > Arduino库选项。对于Select optimizations选项,我们将选择Enable EON Compiler,这将减少模型的内存使用。此外,我们将选择量化(Int8)模型。现在单击Build按钮,几秒钟后库包就会被下载到您的本地计算机上。

设置蓝调无线Notecard和Notehub
在开始运行应用程序之前,我们应该设置Notecard。设置一个Notecard与nocarrier AA测试一切都按预期工作。应用程序代码实际上在引导时设置Notecard,以确保它始终处于已知的状态。我们还需要建立Notehub,这是一个云服务,从Notecard接收数据,让我们可以管理设备,并将数据路由到我们自己的云应用程序和服务。我们可以在https://notehub.io/sign-up上创建一个免费的帐户,成功登录后我们可以创建一个新项目。

复制ProjectUID,它被Notehub用来将Notecard与创建的项目关联起来。经过几次Notehub同步之后,我们将能够在Notehub Events页面中看到日志,如下所示。

对于短信提醒,我们需要在Twilio设置一个帐户,并通过单击路由页面右上角的创建路由链接创建一个路由。请遵循由Blues Wireless提供的精心编写的指南中给出的说明,利用通用HTTP/HTTPS请求/响应路由类型调用Twilio API。

在Filters部分,我们必须指定要将哪个Notecard出站文件数据路由到Twilio。它将确保我们总是发送预期的数据。在应用程序代码中,我们将向twilio添加注释。问:文件。

要发送SMS消息,Twilio API要求表单数据具有三个键/值对(Body、From和To)。这可以使用JSONata (JSON数据的查询和转换语言)表达式来实现,将数据格式化为所需的形式。我们应该在Data > Transform字段中选择JSONata Expression,我们可以在文本区域中输入JSONata表达式,如下所示。

完整的JSONata表达式如下所示。该表达式将JSON有效负载格式化为Twilio API可使用的消息格式。该消息是由Fall Detection或Button Press事件生成的。根据位置数据可用性,在消息中包含谷歌地图搜索或方向URL。


    $locations := $filter(body.locations, function($v) {
        when - $v.time > 3600
    });

    $origin := $count($locations) > 0
        ? body.locations[0].lat & "," & body.locations[0].lon: "";

    $waypoints := $count($locations) > 1
        ? $map($locations, function($v, $i) {
       $i > 0 ? $v.lat & ',' & $v.lon : ""
    }) : "";
    $waypoints := $filter($waypoints, function($v) {
        $v != ""
    });

    $destination := (where_lat ? where_lat : tower_lat)  &  ","  & (where_lon ? where_lon : tower_lon);

    $url_search := "https://google.com/maps/search/?api=1";
    $url_direction := "https://google.com/maps/dir/?api=1";

    "&Body=" & $replace(body.event, "_", " ") & " at "
    & ( $origin = "" ? $url_search : $url_direction)
    & ( $origin = "" ? "" : "%26origin=" & $origin)
    & ($count($waypoints) > 0 ? "%26waypoints=" & $join($waypoints, "|") : "")
    & ( $origin = "" ? "%26query=" & $destination : "%26destination=" & $destination)
    & "&From=" & body.from & "&To=" & body.to & "&"
)

应用程序工作流程
这是应用程序工作流的高级概述。

运行的推论
请按照这里的说明下载和安装Arduino IDE。安装完成后,打开Arduino IDE,进入“Tools > board > Boards Manager”安装树莓派Pico的单板包。搜索如下所示的单板包并安装。

安装完成后,在“Tools > board > Raspberry Pi RP2040 boards”菜单中选择“Raspberry Pi Pico”,在“Tools > port”菜单中选择连接的单板的串口。我们需要安装蓝调无线Notecard库使用库管理器(工具>管理库…)作为。所示。

另外,我们需要使用库管理器安装RingBuffer库。

下面是用于推理的Arduino草图。对于连续运动事件检测,应用程序使用两个可用的MCU核心,一个用于推断,另一个用于数据采样,因此没有事件应该错过。

/* Includes ---------------------------------------------------------------- */
#include <Notecard.h>
#include <Wire.h>
#include <RingBuf.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>
#include <Fall_Detection_inferencing.h>

#define serialDebugOut Serial
#define I2C_SDA_PIN  2
#define I2C_SCL_PIN  3
#define ATTN_PIN 7
#define EN_PIN  22
#define LED_PIN 25
#define BTN_PIN 28
#define BTN_LONG_PRESS_MS 3000
#define MY_PRODUCT_ID       "com.xxx.yyy:my_project"
#define FROM_PHONE_NUMBER   "+16xxxxxxxx"
#define TO_PHONE_NUMBER     "+8xxxxxxxxx"
#define N_LOC 5

void btnISR(void);
void attnISR(void);

volatile bool btnInterruptOccurred = false;
volatile bool notecardAttnFired = false;

typedef struct  {
  double lat;
  double lon;
  unsigned long timestamp;
} location_t;

RingBuf<location_t, N_LOC> locations;

// Accelerometer data queue
queue_t sample_queue;
// Init Accelerometer
Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345);
// Notecard instance
Notecard notecard;

// This buffer is filled by the accelerometer data
float signal_buf[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];

int raw_feature_get_data(size_t offset, size_t length, float *out_ptr)
{
  memcpy(out_ptr, signal_buf + offset, length * sizeof(float));
  return 0;
}

// Interrupt Service Routine for BTN_PIN transitions rising from LOW to HIGH
void btnISR()
{
  btnInterruptOccurred = true;
}

void  attnISR() {
  notecardAttnFired = true;
}

void restore_notecard()
{
  J *req = notecard.newRequest("card.restore");
  if (req) {
    JAddBoolToObject(req, "delete", true);
    if (!notecard.sendRequest(req)) {
      notecard.logDebug("ERROR: restore card request\n");
    }
  } else {
    notecard.logDebug("ERROR: Failed to restore card!\n");
  }
}

void setup_notehub()
{
  // Setup Notehub
  J *req = notecard.newRequest("hub.set");
  if (req) {
    JAddStringToObject(req, "product", MY_PRODUCT_ID);
    JAddBoolToObject(req, "sync", true);
    JAddStringToObject(req, "mode", "periodic");
    JAddNumberToObject(req, "outbound", 15); // 15 mins
    JAddNumberToObject(req, "inbound", 60); // 60 mins
    if (!notecard.sendRequest(req)) {
      notecard.logDebug("ERROR: Setup Notehub request\n");
    }
  } else {
    notecard.logDebug("ERROR: Failed to set notehub!\n");
  }
}

void enable_tracking_notecard()
{
  J *req = NoteNewRequest("card.location.mode");
  if (req) {
    JAddStringToObject(req, "mode", "periodic");
    JAddNumberToObject(req, "seconds", 300);
    if (!notecard.sendRequest(req)) {
      notecard.logDebug("ERROR: card.location.mode request\n");
    }

    J *req = notecard.newRequest("card.location.track");
    if (req) {
      JAddBoolToObject(req, "start", true);
      JAddBoolToObject(req, "sync", true);
      JAddBoolToObject(req, "heartbeat", true);
      JAddNumberToObject(req, "hours", 1);
      if (!notecard.sendRequest(req)) {
        notecard.logDebug("ERROR: card.location.track request\n");
      }

      J *req = NoteNewRequest("card.motion.mode");
      if (req) {
        JAddBoolToObject(req, "start", true);
        if (!notecard.sendRequest(req)) {
          notecard.logDebug("ERROR: card.motion.mode request\n");
        }
      }  else {
        notecard.logDebug("ERROR: Failed to set card motion mode!\n");
      }
    } else {
      notecard.logDebug("ERROR: Failed to set location track!\n");
    }
  } else {
    notecard.logDebug("ERROR: Failed to set  location mode!\n");
  }
}

void register_location()
{
  J *rsp = notecard.requestAndResponse(notecard.newRequest("card.location"));

  if (rsp != NULL) {
    location_t location;
    location.lat = JGetNumber(rsp, "lat");
    location.lon = JGetNumber(rsp, "lon");
    location.timestamp = JGetNumber(rsp, "time");
    
    notecard.deleteResponse(rsp);
    notecard.logDebugf("lat=%f,  lon=%f\n", location.lat, location.lon);
    
    if (locations.isFull()) {
      location_t loc;
      locations.pop(loc);
    }
    
    locations.push(location);
  }
}

void arm_attn()
{
  // Arm ATTN Interrupt
  J *req = NoteNewRequest("card.attn");
  if (req) {
    // arm ATTN if not already armed and fire whenever the Notecard GPS module makes a position fix.
    JAddStringToObject(req, "mode", "rearm,location");
    
   // JAddStringToObject(req, "mode", "sleep");
   // JAddNumberToObject(req, "seconds", 120);

    if (notecard.sendRequest(req)) {
      notecard.logDebug("Arm ATTN interrupt enabled!\n");
    } else {
      notecard.logDebug("ERROR: Failed to arm ATTN interrupt!\n");
    }
  }
}

void send_notification(char *event)
{
  // Add a note
  J *req = notecard.newRequest("note.add");
  if (req != NULL) {
    // send immediately
    JAddBoolToObject(req, "sync", true);
    JAddStringToObject(req, "file", "twilio.qo");
    J *body = JCreateObject();
    if (body != NULL) {
      JAddStringToObject(body, "event", event);
      J *arr = JAddArrayToObject(body, "locations");

      for (uint8_t i = 0; i < locations.size(); i++) {
        J *location = JCreateObject();
        if (location != NULL) {
          JAddNumberToObject(location, "lat", locations[i].lat);
          JAddNumberToObject(location, "lon", locations[i].lon);
          JAddNumberToObject(location, "time", locations[i].timestamp);
          JAddItemToObject(arr, "", location);
        }
      }
      JAddStringToObject(body, "from", FROM_PHONE_NUMBER);
      JAddStringToObject(body, "to", TO_PHONE_NUMBER);
      JAddItemToObject(req, "body", body);
    }

    if (!notecard.sendRequest(req)) {
      notecard.logDebug("ERROR: add note request\n");
    }
  }
}


// Running on core0
void setup()
{
  serialDebugOut.begin(115200);
  pinMode(LED_PIN, OUTPUT);

//  while (!serialDebugOut) {
//    delay(250);
//  }

  pinMode(EN_PIN, OUTPUT);
  digitalWrite(EN_PIN, HIGH);
  // Wait 2.5 seconds until Notecard is ready
  sleep_ms(10000);

  digitalWrite(LED_PIN, LOW);

  // Notecard I2C SDA/SCL is attached to RPi Pico GPIO 2/3 which uses i2c1/Wire1 instead of default i2c0/Wire
  Wire1.setSDA(I2C_SDA_PIN);
  Wire1.setSCL(I2C_SCL_PIN);
  Wire1.begin();

  // Attach Button Interrupt
  pinMode(BTN_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(BTN_PIN), btnISR, RISING);

  // Attach Notecard Interrupt
  pinMode(ATTN_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(ATTN_PIN), attnISR, RISING);

  // Initialize Notecard with I2C communication
  notecard.begin(NOTE_I2C_ADDR_DEFAULT, NOTE_I2C_MAX_DEFAULT, Wire1);
  notecard.setDebugOutputStream(serialDebugOut);

  // Restore Notecard 
  //restore_notecard();
  //sleep_ms(100);
  setup_notehub();
  sleep_ms(100);
  // Configure location tracking
  enable_tracking_notecard();
  sleep_ms(100);
  // Arm ATTN
  arm_attn();
  sleep_ms(1000);
}

int sample_start = 0;
int sample_end = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE / 4;
int continous_fall_detected = 0;
uint64_t last_fall_detected_time = 0;

// Running on core0
void loop()
{

  if (notecardAttnFired) {
    notecardAttnFired = false;
    notecard.logDebug("ATTN fired\n");
    // Save location data
    register_location();
    // Re-arm ATTN
    arm_attn();
  }

  if (btnInterruptOccurred) {
    btnInterruptOccurred = false;
    unsigned long int start_time = millis();

    while (digitalRead(BTN_PIN) == HIGH) {
      if (millis() - start_time > BTN_LONG_PRESS_MS) {
        send_notification("BUTTON_PRESSED");
        break;
      }
    }
  }


  ei_impulse_result_t result = { 0 };
  signal_t features_signal;
  features_signal.total_length = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE;
  features_signal.get_data = &raw_feature_get_data;

  // get data from the queue
  for (int i = sample_start; i < sample_end; i++) {
    queue_remove_blocking(&sample_queue, &signal_buf[i]);
  }

  if (sample_end ==  EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
    sample_start = 0;
    sample_end = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE / 4;
  } else {
    sample_start = sample_end;
    sample_end += EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE / 4;
  }

  // invoke the impulse
  EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false);
  if (res == 0) {
    // above 80% confidence score
    if (result.classification[1].value > 0.8f) {
      ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n",
                result.timing.dsp, result.timing.classification, result.timing.anomaly);
      for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        ei_printf("\t%s: %.5f\n", result.classification[ix].label,
                  result.classification[ix].value);
      }

      continous_fall_detected += 1;

      if (continous_fall_detected > 2) {
        send_notification("FALL_DETECTED");
        continous_fall_detected = 0;
        digitalWrite(LED_PIN, HIGH);
      }
    } else {
      continous_fall_detected = 0;

      // turn off the led after 5s since last fall detected
      if (ei_read_timer_ms() - last_fall_detected_time >= 5000) {
        digitalWrite(LED_PIN, LOW);
      }
    }
  }
}

// Running on core1
void setup1()
{
  if (!accel.begin()) {
    /* There was a problem detecting the ADXL345 ... check your connections */
    Serial.println("Ooops, no ADXL345 detected ... Check your wiring!");
    while (1);
  }

  /* Set the range to whatever is appropriate for your project */
  accel.setRange(ADXL345_RANGE_16_G);
  accel.setDataRate(ADXL345_DATARATE_400_HZ);

  // add space for 4 additional samples to avoid blocking main thread
  queue_init(&sample_queue, sizeof(float), EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE + (4 * sizeof(float)));

  // wait 10s until core0 finishes setup and starts fetching accelerometer data
  sleep_ms(10000);
}

uint64_t last_sample_time = 0;

// Running on core1
void loop1()
{
  sensors_event_t event;
  accel.getEvent(&event);
  float acceleration[EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME];

  // read sample every 5000 us = 5ms (= 200 Hz)
  if (ei_read_timer_us() - last_sample_time >= 5000) {
    acceleration[0] = event.acceleration.x;
    acceleration[1] = event.acceleration.y;
    acceleration[2] = event.acceleration.z;

    //ei_printf("%.1f, %.1f, %.1f\n", acceleration[0], acceleration[1], acceleration[2]);

    for (int i = 0; i < EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME; i++) {
      if (queue_try_add(&sample_queue, &acceleration[i]) == false) {
        //ei_printf("Data queue full!\n");
        sleep_ms(100);
        break;
      }
    }
    last_sample_time = ei_read_timer_us();
  }
}


void ei_printf(const char *format, ...) {
  static char print_buf[1024] = { 0 };

  va_list args;
  va_start(args, format);
  int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
  va_end(args);

  if (r > 0) {
    Serial.write(print_buf);
  }
}

要运行推断草图,请使用下面的命令克隆应用程序存储库。

$ git clone https://github.com/metanav/Piconoto_SOS.git

使用菜单Sketch > Include library > Add.ZIP library在Arduino IDE中导入库包Piconoto_SOS/ fall_detection_inference .zip。通过导航菜单File > Examples > Fall_Detection_inferencing > piconoto_fall_detector打开推理草图,并编译/上传固件到连接的RPi Pico板。我们可以使用波特率为115200 bps的工具> Serial monitor来监视推断输出和Notecard调试日志。

套管
为了保护和方便佩戴,该装置被放置在一个袋子内,可以用腰带固定在腰部。此外,袋材料允许GPS和蜂窝信号渗透。

坠落探测警报消息

如果检测到摔倒,我们应该会在5秒内收到如下所示的SMS警报消息。我们可以跟随短信中的链接打开谷歌地图,我们可以知道确切的位置,并快速找到人。由于该设备能够捕获最近的GPS位置,谷歌地图方向URL被JSONata表达式包含在通过Notehub路由到Twilio的消息中。

SOS按钮长按警报信息

在下面的截图中,当GPS无法得到一个固定的位置时,最近的位置无法捕获,所以只有蜂窝塔的位置通过通过Notehub路由到Twilio的JSONata表达式包含在谷歌地图搜索URL中。

结论
这个项目提出了一个概念验证装置,还考虑到了需要让老年人易于使用。该项目还展示了一个简单的神经网络可以用来解决复杂的问题,通过正确的方式进行信号处理,并可以运行在低功率资源受限设备上,具有可靠和经济的位置感知蜂窝网络连接。

如果您对此项目有任何想法、意见或问题,请在下方留言。

以上内容翻译自网络,原作者:Naveen,如涉及侵权,可联系删除。

加入微信技术交流群

技术交流,职业进阶

关注与非网服务号

获取电子工程师福利

加入电路城 QQ 交流群

与技术大牛交朋友

讨论