Skip to content

Add in Blood Oxygen support#1607

Open
32bitx64bit wants to merge 23 commits into
coredevices:mainfrom
32bitx64bit:blood-oxygen
Open

Add in Blood Oxygen support#1607
32bitx64bit wants to merge 23 commits into
coredevices:mainfrom
32bitx64bit:blood-oxygen

Conversation

@32bitx64bit

Copy link
Copy Markdown

Adds in blood oxygen support.

Related commits :
coredevices/pebbleos-nonfree#4
coredevices/mobileapp#267

@32bitx64bit

Copy link
Copy Markdown
Author

Was tested on my pebble watch.

Power draw was for obvious reasons, higher than having just HR or neither activated.

Code was tested, and from my testing over the week, seems to work well.

@ericmigi

Copy link
Copy Markdown
Collaborator

very cool! do you happen to have any other SP02 enabled devices to benchmark against? did you add UI to display this on watch?

cc @sjp4

@gmarull gmarull left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, however, this is not mergeable as is. Make sure to remove unnecessary changes (e.g. why enable a demo app at board level?), group changes in logical commits, CI green, etc. (check contributing guidelines). Changes in the nonfree repo also need better explanation.

@32bitx64bit

Copy link
Copy Markdown
Author

very cool! do you happen to have any other SP02 enabled devices to benchmark against? did you add UI to display this on watch?

cc @sjp4

I sadly do not have any devices to monitor SpO2 outside of my Pebble.

Well I guess my Bangle could work? But they also dont have blood oxygen support 😂

If anyone does have one though feel free to test.

Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
@32bitx64bit

Copy link
Copy Markdown
Author

Thanks for the contribution, however, this is not mergeable as is. Make sure to remove unnecessary changes (e.g. why enable a demo app at board level?), group changes in logical commits, CI green, etc. (check contributing guidelines). Changes in the nonfree repo also need better explanation.

Appreciate the feedback, I'll fixup the PR and get it ready when I get some free time.

@32bitx64bit

Copy link
Copy Markdown
Author

If you have any other requests just lmk!

I'm pretty open, and have 2 days off work of free time.

@toxuin

toxuin commented Jun 25, 2026

Copy link
Copy Markdown

I'm installing this and going to compare it with Ultrahuman Air ring which has SpO2 sensor. Will report back.

From looking at the code - it looks quite good! I like hrm_enable(HRMDevice *dev, HRMFeature features, bool low_latency) - clearly a win for both those who need the feature (they get the feature lol) and those who don't (they get more battery life).

