Bangle.js2: Switched to proprietary heart rate algorithm (from our Open Source version). It just works better.

This commit is contained in:
Gordon Williams 2023-04-24 14:55:25 +01:00
parent 82abd844c3
commit 21e7dd8061
31 changed files with 295 additions and 52 deletions

View File

@ -14,6 +14,8 @@
Puck.js: Ensure acc/gyro doesn't cause watch events to be added to the input queue (more efficient)
Waveform: Fix waveform input/output when not done at system start - Since 2v13 start time was set in the past
Fix ordering of Pin check in wrapper (so autocomplete and Pin prototype works)
Bangle.js2: Switched to 'hard' float abi from 'soft' (needed for proprietary hrm algorithm)
Bangle.js2: Switched to proprietary heart rate algorithm (from our Open Source version). It just works better.
2v17 : Bangle.js: When reading file info from a filename table, do it in blocks of 8 (20% faster file search)
Bangle.js2: Increase flash buffer size from 16->32 bytes (5% performance increase)

View File

@ -68,8 +68,13 @@ info = {
'WRAPPERSOURCES += libs/graphics/jswrap_font_12x20.c',
'SOURCES += libs/misc/nmea.c',
'SOURCES += libs/misc/stepcount.c',
'SOURCES += libs/misc/heartrate.c',
'SOURCES += libs/misc/hrm_vc31.c',
'SOURCES += libs/misc/hrm_vc31.c',
# Standard open-source heart rate algorithm:
# 'SOURCES += libs/misc/heartrate.c',
# Proprietary heart rate algorithm:
'FLOAT_ABI_HARD=1', # needed for VC31 binary algorithm
'SOURCES += libs/misc/heartrate_vc31_binary.c', 'DEFINES += -DHEARTRATE_VC31_BINARY=1', 'PRECOMPILED_OBJS += libs/misc/vc31_binary/algo.o libs/misc/vc31_binary/modle5_10.o libs/misc/vc31_binary/modle5_11.o libs/misc/vc31_binary/modle5_12.o libs/misc/vc31_binary/modle5_13.o libs/misc/vc31_binary/modle5_14.o libs/misc/vc31_binary/modle5_15.o libs/misc/vc31_binary/modle5_16.o libs/misc/vc31_binary/modle5_17.o libs/misc/vc31_binary/modle5_18.o libs/misc/vc31_binary/modle5_1.o libs/misc/vc31_binary/modle5_2.o libs/misc/vc31_binary/modle5_3.o libs/misc/vc31_binary/modle5_4.o libs/misc/vc31_binary/modle5_5.o libs/misc/vc31_binary/modle5_6.o libs/misc/vc31_binary/modle5_7.o libs/misc/vc31_binary/modle5_8.o libs/misc/vc31_binary/modle5_9.o',
# ------------------------
'SOURCES += libs/misc/unistroke.c',
'WRAPPERSOURCES += libs/misc/jswrap_unistroke.c',
'DEFINES += -DESPR_BANGLE_UNISTROKE=1',

View File

@ -538,9 +538,6 @@ Can be used for housekeeping tasks that don't want to be run during the day.
#define ACCEL_HISTORY_LEN 50 ///< Number of samples of accelerometer history
typedef struct {
short x,y,z;
} Vector3;
// =========================================================================
// DEVICE SPECIFIC CONFIG
@ -794,7 +791,7 @@ bool lcdFadeHandlerActive;
// compass data
Vector3 mag, magmin, magmax;
#endif
/// accelerometer data
/// accelerometer data. 8192 = 1G
Vector3 acc;
/// squared accelerometer magnitude
int accMagSquared;
@ -1489,7 +1486,7 @@ void peripheralPollHandler() {
#ifdef HEARTRATE
static void hrmHandler(int ppgValue) {
if (hrm_new(ppgValue)) {
if (hrm_new(ppgValue, &acc)) {
bangleTasks |= JSBT_HRM_DATA;
// keep track of best HRM sample during this period
if (hrmInfo.confidence >= healthCurrent.bpmConfidence) {
@ -3910,13 +3907,13 @@ bool jswrap_banglejs_idle() {
#ifdef HEARTRATE
if (bangleTasks & JSBT_HRM_INSTANT_DATA) {
JsVar *o = hrm_sensor_getJsVar();
if (o) {
if (o) {
jsvObjectSetChildAndUnLock(o,"raw",jsvNewFromInteger(hrmInfo.raw));
jsvObjectSetChildAndUnLock(o,"filt",jsvNewFromInteger(hrmInfo.filtered));
jsvObjectSetChildAndUnLock(o,"avg",jsvNewFromInteger(hrmInfo.avg));
jsvObjectSetChildAndUnLock(o,"isBeat",jsvNewFromBool(hrmInfo.isBeat));
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromFloat(hrmInfo.bpm10 / 10.0));
jsvObjectSetChildAndUnLock(o,"confidence",jsvNewFromInteger(hrmInfo.confidence));
jsvObjectSetChildAndUnLock(o,"filt",jsvNewFromInteger(hrmInfo.filtered));
jsvObjectSetChildAndUnLock(o,"avg",jsvNewFromInteger(hrmInfo.avg));
hrm_get_hrm_raw_info(o);
jsiQueueObjectCallbacks(bangle, JS_EVENT_PREFIX"HRM-raw", &o, 1);
jsvUnLock(o);
}
@ -3926,16 +3923,7 @@ bool jswrap_banglejs_idle() {
if (o) {
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromInteger(hrmInfo.bpm10 / 10.0));
jsvObjectSetChildAndUnLock(o,"confidence",jsvNewFromInteger(hrmInfo.confidence));
JsVar *a = jsvNewEmptyArray();
if (a) {
int n = hrmInfo.timeIdx;
for (int i=0;i<HRM_HIST_LEN;i++) {
jsvArrayPushAndUnLock(a, jsvNewFromFloat(hrm_time_to_bpm10(hrmInfo.times[n]) / 10.0));
n++;
if (n==HRM_HIST_LEN) n=0;
}
jsvObjectSetChildAndUnLock(o,"history",a);
}
hrm_get_hrm_info(o);
jsiQueueObjectCallbacks(bangle, JS_EVENT_PREFIX"HRM", &o, 1);
jsvUnLock(o);
}

View File

@ -347,7 +347,7 @@ bool hrm_had_beat() {
}
/// Add new heart rate value
bool hrm_new(int hrmValue) {
bool hrm_new(int hrmValue, Vector3 *acc) {
if (hrmValue<HRMVALUE_MIN) hrmValue=HRMVALUE_MIN;
if (hrmValue>HRMVALUE_MAX) hrmValue=HRMVALUE_MAX;
hrmInfo.raw = hrmValue;
@ -379,3 +379,22 @@ bool hrm_new(int hrmValue) {
return hadBeat;
}
// Append extra information to an existing HRM event object
void hrm_get_hrm_info(JsVar *o) {
JsVar *a = jsvNewEmptyArray();
if (a) {
int n = hrmInfo.timeIdx;
for (int i=0;i<HRM_HIST_LEN;i++) {
jsvArrayPushAndUnLock(a, jsvNewFromFloat(hrm_time_to_bpm10(hrmInfo.times[n]) / 10.0));
n++;
if (n==HRM_HIST_LEN) n=0;
}
jsvObjectSetChildAndUnLock(o,"history",a);
}
}
// Append extra information to an existing HRM-raw event object
void hrm_get_hrm_raw_info(JsVar *o) {
jsvObjectSetChildAndUnLock(o,"isBeat",jsvNewFromBool(hrmInfo.isBeat));
}

View File

@ -15,8 +15,10 @@
#include "jsutils.h"
#ifndef HEARTRATE_VC31_BINARY
#define HRM_HIST_LEN 16 // how many BPM values do we keep a history of
#define HRM_MEDIAN_LEN 8 // how many BPM values do we average in our median filter to get a BPM reading
#endif
// Do we use 8 or 16 bits for data storage?
#ifdef HEARTRATE_DEVICE_VC31
@ -30,30 +32,42 @@
#endif
typedef struct {
uint16_t bpm10; // 10x BPM
uint8_t confidence; // 0..100%
HrmValueType raw;
int16_t avg; // average signal value, moving average
int16_t filtered;
#ifndef HEARTRATE_VC31_BINARY
int16_t filtered1; // before filtered
int16_t filtered2; // before filtered1
int16_t avg; // average signal value, moving average
bool wasLow; // has the signal gone below the average? set =false when a beat detected
bool isBeat; // was this sample classified as a detected beat?
JsSysTime lastBeatTime; // timestamp of last heartbeat
uint8_t times[HRM_HIST_LEN]; // times of previous beats, in 1/100th secs
uint8_t timeIdx; // index in times
uint16_t bpm10; // 10x BPM
uint8_t confidence; // 0..100%
#else // HEARTRATE_VC31_BINARY
bool isWorn; // is the bangle being worn? Used to re-init if we go from not worn to worn
JsSysTime lastPPGTime; // timestamp of last PPG sample
/* last values we got from the algo. It doesn't tell us when there's a new
value so we have to guess based on when it changes */
int lastHRM, lastConfidence;
int msSinceLastHRM; // how long was it since the last HRM reading?
#endif
} HrmInfo;
extern HrmInfo hrmInfo;
uint16_t hrm_time_to_bpm10(uint8_t time);
/// Initialise heart rate monitoring
void hrm_init();
/// Add new heart rate value, return true if there was a heart beat
bool hrm_new(int hrmValue);
bool hrm_new(int hrmValue, Vector3 *acc);
void hrm_sensor_on();
void hrm_sensor_off();
// Append extra information to an existing HRM event object
void hrm_get_hrm_info(JsVar *var);
// Append extra information to an existing HRM-raw event object
void hrm_get_hrm_raw_info(JsVar *var);

View File

@ -0,0 +1,89 @@
/*
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
*
* Copyright (C) 2021 Gordon Williams <gw@pur3.co.uk>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* heart rate monitoring using VC31 proprietary binary blob
* ----------------------------------------------------------------------------
*/
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include "heartrate.h"
#include "hrm_vc31.h"
#include "hrm.h"
#include "jshardware.h"
#include "jsinteractive.h"
#include "vc31_binary/algo.h"
HrmInfo hrmInfo;
/// Initialise heart rate monitoring
void hrm_init() {
memset(&hrmInfo, 0, sizeof(hrmInfo));
hrmInfo.isWorn = false;
hrmInfo.lastPPGTime = jshGetSystemTime();
}
/// Add new heart rate value
bool hrm_new(int ppgValue, Vector3 *acc) {
// work out time passed since last sample (it might not be exactly hrmUpdateInterval)
JsSysTime time = jshGetSystemTime();
int timeDiff = (int)(jshGetMillisecondsFromTime(time-hrmInfo.lastPPGTime)+0.5);
hrmInfo.lastPPGTime = time;
// if we've just started wearing again, reset the algorith
if (vcInfo.isWearing && !hrmInfo.isWorn) {
hrmInfo.isWorn = true;
// initialise VC31 algorithm (should do when going from wearing to not wearing)
Algo_Init();
hrmInfo.lastHRM = 0;
hrmInfo.lastConfidence = 0;
hrmInfo.msSinceLastHRM = 0;
hrmInfo.avg = ppgValue;
} else {
hrmInfo.isWorn = vcInfo.isWearing;
if (!hrmInfo.isWorn) return false;
}
hrmInfo.filtered = ppgValue-hrmInfo.avg;
hrmInfo.avg = (hrmInfo.avg*15 + ppgValue) >> 4;
// Feed data into algorithm
AlgoInputData_t inputData;
inputData.axes.x = acc->y >> 5; // perpendicular to the direction of the arm
inputData.axes.y = acc->x >> 5; // along the direction of the arm
inputData.axes.z = acc->z >> 5;
inputData.ppgSample = vcInfo.ppgValue | (vcInfo.wasAdjusted ? 0x1000 : 0);
inputData.envSample = vcInfo.envValue;
hrmInfo.msSinceLastHRM += timeDiff;
Algo_Input(&inputData, timeDiff, SPORT_TYPE_NORMAL,0/*surfaceRecogMode*/,0/*opticalAidMode*/);
AlgoOutputData_t outputData;
Algo_Output(&outputData);
jsiConsolePrintf("HRM %d %d %d\n", outputData.hrData, outputData.reliability, hrmInfo.msSinceLastHRM);
if (outputData.hrData!=hrmInfo.lastHRM ||
outputData.reliability!=hrmInfo.lastConfidence ||
((hrmInfo.msSinceLastHRM > 2000) && outputData.hrData && outputData.reliability)) {
// update when figures change OR when 2 secs have passed (readings are usually every 1 sec)
hrmInfo.lastConfidence = outputData.reliability;
hrmInfo.lastHRM = outputData.hrData;
hrmInfo.bpm10 = 10 * outputData.hrData;
hrmInfo.confidence = outputData.reliability;
hrmInfo.msSinceLastHRM = 0;
return true;
}
return false;
}
// Append extra information to an existing HRM event object
void hrm_get_hrm_info(JsVar *o) {
NOT_USED(o);
}
// Append extra information to an existing HRM-raw event object
void hrm_get_hrm_raw_info(JsVar *o) {
}

View File

@ -13,6 +13,7 @@
*/
#include "hrm.h"
#include "hrm_vc31.h"
#include "jsutils.h"
#include "platform_config.h"
#include "jshardware.h"
@ -164,25 +165,6 @@ typedef enum {
VC31B_DEVICE
} VC31Type;
// Hack to fix Eclipse syntax lint
#ifndef PACKED_FLAGS
#define PACKED_FLAGS
#endif
// ---
// VC31 info shared between VC31A/B
typedef struct {
bool isWearing;
int8_t isWearCnt, unWearCnt; // counters for switching worn/unworn state
uint16_t ppgValue; // current PPG value
uint16_t ppgLastValue; // last PPG value
int16_t ppgOffset; // PPG 'offset' value. When PPG adjusts we change the offset so it matches the last value, then slowly adjust 'ppgOffset' back down to 0
uint8_t wasAdjusted; // true if LED/etc adjusted since the last reading
// the meaning of these is device-dependent but it's nice to have them in one place
uint8_t irqStatus;
uint8_t raw[12];
} PACKED_FLAGS VC31Info;
// VC31A-specific info
typedef struct {
uint8_t ctrl; // current VC31A_CTRL reg value
@ -191,7 +173,6 @@ typedef struct {
uint16_t envValue;
uint16_t psValue;
VC31AdjustInfo_t adjustInfo;
} PACKED_FLAGS VC31AInfo;
// VC31B-specific info
typedef struct {
@ -591,6 +572,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
vcaInfo.preValue = (buf[6] << 8) | buf[5];
vcaInfo.psValue = (buf[8] << 8) | buf[7];
vcaInfo.envValue = (buf[10] << 8) | buf[9];
vcInfo.envValue = vcaInfo.envValue;
if (vcInfo.irqStatus & VC31A_STATUS_D_PPG_OK) {
vc31_new_ppg(ppgValue); // send PPG value
@ -614,7 +596,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
vcbInfo.sampleData.envValue[1] = buf[4] >> 4;
vcbInfo.sampleData.preValue[1] = buf[4] & 0x0F;
vcbInfo.sampleData.envValue[2] = buf[5] >> 4;
vcbInfo.sampleData.psValue = buf[5] & 0x0F;
vcbInfo.sampleData.psValue = buf[5] & 0x0F;
buf = &vcInfo.raw[6];
vc31_rx(VC31B_REG17, buf, 6);
vcbInfo.sampleData.pdResValue[0] = (buf[3] >> 4) & 0x07;
@ -626,6 +608,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
// if we had environment sensing, check for wear status and update LEDs accordingly
if (vcInfo.irqStatus & VC31B_INT_PS) {
vcInfo.envValue = vcbInfo.sampleData.envValue[2];
//jsiConsolePrintf("e %d %d\n", vcbInfo.sampleData.psValue, vcbInfo.sampleData.envValue[2] );
vc31b_wearstatus();
}

39
libs/misc/hrm_vc31.h Normal file
View File

@ -0,0 +1,39 @@
/*
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
*
* Copyright (C) 2019 Gordon Williams <gw@pur3.co.uk>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* VC31 heart rate sensor - additional info
* ----------------------------------------------------------------------------
*/
#ifndef EMULATED
// Hack to fix Eclipse syntax lint
#ifndef PACKED_FLAGS
#define PACKED_FLAGS
#endif
// ---
// VC31 info shared between VC31A/B
typedef struct {
bool isWearing;
int8_t isWearCnt, unWearCnt; // counters for switching worn/unworn state
uint16_t ppgValue; // current PPG value
uint16_t ppgLastValue; // last PPG value
int16_t ppgOffset; // PPG 'offset' value. When PPG adjusts we change the offset so it matches the last value, then slowly adjust 'ppgOffset' back down to 0
uint8_t wasAdjusted; // true if LED/etc adjusted since the last reading
uint16_t envValue; // env value (but VC31B has 3 value slots so we just use the one we know is ok here)
// the meaning of these is device-dependent but it's nice to have them in one place
uint8_t irqStatus;
uint8_t raw[12];
} PACKED_FLAGS VC31Info;
extern VC31Info vcInfo;
#endif

View File

@ -0,0 +1,24 @@
Proprietary VC31 HRM algorithm
===============================
In this directory are the object files and header for Vcare's heart rate algorithm.
These were created by VCare (the VC31 manufacturer) and are not part of the MPLv2 license that covers the rest of the Espruino interpreter.
Usage
-----
Instead of the code below in the `BOARD.py` file that enables the open source heart rate algorithm:
```
'SOURCES += libs/misc/heartrate.c',
```
you'll need:
```
'SOURCES += libs/misc/heartrate_vc31_binary.c',
'DEFINES += -DHEARTRATE_VC31_BINARY=1',
'PRECOMPILED_OBJS += libs/misc/vc31_binary/algo.o libs/misc/vc31_binary/modle5_10.o libs/misc/vc31_binary/modle5_11.o libs/misc/vc31_binary/modle5_12.o libs/misc/vc31_binary/modle5_13.o libs/misc/vc31_binary/modle5_14.o libs/misc/vc31_binary/modle5_15.o libs/misc/vc31_binary/modle5_16.o libs/misc/vc31_binary/modle5_17.o libs/misc/vc31_binary/modle5_18.o libs/misc/vc31_binary/modle5_1.o libs/misc/vc31_binary/modle5_2.o libs/misc/vc31_binary/modle5_3.o libs/misc/vc31_binary/modle5_4.o libs/misc/vc31_binary/modle5_5.o libs/misc/vc31_binary/modle5_6.o libs/misc/vc31_binary/modle5_7.o libs/misc/vc31_binary/modle5_8.o libs/misc/vc31_binary/modle5_9.o',
```

View File

@ -0,0 +1,70 @@
#ifndef APP_ALGO_ALGO_H
#define APP_ALGO_ALGO_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
SPORT_TYPE_NORMAL =0x00, // 日常
SPORT_TYPE_RUNNING =0x01, // 跑步
SPORT_TYPE_RIDE_BIKE =0X02, // 骑行
SPORT_TYPE_JUMP_ROPE =0X03, // 跳绳
SPORT_TYPE_SWIMMING =0X04, // 游泳
SPORT_TYPE_BADMINTON =0X05, // 羽毛球
SPORT_TYPE_TABLE_TENNIS=0X06, // 乒乓球
SPORT_TYPE_TENNIS =0X07, // 网球
SPORT_TYPE_CLIMBING =0X08, // 爬山
SPORT_TYPE_WALKING =0X09, // 徒步
SPORT_TYPE_BASKETBALL =0X0A, // 篮球
SPORT_TYPE_FOOTBALL =0X0B, // 足球
SPORT_TYPE_BASEBALL =0X0C, // 棒球
SPORT_TYPE_VOLLEYBALL =0X0D, // 排球
SPORT_TYPE_CRICKET = 0X0E, // 板球
SPORT_TYPE_RUGBY = 0X0F, // 橄榄球
SPORT_TYPE_HOCKEY =0X10, // 曲棍球
SPORT_TYPE_DANCE = 0X11, // 跳舞
SPORT_TYPE_SPINNING = 0X12, // 动感单车
SPORT_TYPE_YOGA = 0X13, // 瑜伽
SPORT_TYPE_SIT_UP =0X14, // 仰卧起坐
SPORT_TYPE_TREADMILL =0X15, // 跑步机
SPORT_TYPE_GYMNASTICS =0X16, // 体操
SPORT_TYPE_BOATING = 0X17, // 划船
SPORT_TYPE_JUMPING_JACK = 0X18, // 开合跳
SPORT_TYPE_FREE_TRAINING = 0X19,// 自由训练
} AlgoSportMode;
//未出现在上表中的运动形式均先以跑步输入
typedef struct
{
int32_t x;
int32_t y;
int32_t z;
}AlgoAxesData_t;
typedef struct
{
int32_t hrData;
int32_t reliability;
}AlgoOutputData_t;
typedef struct
{
AlgoAxesData_t axes;
int32_t ppgSample;
int32_t envSample;
}AlgoInputData_t;
void Algo_Init(void);
void Algo_Input(AlgoInputData_t *pInputData,int32_t gaptime,AlgoSportMode sportMode,int16_t surfaceRecogMode,int16_t opticalAidMode);
void Algo_Output(AlgoOutputData_t *pOutputData);
void Algo_Version(char *pVersionOutput);
#endif /* APP_ALGO_ALGO_H_ */

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -77,14 +77,20 @@ endif
endif
# ARCHFLAGS are shared by both CFLAGS and LDFLAGS.
ARCHFLAGS = -mcpu=cortex-m4 -mthumb -mabi=aapcs -mfloat-abi=softfp -mfpu=fpv4-sp-d16
ARCHFLAGS += -mcpu=cortex-m4 -mthumb -mabi=aapcs
ifdef FLOAT_ABI_HARD
ARCHFLAGS += -mfloat-abi=hard -mfpu=fpv4-sp-d16
DEFINES += -DFLOAT_ABI_HARD
else
ARCHFLAGS += -mfloat-abi=softfp -mfpu=fpv4-sp-d16
endif
# nRF52 specific.
INCLUDE += -I$(SOFTDEVICE_PATH)/headers
INCLUDE += -I$(SOFTDEVICE_PATH)/headers/nrf52
DEFINES += -DBLE_STACK_SUPPORT_REQD
DEFINES += -DSWI_DISABLE0 -DSOFTDEVICE_PRESENT -DFLOAT_ABI_HARD
DEFINES += -DSWI_DISABLE0 -DSOFTDEVICE_PRESENT
DEFINES += -DNRF52_SERIES
# Nordic screwed over anyone who used -DNRF52 in new SDK versions
# but then old SDKs won't work without it

View File

@ -620,4 +620,8 @@ size_t jsuGetFreeStack();
void *espruino_stackHighPtr; //Used by jsuGetFreeStack
#endif
typedef struct {
short x,y,z;
} Vector3;
#endif /* JSUTILS_H_ */