Skip to content

Commit 21e7dd8

Browse files
committed
Bangle.js2: Switched to proprietary heart rate algorithm (from our Open Source version). It just works better.
1 parent 82abd84 commit 21e7dd8

31 files changed

+295
-52
lines changed

ChangeLog

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
Puck.js: Ensure acc/gyro doesn't cause watch events to be added to the input queue (more efficient)
1515
Waveform: Fix waveform input/output when not done at system start - Since 2v13 start time was set in the past
1616
Fix ordering of Pin check in wrapper (so autocomplete and Pin prototype works)
17+
Bangle.js2: Switched to 'hard' float abi from 'soft' (needed for proprietary hrm algorithm)
18+
Bangle.js2: Switched to proprietary heart rate algorithm (from our Open Source version). It just works better.
1719

1820
2v17 : Bangle.js: When reading file info from a filename table, do it in blocks of 8 (20% faster file search)
1921
Bangle.js2: Increase flash buffer size from 16->32 bytes (5% performance increase)

boards/BANGLEJS2.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,13 @@
6868
'WRAPPERSOURCES += libs/graphics/jswrap_font_12x20.c',
6969
'SOURCES += libs/misc/nmea.c',
7070
'SOURCES += libs/misc/stepcount.c',
71-
'SOURCES += libs/misc/heartrate.c',
72-
'SOURCES += libs/misc/hrm_vc31.c',
71+
'SOURCES += libs/misc/hrm_vc31.c',
72+
# Standard open-source heart rate algorithm:
73+
# 'SOURCES += libs/misc/heartrate.c',
74+
# Proprietary heart rate algorithm:
75+
'FLOAT_ABI_HARD=1', # needed for VC31 binary algorithm
76+
'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',
77+
# ------------------------
7378
'SOURCES += libs/misc/unistroke.c',
7479
'WRAPPERSOURCES += libs/misc/jswrap_unistroke.c',
7580
'DEFINES += -DESPR_BANGLE_UNISTROKE=1',

libs/banglejs/jswrap_bangle.c

+7-19
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,6 @@ Can be used for housekeeping tasks that don't want to be run during the day.
538538

539539
#define ACCEL_HISTORY_LEN 50 ///< Number of samples of accelerometer history
540540

541-
typedef struct {
542-
short x,y,z;
543-
} Vector3;
544541

