Bangle.js: Added heart rate autocorrelation, setHRMPower and 'HRM' event

This commit is contained in:
Gordon Williams 2019-11-27 16:40:08 +00:00
parent d1675bb8f8
commit c923bbc06d
4 changed files with 175 additions and 5 deletions

View File

@ -47,6 +47,7 @@
Bangle.js: EVents for 'swipe' and 'touch' on the touchscreen
Added ability to compile Espruino to JavaScript with Emscripten
Allow g.setColor/setBgColor to take hex Strings of the form `'#00ff00'`
Bangle.js: Added heart rate autocorrelation, setHRMPower and 'HRM' event
2v04 : Allow \1..\9 escape codes in RegExp
ESP8266: reading storage is not working for boot from user2 (fix #1507)

View File

@ -28,11 +28,13 @@
#include "jswrap_math.h"
#include "jswrap_storage.h"
#include "jswrap_array.h"
#include "jswrap_arraybuffer.h"
#include "jswrap_heatshrink.h"
#include "jsflash.h"
#include "nrf_gpio.h"
#include "nrf_delay.h"
#include "nrf_soc.h"
#include "nrf_saadc.h"
#include "nrf5x_utils.h"
#include "jsflash.h" // for jsfRemoveCodeFromFlash
#include "bluetooth.h" // for self-test
@ -118,6 +120,9 @@ Magnetometer/Compass data available with `{x,y,z,dx,dy,dz,heading}` object as a
* `x/y/z` raw x,y,z magnetometer readings
* `dx/dy/dz` readings based on calibration since magnetometer turned on
* `heading` in degrees based on calibrated readings (will be NaN if magnetometer hasn't been rotated around 360 degrees)
To get this event you must turn the compass on
with `Bangle.setCompassPower(1)`.
*/
/*JSON{
"type" : "event",
@ -127,12 +132,15 @@ Magnetometer/Compass data available with `{x,y,z,dx,dy,dz,heading}` object as a
"ifdef" : "BANGLEJS"
}
Raw NMEA GPS data lines received as a string
To get this event you must turn the GPS on
with `Bangle.setGPSPower(1)`.
*/
/*JSON{
"type" : "event",
"class" : "Bangle",
"name" : "GPS",
"params" : [["fix","JsVar",""]],
"params" : [["fix","JsVar","An object with fix info (see below)"]],
"ifdef" : "BANGLEJS"
}
GPS data, as an object. Contains:
@ -150,6 +158,28 @@ GPS data, as an object. Contains:
```
If a value such as `lat` is not known because there is no fix, it'll be `NaN`.
To get this event you must turn the GPS on
with `Bangle.setGPSPower(1)`.
*/
/*JSON{
"type" : "event",
"class" : "Bangle",
"name" : "HRM",
"params" : [["hrm","JsVar","An object with heart rate info (see below)"]],
"ifdef" : "BANGLEJS"
}
Heat rate data, as an object. Contains:
```
{ "bpm": number, // Beats per minute
"confidence": number, // 0-100 percentage confidence in the heart rate
"raw": Uint8Array, // raw samples from heart rate monitor
}
```
To get this event you must turn the heart rate monitor on
with `Bangle.setHRMPower(1)`.
*/
/*JSON{
"type" : "event",
@ -230,6 +260,7 @@ or right hand side.
#define IOEXP_HRM 0x80
#define ACCEL_HISTORY_LEN 50 ///< Number of samples of accelerometer history
#define HRM_HISTORY_LEN 256
uint8_t nmeaCount = 0; // how many characters of NMEA data do we have?
char nmeaIn[NMEA_MAX_SIZE]; // 82 is the max for NMEA
@ -242,6 +273,7 @@ typedef struct {
#define DEFAULT_ACCEL_POLL_INTERVAL 80 // in msec - 12.5 to match accelerometer
#define DEFAULT_LCD_POWER_TIMEOUT 30000 // in msec - default for lcdPowerTimeout
#define HRM_POLL_INTERVAL 20 // in msec
#define ACCEL_POLL_INTERVAL_MAX 5000 // in msec - DEFAULT_ACCEL_POLL_INTERVAL_MAX+TIMER_MAX must be <65535
#define BTN_LOAD_TIMEOUT 1500 // in msec - how long does the button have to be pressed for before we restart
#define TIMER_MAX 60000 // 60 sec - enough to fit in uint16_t without overflow if we add ACCEL_POLL_INTERVAL
@ -311,6 +343,9 @@ bool stepWasLow;
uint8_t touchLastState;
uint8_t touchLastState2;
int8_t hrmHistory[HRM_HISTORY_LEN];
volatile uint8_t hrmHistoryIdx;
/// Promise when beep is finished
JsVar *promiseBeep;
/// Promise when buzz is finished
@ -329,12 +364,13 @@ typedef enum {
JSBT_GESTURE_DATA = 256, ///< we have data from a gesture
JSBT_CHARGE_EVENT = 512, ///< we need to fire a charging event
JSBT_STEP_EVENT = 1024, ///< we've detected a step via the pedometer
JSBT_SWIPE_LEFT = 2048,
JSBT_SWIPE_RIGHT = 4096,
JSBT_SWIPE_LEFT = 2048, ///< swiped left over touchscreen
JSBT_SWIPE_RIGHT = 4096, ///< swiped right over touchscreen
JSBT_SWIPE_MASK = JSBT_SWIPE_LEFT | JSBT_SWIPE_RIGHT,
JSBT_TOUCH_LEFT = 8192,
JSBT_TOUCH_RIGHT = 16384,
JSBT_TOUCH_LEFT = 8192, ///< touch lhs of touchscreen
JSBT_TOUCH_RIGHT = 16384, ///< touch rhs of touchscreen
JSBT_TOUCH_MASK = JSBT_TOUCH_LEFT | JSBT_TOUCH_RIGHT,
JSBT_HRM_DATA = 32768, ///< Heart rate data is ready for analysis
} JsBangleTasks;
JsBangleTasks bangleTasks;
@ -489,6 +525,36 @@ void peripheralPollHandler() {
//jswrap_banglejs_ioWr(IOEXP_HRM,1); // debug using HRM LED
}
void hrmPollHandler() {
//jswrap_banglejs_ioWr(IOEXP_HRM,0); // on
nrf_saadc_input_t ain = 1 + (pinInfo[HEARTRATE_PIN_ANALOG].analog & JSH_MASK_ANALOG_CH);
nrf_saadc_channel_config_t config;
config.acq_time = NRF_SAADC_ACQTIME_10US;
config.gain = NRF_SAADC_GAIN1;
config.mode = NRF_SAADC_MODE_SINGLE_ENDED;
config.pin_p = ain;
config.pin_n = ain;
config.reference = NRF_SAADC_REFERENCE_INTERNAL;
config.resistor_p = NRF_SAADC_RESISTOR_DISABLED;
config.resistor_n = NRF_SAADC_RESISTOR_DISABLED;
// make reading
nrf_saadc_enable();
nrf_saadc_resolution_set(NRF_SAADC_RESOLUTION_8BIT);
nrf_saadc_channel_init(0, &config);
extern nrf_saadc_value_t nrf_analog_read();
int v = nrf_analog_read();
if (v<-128) v=-128;
if (v>127) v=127;
hrmHistory[hrmHistoryIdx] = v;
hrmHistoryIdx = (hrmHistoryIdx+1) & (HRM_HISTORY_LEN-1);
if (hrmHistoryIdx==0)
bangleTasks |= JSBT_HRM_DATA;
//jswrap_banglejs_ioWr(IOEXP_HRM,1); // off
}
void backlightOnHandler() {
if (i2cBusy) return;
jswrap_banglejs_ioWr(IOEXP_LCD_BACKLIGHT, 0); // backlight on
@ -498,6 +564,8 @@ void backlightOffHandler() {
jswrap_banglejs_ioWr(IOEXP_LCD_BACKLIGHT, 1); // backlight off
}
void btnHandlerCommon(int button, bool state, IOEventFlags flags) {
// wake up
if (lcdPowerTimeout) {
@ -816,6 +884,41 @@ void jswrap_banglejs_lcdWr(JsVarInt cmd, JsVar *data) {
lcdST7789_cmd(cmd, dLen, (const uint8_t *)dPtr);
}
/*JSON{
"type" : "staticmethod",
"class" : "Bangle",
"name" : "setHRMPower",
"generate" : "jswrap_banglejs_setHRMPower",
"params" : [
["isOn","bool","True if the heart rate monitor should be on, false if not"]
],
"ifdef" : "BANGLEJS"
}
Set the power to the Heart rate monitor
When on, data is output via the `GPS` event on `Bangle`:
```
Bangle.setHRMPower(1);
Bangle.on('HRM',print);
```
*When on, the Heart rate monitor draws roughly 5mA*
*/
void jswrap_banglejs_setHRMPower(bool isOn) {
jstStopExecuteFn(hrmPollHandler, 0);
if (isOn) {
jshPinAnalog(HEARTRATE_PIN_ANALOG);
jswrap_banglejs_ioWr(IOEXP_HRM, 0); // HRM on
memset(hrmHistory, 0, sizeof(hrmHistory));
hrmHistoryIdx = 0;
JsSysTime t = jshGetTimeFromMilliseconds(HRM_POLL_INTERVAL);
jstExecuteFn(hrmPollHandler, NULL, jshGetSystemTime()+t, t);
} else {
jswrap_banglejs_ioWr(IOEXP_HRM, 1); // HRM off
}
}
/*JSON{
"type" : "staticmethod",
"class" : "Bangle",
@ -1073,6 +1176,7 @@ void jswrap_banglejs_kill() {
jstStopExecuteFn(backlightOnHandler, 0);
jstStopExecuteFn(backlightOffHandler, 0);
jstStopExecuteFn(peripheralPollHandler, 0);
jstStopExecuteFn(hrmPollHandler, 0);
jsvUnLock(promiseBeep);
promiseBeep = 0;
jsvUnLock(promiseBuzz);
@ -1191,6 +1295,67 @@ bool jswrap_banglejs_idle() {
}
}
}
if (bangle && (bangleTasks & JSBT_HRM_DATA)) {
JsVar *o = jsvNewObject();
if (o) {
const int BPM_MIN = 40;
const int BPM_MAX = 200;
const int SAMPLES_PER_SEC = 1000 / HRM_POLL_INTERVAL;
const int CMIN = 60000 / (BPM_MAX*HRM_POLL_INTERVAL);
const int CMAX = 60000 / (BPM_MIN*HRM_POLL_INTERVAL);
assert(CMAX<HRM_HISTORY_LEN);
uint8_t corr[CMAX];
int minCorr = 0x7FFFFFFF, maxCorr = 0;
int minIdx = 0;
for (int c=CMIN;c<CMAX;c++) {
int s = 0;
// correlate
for (int i=CMAX;i<HRM_HISTORY_LEN;i++) {
int d = hrmHistory[i] - hrmHistory[i-c];
s += d*d;
}
// store min and max
if (s<minCorr) {
minCorr = s;
minIdx = c;
}
if (s>maxCorr) {
maxCorr = s;
}
// store in 8 bit array
s = s>>10;
if (s>255) s=255;
corr[c] = s;
}
for (int c=0;c<CMIN;c++)
corr[c] = corr[CMIN]; // just fill in the lower data
// confidence depends on how good a fit correlation is (lower minCorr = better)
int confidence = 120 - (minCorr/600);
// but if maxCorr is low we don't have enough signal, so that's bad too
if (maxCorr<10000) confidence -= (10000-maxCorr)/50;
if (confidence<0) confidence=0;
if (confidence>100) confidence=100;
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromInteger(60000 / (minIdx*HRM_POLL_INTERVAL)));
jsvObjectSetChildAndUnLock(o,"confidence",jsvNewFromInteger(confidence));
JsVar *s = jsvNewNativeString(hrmHistory, sizeof(hrmHistory));
JsVar *ab = jsvNewArrayBufferFromString(s,0);
jsvObjectSetChildAndUnLock(o,"raw",jswrap_typedarray_constructor(ARRAYBUFFERVIEW_INT8,ab,0,0));
jsvUnLock2(ab,s);
// for debugging
if (false) {
jsvObjectSetChildAndUnLock(o,"minDifference",jsvNewFromInteger(minCorr));
jsvObjectSetChildAndUnLock(o,"maxDifference",jsvNewFromInteger(maxCorr));
s = jsvNewArrayBufferWithData(sizeof(corr),corr);
jsvObjectSetChildAndUnLock(o,"correlation",jswrap_typedarray_constructor(ARRAYBUFFERVIEW_UINT8,s,0,0));
jsvUnLock(s);
}
jsiQueueObjectCallbacks(bangle, JS_EVENT_PREFIX"HRM", &o, 1);
jsvUnLock(o);
}
}
if (bangle && (bangleTasks & JSBT_GESTURE_DATA)) {
if (bangle && jsiObjectHasCallbacks(bangle, JS_EVENT_PREFIX"gesture")) {
JsVar *arr = jsvNewTypedArray(ARRAYBUFFERVIEW_INT8, accGestureRecordedCount*3);

View File

@ -24,6 +24,7 @@ bool jswrap_banglejs_isLCDOn();
bool jswrap_banglejs_isCharging();
JsVarInt jswrap_banglejs_getBattery();
void jswrap_banglejs_setHRMPower(bool isOn);
void jswrap_banglejs_setGPSPower(bool isOn);
void jswrap_banglejs_setCompassPower(bool isOn);

View File

@ -425,6 +425,9 @@ if "VIBRATE" in board.devices:
if "SPEAKER" in board.devices:
codeOutDevicePins("SPEAKER", "SPEAKER")
if "HEARTRATE" in board.devices:
codeOutDevicePins("HEARTRATE", "HEARTRATE")
if "BAT" in board.devices:
codeOutDevicePins("BAT", "BAT")