One thing I found that may or may not be an actual problem (I'm not that intimately familiar with the codebase yet) is the teardown path prv_activity_spo2_deinit. Seems like you're pausing both activity algo and workout hr in prv_activity_spo2_set_sampling but only unpause activity one and not the workout one. Someone who understands what's going on better than me - please review this path to see if that is an actual issue, or if there's some other thing that unpauses it, or if I'm just not understanding how things work 😅

@32bitx64bit

Copy link
Copy Markdown
Author

I'm installing this and going to compare it with Ultrahuman Air ring which has SpO2 sensor. Will report back.

From looking at the code - it looks quite good! I like hrm_enable(HRMDevice *dev, HRMFeature features, bool low_latency) - clearly a win for both those who need the feature (they get the feature lol) and those who don't (they get more battery life).

One thing I found that may or may not be an actual problem (I'm not that intimately familiar with the codebase yet) is the teardown path prv_activity_spo2_deinit. Seems like you're pausing both activity algo and workout hr in prv_activity_spo2_set_sampling but only unpause activity one and not the workout one. Someone who understands what's going on better than me - please review this path to see if that is an actual issue, or if there's some other thing that unpauses it, or if I'm just not understanding how things work 😅

Appreciate it man! ❤️

Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
Signed-off-by: Gavin X <gavinx2031@triro.net>
@32bitx64bit

32bitx64bit commented Jun 25, 2026

Copy link
Copy Markdown
Author

I'm installing this and going to compare it with Ultrahuman Air ring which has SpO2 sensor. Will report back.
From looking at the code - it looks quite good! I like hrm_enable(HRMDevice *dev, HRMFeature features, bool low_latency) - clearly a win for both those who need the feature (they get the feature lol) and those who don't (they get more battery life).
One thing I found that may or may not be an actual problem (I'm not that intimately familiar with the codebase yet) is the teardown path prv_activity_spo2_deinit. Seems like you're pausing both activity algo and workout hr in prv_activity_spo2_set_sampling but only unpause activity one and not the workout one. Someone who understands what's going on better than me - please review this path to see if that is an actual issue, or if there's some other thing that unpauses it, or if I'm just not understanding how things work 😅

Appreciate it man! ❤️

Good catch, was indeed a bug. Fixed it.

@toxuin

toxuin commented Jun 27, 2026

Copy link
Copy Markdown

It works, and it tracks with Ultrahuman Ring Air within 1%!

I haven't encountered any crashes. Activity detection algo works with it, too.

I don't have any battery stats info due to the app being built by me (not Core Devices) and lacking ability to login with my Pebble account, unfortunately.

screenshot

BTW, it seems like you're building the SpO2 test app into retail firmware. Looking at other test apps - they aren't included when release config is enabled.

@32bitx64bit

32bitx64bit commented Jun 27, 2026

Copy link
Copy Markdown
Author

It works, and it tracks with Ultrahuman Ring Air within 1%!

I haven't encountered any crashes. Activity detection algo works with it, too.

I don't have any battery stats info due to the app being built by me (not Core Devices) and lacking ability to login with my Pebble account, unfortunately.

screenshot

BTW, it seems like you're building the SpO2 test app into retail firmware. Looking at other test apps - they aren't included when release config is enabled.

Awesome, appreciate the testing.

As for battery, I'm on day 2, at 44%. So battery drain is definitely higher when SpO2 is enabled (which is expected)

I have SpO2 track interval at 10 minutes, same thing for the HR, so honestly not bad.
(For most use cases I'd probably recomend setting it to 30 minute intervals, as blood oxygen doesn't change much)

@toxuin

toxuin commented Jun 27, 2026

Copy link
Copy Markdown

Whoa - no way you lost 56% in 2 days! 😅 I'm on day 2 as well and I'm at 83% with doing tons of comms (flashing firmware and debugging watchfaces), occasional runs of SpO2 tester app and a custom watchface that loves haptic feedback.

Honestly, for me at least, I have not noticed any drastic change in battery drain.

@32bitx64bit

Copy link
Copy Markdown
Author

Whoa - no way you lost 56% in 2 days! 😅 I'm on day 2 as well and I'm at 83% with doing tons of comms (flashing firmware and debugging watchfaces), occasional runs of SpO2 tester app and a custom watchface that loves haptic feedback.

Honestly, for me at least, I have not noticed any drastic change in battery drain.

I have HR + SpO2 tracking on. With intervals of 10 minutes between readings. It'd probably be better if I set spo2 to 30 minute intervals.

Not sure if you're also using background tracking

@toxuin

toxuin commented Jul 2, 2026

Copy link
Copy Markdown

Update on the power draw: over last ~5 days, I observed about 8-10% drop per day with 10-minute measurement (both HR + SpO2). I now lowered it to read SpO2 once per hour and will report back in a few days on how that changed things.

@ericmigi

ericmigi commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

exciting to see, thanks for sharing the progress

karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 6, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>
karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 7, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>

Co-Authored-By: Claude <noreply@anthropic.com>
@32bitx64bit

Copy link
Copy Markdown
Author

Whoa - no way you lost 56% in 2 days! 😅 I'm on day 2 as well and I'm at 83% with doing tons of comms (flashing firmware and debugging watchfaces), occasional runs of SpO2 tester app and a custom watchface that loves haptic feedback.
Honestly, for me at least, I have not noticed any drastic change in battery drain.

I have HR + SpO2 tracking on. With intervals of 10 minutes between readings. It'd probably be better if I set spo2 to 30 minute intervals.

Not sure if you're also using background tracking

Recompiled the OS, my power usage is more in line with yours. Must've screwed something up on my end I guess.

@ericmigi

ericmigi commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

If measuring power consumption, remember to compile with debug flags off so you're compiling as a release build. Debug builds turn UART RX on and use 10% per day

@toxuin

toxuin commented Jul 7, 2026

Copy link
Copy Markdown

Have I screwed something up with my build as well?.. 😅 It seems like there is no difference in power draw between sampling every 10 minutes and every hour. I'm seeing the same 8-10% per day drop even when the setting is in 1hr position.

@mhalano

mhalano commented Jul 7, 2026

Copy link
Copy Markdown

@toxuin Couldn't be the debug being enabled as mentioned by @ericmigi?

@toxuin

toxuin commented Jul 7, 2026

Copy link
Copy Markdown

I was under impression that I had release config enabled previously, but just in case, I recompiled with CONFIG_RELEASE=y now. Will test again.

karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 9, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>

Co-Authored-By: Claude <noreply@anthropic.com>
@32bitx64bit

Copy link
Copy Markdown
Author

If measuring power consumption, remember to compile with debug flags off so you're compiling as a release build. Debug builds turn UART RX on and use 10% per day

Good to know.

karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 16, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>

Co-Authored-By: Claude <noreply@anthropic.com>
karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 17, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>

Co-Authored-By: Claude <noreply@anthropic.com>
@toxuin

toxuin commented Jul 18, 2026

Copy link
Copy Markdown

I am seeing same-ish power consumption no matter what setting I select for polling period. Maybe it's the fact of sensor configuration for SpO2 that makes it consume power, and LEDs are relatively small factor in this?..
I made sure the firmware compiled with release config.

@32bitx64bit

Copy link
Copy Markdown
Author

I am seeing same-ish power consumption no matter what setting I select for polling period. Maybe it's the fact of sensor configuration for SpO2 that makes it consume power, and LEDs are relatively small factor in this?.. I made sure the firmware compiled with release config.

Hm. perhaps external factors.

I did a quick rereview, don't see any apparent issues with the period settings. So its possible its just unrelated factors such as HR.

@toxuin

toxuin commented Jul 18, 2026

Copy link
Copy Markdown

I haven't noticed anything that might've predict such behaviour either when I reviewed the code earlier.

I am going to revert to stock firmware for about a week - just to exclude "toxuin is taking crazy pills" factor. I am very much looking forwards to getting this merged - me running the original firmware is not an indication of there being anything wrong with this branch. Quite the opposite: I think I mentioned before that I got literally zero crashes or weirdness during the entire 20+ days of testing. And I did a manual (not AI lol) review of this branch and found some good decisions in it (stated above in my previous comments) and only a single issue - rather minor and already fixed.

Installing stock firmware to exclude my usage patterns, my hardware or my settings affecting it. Will report back in about a week.

@karthakon

Copy link
Copy Markdown

On the battery question — while testing my own HRV sampling work (#1670), I hit something that surprised me: I'd been refreshing my watch app's screen on every sensor event, and switching to a static screen while recording overnight gave markedly better battery life. A much bigger improvement than I expected — enough that it overshadowed the sensor's own draw in my tests. Might be worth keeping the display static across runs when comparing battery, in case it's a factor for you too.

karthakon added a commit to karthakon/PebbleOS that referenced this pull request Jul 23, 2026
Change hrm_enable() to take the requested HRMFeature set (matching
the direction of coredevices#1607) and a low_latency flag (reserved, unused
here). hrm_manager computes the union of active subscriber features,
passes it to the driver, and restarts the sensor when the needed
feature set changes, so HRV is only collected while a subscriber
holds HRMFeature_HRV.

In the GH3X2X driver, when HRV is requested: enable the HRV function,
mirror the HR PPG channel map onto it (the shipped register config
only maps channels for HR/SpO2/ADT, so HRV otherwise comes up with
zero channels and never receives frames), and run it at the HR
sampling rate. gh3x2x_hrv_result_report() forwards each valid RR
interval from the Goodix HRV algorithm to hrm_manager.

hrm_manager broadcasts HRV results as HealthEventHRVUpdate so apps
subscribed through health_service receive them without a raw HRM
session. Everything is behind the new HRM_HRV Kconfig, default n.

Part of coredevices#1630.

Signed-off-by: karthakon <33638614+karthakon@users.noreply.github.com>

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants