Skip to content

Commit 97e9846

Browse files
committed
Bangle.js: Added 'bpmMin/bpmMax' and 'activity' to 'health' event and 'Bangle.getHealthStatus'
1 parent 274f2ae commit 97e9846

File tree

2 files changed

+62
-8
lines changed

2 files changed

+62
-8
lines changed

ChangeLog

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
STM32: Fix spi.write usage on same port that SD card is used with (fix #2613)
2222
Bangle.js2: Added new Renaissance fonts and g.findFont function for selecting the best font
2323
Bangle.js2: Fix issue when an onchange callback from E.showMenu submenu changes the menu immediately
24+
Bangle.js: Added 'bpmMin/bpmMax' and 'activity' to 'health' event and 'Bangle.getHealthStatus'
2425

2526
2v25 : ESP32C3: Get analogRead working correctly
2627
Graphics: Adjust image alignment when rotating images to avoid cropping (fix #2535)

libs/banglejs/jswrap_bangle.c

+61-8
Original file line numberDiff line numberDiff line change
@@ -920,15 +920,28 @@ TouchGestureType touchGesture; /// is JSBT_SWIPE is set, what happened?
920920

921921
/// How often should we fire 'health' events?
922922
#define HEALTH_INTERVAL 600000 // 10 minutes (600 seconds)
923+
/// What is the current activity. In order of priority
924+
typedef enum {
925+
HSA_UNKNOWN,
926+
HSA_NOT_WORN,
927+
HSA_WALKING,
928+
HSA_EXERCISE,
929+
} PACKED_FLAGS HealthStateActivity;
930+
/// Strings for HealthStateActivity, matching https://codeberg.org/Freeyourgadget/Gadgetbridge/src/branch/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java
931+
#define HSA_STRINGS "UNKNOWN","NOT_WORN","WALKING","EXERCISE"
932+
#define HSA_STRINGS_LEN 4
923933
/// Struct with currently tracked health info
924934
typedef struct {
925935
uint8_t index; ///< time_in_ms / HEALTH_INTERVAL - we fire a new Health event when this changes
926936
uint32_t movement; ///< total accelerometer difference. Used for activity tracking.
927937
uint16_t movementSamples; ///< Number of samples added to movement
928938
uint16_t stepCount; ///< steps during current period
929939
uint16_t bpm10; ///< beats per minute (x10)
940+
uint16_t bpm10min, bpm10max; ///< beats per minute min and max (x10)
930941
uint8_t bpmConfidence; ///< confidence of current BPM figure
931-
} HealthState;
942+
uint16_t sportActivityTime; ///< Time in msec spent doing high acceleration activity
943+
HealthStateActivity activity; ///< what's the current activity
944+
} PACKED_FLAGS HealthState;
932945
/// Currently tracked health info during this period
933946
HealthState healthCurrent;
934947
/// Health info during the last period, used when firing a health event
@@ -1541,15 +1554,27 @@ void peripheralPollHandler() {
15411554
#ifdef HEARTRATE_VC31_BINARY
15421555
// Activity detection
15431556
hrmSportActivity = ((hrmSportActivity*63)+MIN(accDiff,4096))>>6; // running average
1544-
if (hrmSportTimer < TIMER_MAX) {
1557+
if (hrmSportTimer < TIMER_MAX)
15451558
hrmSportTimer += pollInterval;
1546-
}
1547-
if (hrmSportActivity > HRM_SPORT_ACTIVITY_THRESHOLD) // if enough movement, zero timer (enter sport mode)
1559+
if (hrmSportActivity > HRM_SPORT_ACTIVITY_THRESHOLD) { // if enough movement, zero timer (enter sport mode)
15481560
hrmSportTimer = 0;
1561+
if (healthCurrent.sportActivityTime < TIMER_MAX)
1562+
healthCurrent.sportActivityTime += pollInterval;
1563+
// If we've been very active for over 30 seconds in our 10 minute slot, assume we're doing sport
1564+
if (healthCurrent.sportActivityTime > 30000) { // remember it can't be more than TIMER_MAX(60k)
1565+
if (healthCurrent.activity < HSA_EXERCISE)
1566+
healthCurrent.activity = HSA_EXERCISE;
1567+
}
1568+
}
15491569
if (hrmSportMode>=0) // if HRM sport mode is forced, just use that
15501570
hrmInfo.sportMode = hrmSportMode;
1551-
else // else set to running mode if we've had enough activity recently
1552-
hrmInfo.sportMode = (hrmSportTimer < HRM_SPORT_ACTIVITY_TIMEOUT) ? SPORT_TYPE_RUNNING : SPORT_TYPE_NORMAL;
1571+
else { // else set to running mode if we've had enough activity recently
1572+
if (hrmSportTimer < HRM_SPORT_ACTIVITY_TIMEOUT) {
1573+
hrmInfo.sportMode = SPORT_TYPE_RUNNING;
1574+
} else {
1575+
hrmInfo.sportMode = SPORT_TYPE_NORMAL;
1576+
}
1577+
}
15531578
#endif
15541579
// Power saving
15551580
if (bangleFlags & JSBF_POWER_SAVE) {
@@ -1612,6 +1637,8 @@ void peripheralPollHandler() {
16121637
stepCounter += newSteps;
16131638
healthCurrent.stepCount += newSteps;
16141639
healthDaily.stepCount += newSteps;
1640+
if (healthCurrent.stepCount > 200 && healthCurrent.activity<HSA_WALKING) // if 200 steps in this 10 minute chunk, assume walking
1641+
healthCurrent.activity = HSA_WALKING;
16151642
bangleTasks |= JSBT_STEP_EVENT;
16161643
jshHadEvent();
16171644
}
@@ -1681,8 +1708,15 @@ void peripheralPollHandler() {
16811708
// Did we enter a new 10 minute interval?
16821709
JsVarFloat msecs = jshGetMillisecondsFromTime(time);
16831710
uint8_t healthIndex = (uint8_t)(msecs/HEALTH_INTERVAL);
1684-
if (healthIndex != healthCurrent.index) {
1685-
// we did - fire 'Bangle.health' event
1711+
if (healthIndex != healthCurrent.index) { // we did
1712+
// quick check - if we don't know what's happening and the Bangle isn't moving, assume it's not worn
1713+
if (healthCurrent.activity == HSA_UNKNOWN) {
1714+
uint32_t movement = healthCurrent.movement / healthCurrent.movementSamples;
1715+
if (movement < 120) healthCurrent.activity = HSA_NOT_WORN;
1716+
}
1717+
if (healthDaily.activity < healthCurrent.activity)
1718+
healthDaily.activity = healthCurrent.activity;
1719+
// now fire 'Bangle.health' event and reset the current health status
16861720
healthLast = healthCurrent;
16871721
healthStateClear(&healthCurrent);
16881722
healthCurrent.index = healthIndex;
@@ -1727,8 +1761,20 @@ static void hrmHandler(int ppgValue) {
17271761
healthDaily.bpmConfidence = hrmInfo.confidence;
17281762
healthDaily.bpm10 = hrmInfo.bpm10;
17291763
}
1764+
if (hrmInfo.confidence >= 90) {
1765+
if (hrmInfo.bpm10 < healthCurrent.bpm10min) healthCurrent.bpm10min = hrmInfo.bpm10;
1766+
if (hrmInfo.bpm10 < healthDaily.bpm10min) healthDaily.bpm10min = hrmInfo.bpm10;
1767+
if (hrmInfo.bpm10 > healthCurrent.bpm10max) healthCurrent.bpm10max = hrmInfo.bpm10;
1768+
if (hrmInfo.bpm10 > healthDaily.bpm10max) healthDaily.bpm10max = hrmInfo.bpm10;
1769+
}
17301770
jshHadEvent();
17311771
}
1772+
#ifdef HEARTRATE_VC31_BINARY
1773+
if (!hrmInfo.isWorn) {
1774+
if (healthCurrent.activity == HSA_UNKNOWN)
1775+
healthCurrent.activity = HSA_NOT_WORN;
1776+
}
1777+
#endif
17321778
if (bangleFlags & JSBF_HRM_INSTANT_LISTENER) {
17331779
// what if we already have HRM data that was queued - eg if working with FIFO?
17341780
/*if (bangleTasks & JSBT_HRM_INSTANT_DATA)
@@ -3529,6 +3575,8 @@ JsVar *jswrap_banglejs_getAccel() {
35293575
* `steps` is the number of steps during this period
35303576
* `bpm` the best BPM reading from HRM sensor during this period
35313577
* `bpmConfidence` best BPM confidence (0-100%) during this period
3578+
* `bpmMin`/`bpmMax` (2v26+) the minimum/maximum BPM reading from HRM sensor during this period (where confidence is over 90)
3579+
* `activity` (2v26+) the currently assumed activity, one of "UNKNOWN","NOT_WORN","WALKING","EXERCISE"
35323580
35333581
*/
35343582
static JsVar *_jswrap_banglejs_getHealthStatusObject(HealthState *health) {
@@ -3540,7 +3588,12 @@ static JsVar *_jswrap_banglejs_getHealthStatusObject(HealthState *health) {
35403588
#ifdef HEARTRATE
35413589
jsvObjectSetChildAndUnLock(o,"bpm",jsvNewFromFloat(health->bpm10 / 10.0));
35423590
jsvObjectSetChildAndUnLock(o,"bpmConfidence",jsvNewFromInteger(health->bpmConfidence));
3591+
jsvObjectSetChildAndUnLock(o,"bpmMin",jsvNewFromFloat(health->bpm10min / 10.0));
3592+
jsvObjectSetChildAndUnLock(o,"bpmMax",jsvNewFromFloat(health->bpm10max / 10.0));
35433593
#endif
3594+
const char *ACT_STRINGS[HSA_STRINGS_LEN] = { HSA_STRINGS };
3595+
if (health->activity < HSA_STRINGS_LEN)
3596+
jsvObjectSetChildAndUnLock(o,"activity",jsvNewFromString(ACT_STRINGS[health->activity]));
35443597
}
35453598
return o;
35463599
}

0 commit comments

Comments
 (0)