545542
// =========================================================================
546543
// DEVICE SPECIFIC CONFIG
@@ -794,7 +791,7 @@ bool lcdFadeHandlerActive;
794791
// compass data
795792
Vector3 mag, magmin, magmax;
796793
#endif
797-
/// accelerometer data
794+
/// accelerometer data. 8192 = 1G
798795
Vector3 acc;
799796
/// squared accelerometer magnitude
800797
int accMagSquared;
@@ -1489,7 +1486,7 @@ void peripheralPollHandler() {
14891486

14901487
#ifdef HEARTRATE
14911488
static void hrmHandler(int ppgValue) {
1492-
if (hrm_new(ppgValue)) {
1489+
if (hrm_new(ppgValue, &acc)) {
14931490
bangleTasks |= JSBT_HRM_DATA;
14941491
// keep track of best HRM sample during this period
14951492
if (hrmInfo.confidence >= healthCurrent.bpmConfidence) {
@@ -3910,13 +3907,13 @@ bool jswrap_banglejs_idle() {
39103907
#ifdef HEARTRATE
39113908
if (bangleTasks & JSBT_HRM_INSTANT_DATA) {
39123909
JsVar *o = hrm_sensor_getJsVar();
3913-
if (o) {
3910+
if (o) {
39143911
jsvObjectSetChildAndUnLock(o,"raw",jsvNewFromInteger(hrmInfo.raw));
3915-
jsvObjectSetChildAndUnLock(o,"filt",jsvNewFromInteger(hrmInfo.filtered));
3916-
jsvObjectSetChildAndUnLock(o,"avg",jsvNewFromInteger(hrmInfo.avg));
3917-
jsvObjectSetChildAndUnLock(o,"isBeat",jsvNewFromBool(hrmInfo.isBeat));
39183912
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromFloat(hrmInfo.bpm10 / 10.0));
39193913
jsvObjectSetChildAndUnLock(o,"confidence",jsvNewFromInteger(hrmInfo.confidence));
3914+
jsvObjectSetChildAndUnLock(o,"filt",jsvNewFromInteger(hrmInfo.filtered));
3915+
jsvObjectSetChildAndUnLock(o,"avg",jsvNewFromInteger(hrmInfo.avg));
3916+
hrm_get_hrm_raw_info(o);
39203917
jsiQueueObjectCallbacks(bangle, JS_EVENT_PREFIX"HRM-raw", &o, 1);
39213918
jsvUnLock(o);
39223919
}
@@ -3926,16 +3923,7 @@ bool jswrap_banglejs_idle() {
39263923
if (o) {
39273924
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromInteger(hrmInfo.bpm10 / 10.0));
39283925
jsvObjectSetChildAndUnLock(o,"confidence",jsvNewFromInteger(hrmInfo.confidence));
3929-
JsVar *a = jsvNewEmptyArray();
3930-
if (a) {
3931-
int n = hrmInfo.timeIdx;
3932-
for (int i=0;i<HRM_HIST_LEN;i++) {
3933-
jsvArrayPushAndUnLock(a, jsvNewFromFloat(hrm_time_to_bpm10(hrmInfo.times[n]) / 10.0));
3934-
n++;
3935-
if (n==HRM_HIST_LEN) n=0;
3936-
}
3937-
jsvObjectSetChildAndUnLock(o,"history",a);
3938-
}
3926+
hrm_get_hrm_info(o);
39393927
jsiQueueObjectCallbacks(bangle, JS_EVENT_PREFIX"HRM", &o, 1);
39403928
jsvUnLock(o);
39413929
}

libs/misc/heartrate.c

+20-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ bool hrm_had_beat() {
347347
}
348348

349349
/// Add new heart rate value
350-
bool hrm_new(int hrmValue) {
350+
bool hrm_new(int hrmValue, Vector3 *acc) {
351351
if (hrmValue<HRMVALUE_MIN) hrmValue=HRMVALUE_MIN;
352352
if (hrmValue>HRMVALUE_MAX) hrmValue=HRMVALUE_MAX;
353353
hrmInfo.raw = hrmValue;
@@ -379,3 +379,22 @@ bool hrm_new(int hrmValue) {
379379

380380
return hadBeat;
381381
}
382+
383+
// Append extra information to an existing HRM event object
384+
void hrm_get_hrm_info(JsVar *o) {
385+
JsVar *a = jsvNewEmptyArray();
386+
if (a) {
387+
int n = hrmInfo.timeIdx;
388+
for (int i=0;i<HRM_HIST_LEN;i++) {
389+
jsvArrayPushAndUnLock(a, jsvNewFromFloat(hrm_time_to_bpm10(hrmInfo.times[n]) / 10.0));
390+
n++;
391+
if (n==HRM_HIST_LEN) n=0;
392+
}
393+
jsvObjectSetChildAndUnLock(o,"history",a);
394+
}
395+
}
396+
397+
// Append extra information to an existing HRM-raw event object
398+
void hrm_get_hrm_raw_info(JsVar *o) {
399+
jsvObjectSetChildAndUnLock(o,"isBeat",jsvNewFromBool(hrmInfo.isBeat));
400+
}

libs/misc/heartrate.h

+21-7
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
#include "jsutils.h"
1717

18+
#ifndef HEARTRATE_VC31_BINARY
1819
#define HRM_HIST_LEN 16 // how many BPM values do we keep a history of
1920
#define HRM_MEDIAN_LEN 8 // how many BPM values do we average in our median filter to get a BPM reading
21+
#endif
2022

2123
// Do we use 8 or 16 bits for data storage?
2224
#ifdef HEARTRATE_DEVICE_VC31
@@ -30,30 +32,42 @@
3032
#endif
3133

3234
typedef struct {
35+
uint16_t bpm10; // 10x BPM
36+
uint8_t confidence; // 0..100%
37+
3338
HrmValueType raw;
39+
int16_t avg; // average signal value, moving average
3440
int16_t filtered;
41+
#ifndef HEARTRATE_VC31_BINARY
3542
int16_t filtered1; // before filtered
3643
int16_t filtered2; // before filtered1
37-
int16_t avg; // average signal value, moving average
38-
3944
bool wasLow; // has the signal gone below the average? set =false when a beat detected
4045
bool isBeat; // was this sample classified as a detected beat?
4146
JsSysTime lastBeatTime; // timestamp of last heartbeat
4247
uint8_t times[HRM_HIST_LEN]; // times of previous beats, in 1/100th secs
4348
uint8_t timeIdx; // index in times
44-
uint16_t bpm10; // 10x BPM
45-
uint8_t confidence; // 0..100%
49+
#else // HEARTRATE_VC31_BINARY
50+
bool isWorn; // is the bangle being worn? Used to re-init if we go from not worn to worn
51+
JsSysTime lastPPGTime; // timestamp of last PPG sample
52+
/* last values we got from the algo. It doesn't tell us when there's a new
53+
value so we have to guess based on when it changes */
54+
int lastHRM, lastConfidence;
55+
int msSinceLastHRM; // how long was it since the last HRM reading?
56+
#endif
4657
} HrmInfo;
4758

4859
extern HrmInfo hrmInfo;
4960

50-
uint16_t hrm_time_to_bpm10(uint8_t time);
51-
5261
/// Initialise heart rate monitoring
5362
void hrm_init();
5463

5564
/// Add new heart rate value, return true if there was a heart beat
56-
bool hrm_new(int hrmValue);
65+
bool hrm_new(int hrmValue, Vector3 *acc);
5766

5867
void hrm_sensor_on();
5968
void hrm_sensor_off();
69+
70+
// Append extra information to an existing HRM event object
71+
void hrm_get_hrm_info(JsVar *var);
72+
// Append extra information to an existing HRM-raw event object
73+
void hrm_get_hrm_raw_info(JsVar *var);

libs/misc/heartrate_vc31_binary.c

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
3+
*
4+
* Copyright (C) 2021 Gordon Williams <gw@pur3.co.uk>
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public
7+
* License, v. 2.0. If a copy of the MPL was not distributed with this
8+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*
10+
* ----------------------------------------------------------------------------
11+
* heart rate monitoring using VC31 proprietary binary blob
12+
* ----------------------------------------------------------------------------
13+
*/
14+
15+
#include <string.h>
16+
#include <stdlib.h>
17+
#include <stdint.h>
18+
#include <stdbool.h>
19+
#include "heartrate.h"
20+
#include "hrm_vc31.h"
21+
#include "hrm.h"
22+
#include "jshardware.h"
23+
#include "jsinteractive.h"
24+
#include "vc31_binary/algo.h"
25+
26+
HrmInfo hrmInfo;
27+
28+
/// Initialise heart rate monitoring
29+
void hrm_init() {
30+
memset(&hrmInfo, 0, sizeof(hrmInfo));
31+
hrmInfo.isWorn = false;
32+
hrmInfo.lastPPGTime = jshGetSystemTime();
33+
}
34+
35+
/// Add new heart rate value
36+
bool hrm_new(int ppgValue, Vector3 *acc) {
37+
// work out time passed since last sample (it might not be exactly hrmUpdateInterval)
38+
JsSysTime time = jshGetSystemTime();
39+
int timeDiff = (int)(jshGetMillisecondsFromTime(time-hrmInfo.lastPPGTime)+0.5);
40+
hrmInfo.lastPPGTime = time;
41+
// if we've just started wearing again, reset the algorith
42+
if (vcInfo.isWearing && !hrmInfo.isWorn) {
43+
hrmInfo.isWorn = true;
44+
// initialise VC31 algorithm (should do when going from wearing to not wearing)
45+
Algo_Init();
46+
hrmInfo.lastHRM = 0;
47+
hrmInfo.lastConfidence = 0;
48+
hrmInfo.msSinceLastHRM = 0;
49+
hrmInfo.avg = ppgValue;
50+
} else {
51+
hrmInfo.isWorn = vcInfo.isWearing;
52+
if (!hrmInfo.isWorn) return false;
53+
}
54+
hrmInfo.filtered = ppgValue-hrmInfo.avg;
55+
hrmInfo.avg = (hrmInfo.avg*15 + ppgValue) >> 4;
56+
// Feed data into algorithm
57+
AlgoInputData_t inputData;
58+
inputData.axes.x = acc->y >> 5; // perpendicular to the direction of the arm
59+
inputData.axes.y = acc->x >> 5; // along the direction of the arm
60+
inputData.axes.z = acc->z >> 5;
61+
inputData.ppgSample = vcInfo.ppgValue | (vcInfo.wasAdjusted ? 0x1000 : 0);
62+
inputData.envSample = vcInfo.envValue;
63+
hrmInfo.msSinceLastHRM += timeDiff;
64+
Algo_Input(&inputData, timeDiff, SPORT_TYPE_NORMAL,0/*surfaceRecogMode*/,0/*opticalAidMode*/);
65+
AlgoOutputData_t outputData;
66+
Algo_Output(&outputData);
67+
jsiConsolePrintf("HRM %d %d %d\n", outputData.hrData, outputData.reliability, hrmInfo.msSinceLastHRM);
68+
if (outputData.hrData!=hrmInfo.lastHRM ||
69+
outputData.reliability!=hrmInfo.lastConfidence ||
70+
((hrmInfo.msSinceLastHRM > 2000) && outputData.hrData && outputData.reliability)) {
71+
// update when figures change OR when 2 secs have passed (readings are usually every 1 sec)
72+
hrmInfo.lastConfidence = outputData.reliability;
73+
hrmInfo.lastHRM = outputData.hrData;
74+
hrmInfo.bpm10 = 10 * outputData.hrData;
75+
hrmInfo.confidence = outputData.reliability;
76+
hrmInfo.msSinceLastHRM = 0;
77+
return true;
78+
}
79+
return false;
80+
}
81+
82+
// Append extra information to an existing HRM event object
83+
void hrm_get_hrm_info(JsVar *o) {
84+
NOT_USED(o);
85+
}
86+
87+
// Append extra information to an existing HRM-raw event object
88+
void hrm_get_hrm_raw_info(JsVar *o) {
89+
}

libs/misc/hrm_vc31.c

+4-21
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
#include "hrm.h"
16+
#include "hrm_vc31.h"
1617
#include "jsutils.h"
1718
#include "platform_config.h"
1819
#include "jshardware.h"
@@ -164,25 +165,6 @@ typedef enum {
164165
VC31B_DEVICE
165166
} VC31Type;
166167

167-
// Hack to fix Eclipse syntax lint
168-
#ifndef PACKED_FLAGS
169-
#define PACKED_FLAGS
170-
#endif
171-
// ---
172-
173-
// VC31 info shared between VC31A/B
174-
typedef struct {
175-
bool isWearing;
176-
int8_t isWearCnt, unWearCnt; // counters for switching worn/unworn state
177-
uint16_t ppgValue; // current PPG value
178-
uint16_t ppgLastValue; // last PPG value
179-
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
180-
uint8_t wasAdjusted; // true if LED/etc adjusted since the last reading
181-
// the meaning of these is device-dependent but it's nice to have them in one place
182-
uint8_t irqStatus;
183-
uint8_t raw[12];
184-
} PACKED_FLAGS VC31Info;
185-
186168
// VC31A-specific info
187169
typedef struct {
188170
uint8_t ctrl; // current VC31A_CTRL reg value
@@ -191,7 +173,6 @@ typedef struct {
191173
uint16_t envValue;
192174
uint16_t psValue;
193175
VC31AdjustInfo_t adjustInfo;
194-
195176
} PACKED_FLAGS VC31AInfo;
196177
// VC31B-specific info
197178
typedef struct {
@@ -591,6 +572,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
591572
vcaInfo.preValue = (buf[6] << 8) | buf[5];
592573
vcaInfo.psValue = (buf[8] << 8) | buf[7];
593574
vcaInfo.envValue = (buf[10] << 8) | buf[9];
575+
vcInfo.envValue = vcaInfo.envValue;
594576

595577
if (vcInfo.irqStatus & VC31A_STATUS_D_PPG_OK) {
596578
vc31_new_ppg(ppgValue); // send PPG value
@@ -614,7 +596,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
614596
vcbInfo.sampleData.envValue[1] = buf[4] >> 4;
615597
vcbInfo.sampleData.preValue[1] = buf[4] & 0x0F;
616598
vcbInfo.sampleData.envValue[2] = buf[5] >> 4;
617-
vcbInfo.sampleData.psValue = buf[5] & 0x0F;
599+
vcbInfo.sampleData.psValue = buf[5] & 0x0F;
618600
buf = &vcInfo.raw[6];
619601
vc31_rx(VC31B_REG17, buf, 6);
620602
vcbInfo.sampleData.pdResValue[0] = (buf[3] >> 4) & 0x07;
@@ -626,6 +608,7 @@ void vc31_irqhandler(bool state, IOEventFlags flags) {
626608

627609
// if we had environment sensing, check for wear status and update LEDs accordingly
628610
if (vcInfo.irqStatus & VC31B_INT_PS) {
611+
vcInfo.envValue = vcbInfo.sampleData.envValue[2];
629612
//jsiConsolePrintf("e %d %d\n", vcbInfo.sampleData.psValue, vcbInfo.sampleData.envValue[2] );
630613
vc31b_wearstatus();
631614
}

libs/misc/hrm_vc31.h

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
3+
*
4+
* Copyright (C) 2019 Gordon Williams <gw@pur3.co.uk>
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public
7+
* License, v. 2.0. If a copy of the MPL was not distributed with this
8+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*
10+
* ----------------------------------------------------------------------------
11+
* VC31 heart rate sensor - additional info
12+
* ----------------------------------------------------------------------------
13+
*/
14+
15+
#ifndef EMULATED
16+
17+
// Hack to fix Eclipse syntax lint
18+
#ifndef PACKED_FLAGS
19+
#define PACKED_FLAGS
20+
#endif
21+
// ---
22+
23+
// VC31 info shared between VC31A/B
24+
typedef struct {
25+
bool isWearing;
26+
int8_t isWearCnt, unWearCnt; // counters for switching worn/unworn state
27+
uint16_t ppgValue; // current PPG value
28+
uint16_t ppgLastValue; // last PPG value
29+
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
30+
uint8_t wasAdjusted; // true if LED/etc adjusted since the last reading
31+
uint16_t envValue; // env value (but VC31B has 3 value slots so we just use the one we know is ok here)
32+
// the meaning of these is device-dependent but it's nice to have them in one place
33+
uint8_t irqStatus;
34+
uint8_t raw[12];
35+
} PACKED_FLAGS VC31Info;
36+
37+
extern VC31Info vcInfo;
38+
39+
#endif

0 commit comments

Comments
 (0)