Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster heart rate display #363

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

thiswillbeyourgithub
Copy link
Contributor

@thiswillbeyourgithub thiswillbeyourgithub commented Sep 22, 2022

Signed-off-by: thiswillbeyourgithub github@32mail.33mail.com
Hi,

After chatting with @drtonyr in #357 I ended up spending some time in ppg.py and figured that instead of waiting 10s to display the heart rate you could just wait 1s, then display the value and every new second recompute a more accurate value.

Hopefully this will get my friends to restart using the watch I offered them :)

Signed-off-by: thiswillbeyourgithub <github@32mail.33mail.com>
@thiswillbeyourgithub thiswillbeyourgithub marked this pull request as ready for review September 22, 2022 15:21
@jcp-sd
Copy link

jcp-sd commented Sep 24, 2022

Hello,

This is a bit tangential to the specifics of your pull request, but I am posting here because it's more likely it will be seen by others who are familiar with the specifics of HRS 3300. I have been investigating some possible improvements to the spectral filter, possibly replacing the current Biquad filter with a Tchebyshev filter which has min-max property in the stop bands.

My concern is that I noticed that the datasheet for the HRS 3300, and the other device drivers floating around on the internet indicate that the reflectance data is sampled at 25 Hz. But, the wasp-os code claims it is 24 Hz in the comments. The live code lines that calculate the heart rate are also based on 24 Hz.

Who is right ????? That doesn't sound like much, but it's a 4% difference.

The datasheet is available on the PineTime wiki. I can provide specific links to the other drivers I mentioned.

Best, John

@daniel-thompson
Copy link
Collaborator

How much different is the answer after 1 second to the one after ten seconds?

Overall I am reluctant to do much with the PPG algorithms without recorded test data to compare against. wasp-os has facilities to record to the FLASH the observed PPG data and I'd like to collect a corpus of recordings (together with expected heart rates) that we can use to compare the efficacy of any algorithm improvements.

As I hope is well known, the current algorithm was hacked together in less than a day and the primary aim at the time was mostly to have an approach that minimized RAM storage (hence the history buffer being 8-bit post processed values). It should also be note that the data coming of the HRS3300 at the time was very close to junk and I think we have now changed the setting a bit and get better results.

It is not that the existing algorithm shoud be assumed to be any good... just that without recorded test data we have no robust way to compare improvements with the existing approach.

You'll find a longer version of this comment here: InfiniTimeOrg/InfiniTime#532 (comment)

@daniel-thompson
Copy link
Collaborator

Overall I am reluctant to do much with the PPG algorithms without recorded test data to compare against. wasp-os has facilities to record to the FLASH the observed PPG data and I'd like to collect a corpus of recordings (together with expected heart rates) that we can use to compare the efficacy of any algorithm improvements.

Put another way, please take a look at the information about recording heart rate traces (https://wasp-os.readthedocs.io/en/latest/apps.html#module-apps.heart ) and let's create a section in the wasp-os repo where we can store some test data.

If you are able to capture from multiple subjects that's great and at different points in recovery after exercise[1]. Note that it is fine for one hrs.data to contain data from multiple subjects and no need to track which one is which (in fact it is better not to since it improves data anonymity).

[1] For now lets capture all samples with the watch and subject not moving. Figuring out heart rate for a running/walking subject is pretty difficult because swinging your arms (and shake from foot strike) also creates a rhymthmic pattern on the PPG sensor that, when I last tried it, was of much higher magnitude than the heart cycle itself.

@daniel-thompson
Copy link
Collaborator

Who is right ????? That doesn't sound like much, but it's a 4% difference.

I doubt either is "right"! The conversion time is 25ms so both 25Hz and 24Hz sampling is likely to be fine.

@drtonyr
Copy link
Contributor

drtonyr commented Sep 25, 2022

Who is right ????? That doesn't sound like much, but it's a 4% difference.

I doubt either is "right"! The conversion time is 25ms so both 25Hz and 24Hz sampling is likely to be fine.

If you have a 'sample and hold' ADC running at 25Hz (as I believe we have) then you really really should take the samples at 25Hz, not 24Hz. The reason is that if we do 24Hz then once a second we'll drop a sample. Imagine a smooth heart rate pulse, there will be a sharp change of twice what you expect at the point a sample is dropped. If you really want convincing, drop some samples from audio and listen to it.

@drtonyr
Copy link
Contributor

drtonyr commented Sep 25, 2022

Overall I am reluctant to do much with the PPG algorithms without recorded test data to compare against. wasp-os has facilities to record to the FLASH the observed PPG data and I'd like to collect a corpus of recordings (together with expected heart rates) that we can use to compare the efficacy of any algorithm improvements.

Put another way, please take a look at the information about recording heart rate traces (https://wasp-os.readthedocs.io/en/latest/apps.html#module-apps.heart ) and let's create a section in the wasp-os repo where we can store some test data.

If you are able to capture from multiple subjects that's great and at different points in recovery after exercise[1]. Note that it is fine for one hrs.data to contain data from multiple subjects and no need to track which one is which (in fact it is better not to since it improves data anonymity).

[1] For now lets capture all samples with the watch and subject not moving. Figuring out heart rate for a running/walking subject is pretty difficult because swinging your arms (and shake from foot strike) also creates a rhymthmic pattern on the PPG sensor that, when I last tried it, was of much higher magnitude than the heart cycle itself.

I'm strongly in favour of this. I took the latest CI build around my local 5k parkrun yesterday, it was absolutely useless (walking there, jogging round, sprinting, fast walking, all completely wrong).

What we really need is logged accurate heart rates. Then we could correlate the two to get good alignment and we'd have a trusted result. I haven't yet thought of what works well and does good logging, but I have an unmodified P8 and an old Garmin and I saw a Garmin 205 for sale for £20 if that would work.

@daniel-thompson
Copy link
Collaborator

If you have a 'sample and hold' ADC running at 25Hz

It is not clear this is true. The datasheet is somewhat vague about the actually achieved sample rate but the most likely hold period with the current settings is 37.5ms (25ms conversion plus 12.5ms wait period) which translates to 26.67Hz.

If you are especially concerned then try setting _ENABLE to 0x70 in the init function. That should give you 40 samples per second and decrease the measurement jitter by about 5x. Personally I'm sketpical you will see much difference in the HRS performance but it would certainly be interesting for you to share your hrs.data measurements with and without this change.

@jcp-sd
Copy link

jcp-sd commented Sep 25, 2022

Hello to all,
The instructions presented in the documentation for logging raw samples from the HRS3300 do not work as advertised. I created Issue #359 to report that. It also includes a work-around (which worked fine for me) if you want to collect some of your own raw samples.

Here are some plots of my hrs.data

two_buffers
.

two_buffers_zoom

The wiki would not accept my raw hrs.data file, so I have attached it after base64 encoding.

When I first looked at he plot, my first impression was an emphatic "Somethings not right here !!!!" Seeing so many samples where the value did not change at all from the previous one is what looked really odd to me, and that is what motivated me to hunt down the sample rate. I very much agree that the datasheet is not much help.

Here is a link to (what appears to be) the reference device driver from Tian for the HRS3300

Very Best, John

hrs.data.base64.txt

@drtonyr
Copy link
Contributor

drtonyr commented Sep 26, 2022

Thanks @jcp-sd that is very interesting. So we seem to get two or three samples that are the same. If we are sampling at 24Hz then it looks like the the underlying samlping is going on at about 10Hz at present.

Your link didn't show up for me, the top link on Google is http://www.tianyihexin.com/pic/file/20170627/20170627154877337733.pdf, it would be good to all sing from the same data sheet.

@jcp-sd
Copy link

jcp-sd commented Sep 26, 2022

Figuring out heart rate for a running/walking subject is pretty difficult because swinging your arms (and shake from foot strike) also creates a rhymthmic pattern on the PPG sensor that, when I last tried it, was of much higher magnitude than the heart cycle itself.

Daniel is right that getting good heart rate estimates from a wrist band during vigorous activity is very challenging. I'm guessing the proprietary bands make use of the accelerometer data to disambiguate, but those are pretty complicated algorithms and are very resource hungry.

A "poor man's" approach to that might be to watch the accelerometer and just don't bother with the HRS calculations unless there is not much motion of the band. Not what most people want, but my take is one should not display estimates unless you have some confidence in them.

If you want good heart rate estimates during vigorous activity like swimming and running, get something like the Polar monitors that go around your chest.

@drtonyr
Copy link
Contributor

drtonyr commented Sep 26, 2022

Figuring out heart rate for a running/walking subject is pretty difficult because swinging your arms (and shake from foot strike) also creates a rhymthmic pattern on the PPG sensor that, when I last tried it, was of much higher magnitude than the heart cycle itself.

That's interesting. We have a good step counter and so perhaps we should ignore that frequency. However, before doing that we are provided with a ALS sample as well as a HRS sample. The datasheet (I'm assuming we are all using http://www.tianyihexin.com/pic/file/20170627/20170627154877337733.pdf) doesn't define ALS, but I'm assuming it means Ambient Light Sensor.

Daniel is right that getting good heart rate estimates from a wrist band during vigorous activity is very challenging. I'm guessing the proprietary bands make use of the accelerometer data to disambiguate, but those are pretty complicated algorithms and are very resource hungry.

The algorithm may be very resource intensive or may not be, my guess is that we have more than enough grunt for the job as lower powered devices do it (but it may need C not micropython).

I've just found https://github.com/atc1441/HRS3300-Arduino-Library. I'm not up to incorporating this into WASP, but one way forward would be to hack ATCwatch to log the raw HRS/ALS numbers and the output of libheart.a and then we'd know when we were doing a good job.

A "poor man's" approach to that might be to watch the accelerometer and just don't bother with the HRS calculations unless there is not much motion of the band. Not what most people want, but my take is one should not display estimates unless you have some confidence in them.

Agreed. Confidence measures fall out of many algorithms, we should defintely 'hold' the last known good sample for a while then switch to 'None'. I posted and exponentially decaying autocorrelation algorithm earlier, the decay smooths over short bursts of noise and the relative height of the peak gives the confidence.

If you want good heart rate estimates during vigorous activity like swimming and running, get something like the Polar monitors that go around your chest.

Sure, but I'm not sure the point of the comment (or who 'you' means). The aim of this thread is better HRM from the hardware we all have. I have an umodified P8b on my wrist right now and a commercial pulse oxiometer. The pulse oxiometer has the advantage that it measures through the end of the finger, not reflective. The numbers agree, but only in the steady state, like over more than 20s. The umodified P8b takes about 17s before it will say anything and it shuts off a minute later. This suggest to me that it's designed to take a single measurement.

I'm a huge fan of product design. We have proof that the hardware will do a reasonable job provided you wait 20s before the first measurement and providing you don't mind that the number may be about 20s behind the real heart rate. That looks like a good design goal to me, i.e. we should be aiming for a slower heart rate display, not a faster heart rate display. Once we have it going as good as the original firmware then we can look at improving it.

@thiswillbeyourgithub
Copy link
Contributor Author

Glad to see this PR was hijacked in such a productive manner :) Unfortunately my skills on data analysis are not relevant compared to you so I'll probably mostly sit and watch.

How much different is the answer after 1 second to the one after ten seconds?

It's actually pretty descent. In about 3 seconds of recording I have a pretty good ballpark up to about ~8bpm from the final (15s) values. Though I noticed that sometimes I get jumps for example at 7s of recording the data will go from 70bpm to 150 for no reason then go back down. Maybe this is linked to the sample dropping?

For now lets capture all samples with the watch and subject not moving

I think it could be useful to also add a "baseline" recording of the watch in its charging socket as I already got several >150bpm from just the charger alone. By the way I hacked together the other day a new (unpushed PR) thing: a new setting to trigger a bpm recording every few minutes (customizable) and automatically wait when the watch is in use etc. The value is displayed on top left of the clock. The code can be found in my fork in wasp/app/settings.py, wasp/wasp.py and wasp/widgets.py. Let me know if you think making this a PR would be useful!

Figuring out heart rate for a running/walking subject is pretty difficult because swinging your arms (and shake from foot strike) also creates a rhymthmic pattern on the PPG sensor that, when I last tried it, was of much higher magnitude than the heart cycle itself.

Thank god, I was starting to worry after hitting 120 at a regular pace :)

I posted and exponentially decaying autocorrelation algorithm earlier, the decay smooths over short bursts of noise and the relative height of the peak gives the confidence.

Here's the link if needed : #357 (comment)

@drtonyr
Copy link
Contributor

drtonyr commented Sep 26, 2022

Hi @jcp-sd

This is a bit tangential to the specifics of your pull request, but I am posting here because it's more likely it will be seen by others who are familiar with the specifics of HRS 3300. I have been investigating some possible improvements to the spectral filter, possibly replacing the current Biquad filter with a Tchebyshev filter which has min-max property in the stop bands.

Gosh, now this is interesting. My knowledge of this is many decades old, so I'm using https://en.wikipedia.org/wiki/Butterworth_filter#Comparison_with_other_linear_filters as a reference. We do indeed have a very serious high pass filter problem - we must reject anything that corresponds to a heart rate of (say) 250 bpm and above. In wasp/ppg.py I see "Direct Form II Biquad Filter" but I don't see what the cutoff frequency is or whether it's Butterworth, Chebyshev or something else. @daniel-thompson - can you remember?

I might commit the unspeakable and simply cascade a couple of what we already have working. Or I might do the right thing and plot out the frequency repsonse - unfortunatey I'm out of time for this week.

@daniel-thompson
Copy link
Collaborator

Current filters are:

from scipy.signal import butter, bessel, lfilter
sfreq = 24
lowfreq = 0.75
highfreq = 3.5
lowcut = lowfreq * 2 / sfreq
highcut = highfreq * 2 / sfreq 
hp = butter(2, lowcut, btype='highpass')
lp = bessel(2, highcut, btype='lowpass')

Note that I tried higher order filters for the high/lowpass against my original sets of test data but they were no more effective than the simple second order filters. That data set is no longer useful since IIRC we changed the HRS3300 gain settings.

Put another way everything I said before about getting a whole bunch of test data before tuning the filters... only louder!

@drtonyr
Copy link
Contributor

drtonyr commented Sep 26, 2022

Wow thanks! I ran your code and got the same coeffs, all in seconds, that's wonderful.

I did collect some data today, but it doesn't take long for the file size to exceed what can be pulled. So I wondered whether to hack chunked data save (filename per 10mins or something) or hack a heart rate algorithm, and the latter won. I have a really cool split screen display of the autocorrelation function and the original signal after only a high-pass filter, a good way to end the day.

Screenshot_2022-09-26_19-52-38

@jcp-sd
Copy link

jcp-sd commented Sep 26, 2022

Hello All,

Here is a pretty readable paper on the performance of different filters on raw PPG data; "An optimal filter for short photoplethysmogram signals" I had posted it to the IRC channel a few weeks back, but I'm guessing many of you were on holiday and did not see it. It recommends the Tchebyshev, which is one of my favorites based on past experience.

https://www.nature.com/articles/sdata201876

As I hope is well known, the current algorithm was hacked together in less than a day and the primary aim at the time was mostly to have an approach that minimized RAM storage (hence the history buffer being 8-bit post processed values).

@daniel-thompson this is pretty darn good for a days work. I think I am on the same page as many of you as to what to do next. That should be collecting data from different subjects at rest. The data and plots I posted above were collected while resting in a prone position. Heart rate is probably in the 45-55 BPM range.

I am less motivated to do anything with the filters right now. All of these filters are based on the assumption that the samples are uniformly or nearly uniformly spaced, and the plots I posted above seem to suggest this is currently not the case. Also, I agree with @drtonyr that things like dropped samples (or repeated ones, and non-uniform ones) ARE A BIG deal.

My hardware skills are a bit limited, but it seems like the current driver is a polling strategy, rather than interrupt driven? The datasheet is about as clear as mud, but the reference code at the link below might be helpful in getting these things sorted out. It would be very interesting to see what the raw PPG data looks like from the reference code.

https://github.com/lupyuen/hrs3300_sprd

FWIW @drtonyr the link to the datasheet you posted is identical to the one on the PineTime wiki, which is what I was looking at.

Very Best, John

@jcp-sd
Copy link

jcp-sd commented Oct 3, 2022

Hello All,

I did some thorough reading of the data sheet, and poked around in some of the other device drivers out there. I tend to agree with @daniel-thompson's assertion that the actual sample rate depends on the wait time (between conversion cycles) as configured by the HWT bits in the ENABLE 0x01 register.

My arithmetic comes up with different numbers however. I think that the fastest possible sample rate (corresponding to zero wait time) is 25 Hz. This is mentioned in the data sheet in several places. But that is a baseline of 40 milliseconds for the sample period

Here are the possible sample periods I get in milliseconds;

tau_ms = 40.000 52.500 90.000 115.000 140.000 240.000 440.000 840.000

The corresponding sample rates in Hz are;

f_s = 25.0000 19.0476 11.1111 8.6957 7.1429 4.1667 2.2727 1.1905

Knowing the correct sample rate is very important. The cut frequencies of the high and low pass filters won't be right if constructed with the wrong sample rate.

The latest CI build I used is configured for the 52.5 ms sample period, 19.04 Hz rate. It just doesn't seem like the ADC is keeping up with that! Here is another way to visualize the data I showed above. This is a plot of the sample differences (each sample MINUS the previous sample value). An inspection shows that every time there is a sample that differs from the previous one, the very next sample is the same as the previous sample value (giving a zero difference).

raw_diff

My current thought is that when you ask for higher resolution in the samples (as configured by the RESOLUTION 0x16 register), more time is needed for the ADC to come up with an answer. The latest CI build that I used configures the HRS3300 for 16 bit resolution (which may require more than the current 52.5 milliseconds?)

I put together some changes to test this, but I am having some trouble getting it to work. Can I upload the changed file (say heart.py) to the /flash file system with wasptool (which doesn't seem to work?) or do I need to build a new firmware and flash it???

@daniel-thompson
Copy link
Collaborator

daniel-thompson commented Oct 3, 2022 via email

@drtonyr
Copy link
Contributor

drtonyr commented Oct 4, 2022

Thanks @jcp-sd, this is very valuable work.

I've done some signal processing work. I implemented a display of the autocorrelation which clearly shows when there is a periodic heart rate signal and where there isn't. I also implemented a neat way to find the periodicity of that signal. This is what it looks like:

HeartApp

The numbers I get agree with my pulse oximometer in easy condions - that is at rest. There is very little periodicity, if any, when the pulse rate is above 100, i.e. when exercising. In the above screen shot I'm using the simulator, which has cleaner samples than I get with my p8. The bottom graph is the data after high pass filtering, I show red when the raw samples are the same, this just backs up your finding that we have a serious sampling problem and fixing that should be the next step.

@drtonyr
Copy link
Contributor

drtonyr commented Oct 4, 2022

If you have a 'sample and hold' ADC running at 25Hz

It is not clear this is true. The datasheet is somewhat vague about the actually achieved sample rate but the most likely hold period with the current settings is 37.5ms (25ms conversion plus 12.5ms wait period) which translates to 26.67Hz.

If you are especially concerned then try setting _ENABLE to 0x70 in the init function. That should give you 40 samples per second and decrease the measurement jitter by about 5x. Personally I'm sketpical you will see much difference in the HRS performance but it would certainly be interesting for you to share your hrs.data measurements with and without this change.

The data sheet is certainly short of some useful data. Using the 'draw red if duplicated' debug I still get a lot of duplicated samples if I set the wait period to zero. This is what we expected, from the graphs above we are getting real samples at far less than 24Hz.

For those that haven't found it, there a bug in the documentation for the ADC gains. The docs say RESOLUTION Register(0x16) bits 7:4 Reserved but really bits 7:4 are HRS_RES.

I've just made progress! The docs says that the recommended value of Enable HRS is 0x68. We use 0x60, the bit that differs sets the LED current to 40mA and we have 20mA. More current gives more light and larger ADC ranges and so more signal to noise ratio. I've just made my first 120 bps measurement.

I'm also setting the ALS to 8 bits as we aren't using it at the moment. The ALS is still a mystery to me, are we supposed to sample it and remove the reading or wait until it's stable as a confidence measure?

image

@jcp-sd
Copy link

jcp-sd commented Oct 4, 2022

The word ambient suggests (to me at least) that it is light that is not coming from the green LED. The block diagram on page two of the datasheet suggests (to me at least) that the HRS3300 does some sort of internal cancellation to improve the quality of the HRS samples. That does happens on HR sensors from other manufacturers.

Just speculation, as the datasheet is very much a mystery wrapped in an enigma. I suspect the ALS sample values are not needed unless you want them for some other purpose not related to your heart rate calculations. Again, a guess.

Raising the LED current to 40 mA would certainly provide better SNR at the expense of battery.

No slight intended, but I think the sampling issue seen above is due to an error in @daniel-thompson's code. It polls for the samples at 41.666 milliseconds intervals (see heart.py code). But the HRS3300 is initialized with a 12.5 millisecond delay between conversion cycles (a value of 110 for the HWT field of the ENABLE 0x01 register). His arithmetic is wrong, that corresponds to 40 milliseconds (or 1000/25 milliseconds from the 25 Hz baseline) plus the 12.5 millisecond delay which gives 52.5 milliseconds. So the polling of samples is happening too fast and that would certainly explain what is seen in the data.

I'm also a little mystified by why anyone would ever want to sample at rates less than 25 Hz? Slower sampling would be especially noticeable at very high heart rates like 200 BPM. That corresponds to 3.333 Hz (or 200/60) which means a bare minimum sample rate of at least twice that is required (Nyquist's theorem). That's the bare minimum rate, any anything higher just provides more accuracy for rapid heart rates.

I would also still like to investigate my conjecture (outlined above) that higher HRS bit resolutions (see the RESOLUTION 0x16 register) require more time than the baseline 40 milliseconds for the conversion. Again, just speculation given the near total absence of meaningful information in the datasheet.

I don't have the ability to rebuild wasp-os just yet. But I do have fixes to hrs3300.py, heart.py and pgp.py that does the initialization and polling correctly (I think). if someone is interested in testing them to speed up this research it would be a great help. I'm also running low on free time this week, so any help there would be much appreciated.

@drtonyr
Copy link
Contributor

drtonyr commented Oct 5, 2022

The word ambient suggests (to me at least) that it is light that is not coming from the green LED.

Indeed. A seperate photo diode is shown for CH1. Maybe it's as simple as a physical dividor, one half sees directly reflected light, the other half has no direct reflections. But both channels show about the same signal, it's got the same periodicity (from your plot above).

The block diagram on page two of the datasheet suggests (to me at least) that the HRS3300 does some sort of internal cancellation to improve the quality of the HRS samples. That does happens on HR sensors from other manufacturers.

I'm not so sure about this. From the same diagram HRS and ALS data feed direct to the I2C interface. The only thing that touches the data comes from pin 7 Interrupt-open drain, so INT CONTROL must be interrupt control. We also get to set lots of things about the ALS channel, so I think we are meant to read it and use it. Also, if there was the processing power to use it, why not just give us the beats per minute signal that we are searching for?

Raising the LED current to 40 mA would certainly provide better SNR at the expense of battery.

Yes, I'm very much in the mindset of 'get things going then optimise'.

No slight intended, but I think the sampling issue seen above is due to an error in @daniel-thompson's code. It polls for the samples at 41.666 milliseconds intervals (see heart.py code). But the HRS3300 is initialized with a 12.5 millisecond delay between conversion cycles (a value of 110 for the HWT field of the ENABLE 0x01 register). His arithmetic is wrong, that corresponds to 40 milliseconds (or 1000/25 milliseconds from the 25 Hz baseline) plus the 12.5 millisecond delay which gives 52.5 milliseconds. So the polling of samples is happening too fast and that would certainly explain what is seen in the data.

I'm not following. It's polling, if it polls every 41.666 ms then it samples at 24Hz, irrespective of what the hardware is doing. The HRS ADC conversion time is typically 25ms, not 25Hz.

image

I'm also a little mystified by why anyone would ever want to sample at rates less than 25 Hz? Slower sampling would be especially noticeable at very high heart rates like 200 BPM. That corresponds to 3.333 Hz (or 200/60) which means a bare minimum sample rate of at least twice that is required (Nyquist's theorem). That's the bare minimum rate, any anything higher just provides more accuracy for rapid heart rates.

Maybe they just made the chip with lots of control in there? Also I've seen uses on Hackaday other than heart rate.

I don't have the ability to rebuild wasp-os just yet. But I do have fixes to hrs3300.py, heart.py and pgp.py that does the initialization and polling correctly (I think). if someone is interested in testing them to speed up this research it would be a great help. I'm also running low on free time this week, so any help there would be much appreciated.

Are they on github? I use the script below a lot. I only get sporadic time for WASP, but it's a lot of fun.

#!/bin/bash -ex

tools/wasptool --bootloader

make -j `nproc` BOARD=p8 all

tools/ota-dfu/dfu.py -z build-p8/micropython.zip -a F2:61:81:68:9B:F3 --legacy

sleep 2

tools/wasptool --rtc

@jcp-sd
Copy link

jcp-sd commented Oct 5, 2022

Correct. A sample period of 41.666 ms is a sample rate of 24 Hz. No argument about that, and no argument that it is indeed how often the current CI build polls the hardware.

I did see the entry in the table you posted that claims the HRS ADC conversion time is 25 ms (which is on page 7 of the datasheet). But, on page 1 of the datasheet, in the 2-nd column right below the package drawing it states; "Typical heart rate measurement samples the reflected PPG signal at a frequency of 25Hz". Who knows??? I was also leaning towards the 40 ms or 25 Hz interpretation based on the comments on another driver that can be found here;

fanoush/ds-d6#5 (comment)

I will try your suggestion for building wasp-os, but I think I am missing some prerequisites. My first thought was that the container was my easiest path to getting build capability. I'm short on free time for a few days, so that will have to wait.

@drtonyr
Copy link
Contributor

drtonyr commented Oct 6, 2022

I did see the entry in the table you posted that claims the HRS ADC conversion time is 25 ms (which is on page 7 of the datasheet). But, on page 1 of the datasheet, in the 2-nd column right below the package drawing it states; "Typical heart rate measurement samples the reflected PPG signal at a frequency of 25Hz".

My reading is that the typical HRS ADC conversion time for 14 bits is 25 ms, the typical HRS Cycle Wait Time is 12.5ms, so assuming no others then the typical period (14bit) is 25+12.5 = 37.5ms and so the samping frequency is 1 / 0.00375 = 26.666 Hz which is close to 25Hz and 24Hz.

Maybe we should write an app that allows all of the HRS3300 registers to be changed, then sample very fast and see how often the returned samples change? I don't think it's the 16 bits that's killing us, but something is, and the data sheet isn't that helpful.

@jcp-sd
Copy link

jcp-sd commented Oct 10, 2022

Hello All,

Monday is a holiday here in the USA, so I was able to install the docker image to do my own wasp-os builds. I tested my best theory that the baseline ADC conversion time is 40 ms. It is NOT ...

I was then able to collect some data (as suggested) at higher polling rates (100 Hz) with various values of the extra wait time (the HWT bit field in the 0x01 ENABLE register). I also changed PDRIVE[1] to 1, (0x8 in the lower nibble of 0x01 ENABLE) to get a 40 mA drive current on the LED. To make a long story short, the baseline ADC conversion time appears to be around 80 or maybe closer to 90 milliseconds.

The polling period is 10 milliseconds on all of the plots below.
.
Here are a couple plots with HWT = 0x7 which provides NO extra wait time between conversion cycles.

test_1

test_2

Here is a plot with HWT = 0x3 which is supposed to provide 100 milliseconds of delay between ADC conversion cycles (and it does).

test_3

This would be workable as an 80 ms sampling period gives a 12.5 Hz sample rate or 6.25 Hz Nyquist limit. What is very troubling is that an examination of the plots shows that the conversion time does not seem to be stable (see around time = 900 ms in the first plot, around 14350 ms in the second plot, and around 1440 ms in the last plot).

This might be a hiccup or delay in micropython where the wasp.system.request_tick() call does not execute on time. It might be a hiccup with the HRS3300 itself. I could not figure out how to get a 32 bit hardware.Timer() to instrument the code to check on this...

@drtonyr
Copy link
Contributor

drtonyr commented Oct 11, 2022

Thanks @jcp-sd, that's really interesting. I see clean/non-duplicated plots in other HRS3300 projects which suggests that a HRS ADC conversion time of 25ms is possible, but of course the plots could be after filtering which destroys duplication anyway. When you next get time I'd love to see what happens if all the registers are set at their typical values. It seems to make sense that 16bit HRS and ALS numbers will take longer to obtain than 14 bit numbers. Then there is the unknown nibble in _PDRIVER, register 0x0c - I've just read https://github.com/lupyuen/hrs3300_sprd/blob/master/hrs3300_reg_init.h which is interesting, it says:

{0x0c, 0x4e},  //00001110  bit[6]=0,LED DRIVE=22mA;bit[5]=0,sleep mode;p_pulse=1110,duty=50% 

That doesn't suggest it changes the timing, but it's something we set to non-typical values and we really want to achieve the stated typical HRS ADC conversion time so it's got to be worth setting everything to the suggested typical values.

@jcp-sd
Copy link

jcp-sd commented Oct 12, 2022

Hello @drtonyr. I would be more than happy to run some more tests.

When you say typical values, I really just don't know what you mean by that? The values I have seen in the datasheet (Table 1 on page 10), or those used by other drivers are just all over the place. Get back to me with specific register values and I would be glad to run tests with them.

I will have a closer look at the comments in the header file you mentioned (with I think came from TianYiHeXin, the manufacturer of the HRS3300)

@drtonyr
Copy link
Contributor

drtonyr commented Oct 12, 2022

Yes, by 'Typical Values' I was referring to the data sheet. I see in table 1 they are also called 'Recommend
Values' and that term is much clearer. I'm just thinking of standard debugging here, if it's not working then we should set everything to what the manufacturer says should work, get it going and then tweak once it's working.

Looking more closely at the same file and comparing to the data sheet I see:

{0x17, 0x0d},  //00001101  bits[7:5]=011,HRS gain 16*;bit[1]=0,HRS data=AD0

which is interesting as the data sheet just says that bit[1:0] are reserved.

There's also something to understand in register(0x16) which says ALS ADC resolution. Generate TSEL to analog in ALS mode.. This implies there is an ALS mode, but I don't know how to turn ALS mode on or off and I'd like to.

@jcp-sd
Copy link

jcp-sd commented Oct 13, 2022

At last, a ray of hope for the HRS3300.

I put together a couple of tests, one that (mostly) mirrors the recommended values from the data sheet, and one that mirrors the values from the reference driver. I kept my own values for the ENABLE 0x01 register = 0x78 and values for the LED setting register 0x0c = 0x6e. These seem to be documented correctly and work as expected. So for tests below, there is 0 ms of extra wait time, and the LED is powered with 40 mA. All tests used fixed, 10 ms polling periods.

Here is the test that mimics the reference driver (reg 0x16 = 0x78, and reg 0x17 = 0x0d). The conversion time looks to be about 40 ms or so.

test_5

Here is the test that mimics the recommended values from the datatsheet (reg 0x16 = 0x66, and reg 0x17 = 0x10). I didn't notice what the LED was doing in the previous test, but the LED is ON solid for this one. The conversion time looks to be about 20 or 30 ms.

test_7

Here is a custom one (reg 0x16 = 0x78, reg 0x17 = 0x10). The LED turns on and off for this case. The conversion time looks to be around 40 ms again, similar to the first test above.

test_8

It's looking like what I suspected before is true; the AD hardware requires more conversion time for higher bit resolutions. I am not very schooled on hardware, but my recollection is the voltage comparator types have this property??? I just remember the bench volt meters I used in electronics back in the days of Pangea, one could see the digits in the display change as the process converged.

Some slight variation on the 2nd test above (14-bit resolution) looks like a promising candidate for further work on the filters and other stuff.

@drtonyr
Copy link
Contributor

drtonyr commented Oct 13, 2022

Very nice, thanks. So we now know that trying to get 16 bit numbers isn't a good idea if we want samples at 40ms or faster.

I have the start of a debug app. I poll wasp.watch.hrs.read_hrs() until the number changes then record the time it took to change, this gives sub ms timing accuracies. I then bin every ms and plot as a histogram. I can clearly see that 16 bit samples take over 100ms and 14 bit samples can be grabbed in just under 40ms. There are four writable registers we care about, these are set to their recommended values and can be toggled using Checkboxes. The next bit of work is to convert the histogram display into milliseconds (maybe with a variance), compute some more stats then we'll really be able to play with all options. Happy to share the app.

HeartApp

@jcp-sd
Copy link

jcp-sd commented Oct 13, 2022

Hello @drtonyr

That looks to be quite useful. When you get to the point where you can play around with registers 0x16 and 0x17, be sure to keep some notes about what the LED is doing (solid ON or turning ON/OFF). I didn't see anything obvious that would explain the differences I saw there. Once we figure out what those "Reserved" fields are for in registers 0x16 and 0x17, a workable driver is in sight.

Anyone's thoughts on what is meant by HRS ADC gain in the HGAIN bits of reg 0x17? Is this just an amplifier on the output of the photo diode before the input to the ADC?

@drtonyr
Copy link
Contributor

drtonyr commented Oct 15, 2022

So here are some real numbers from my app which you can get from https://github.com/drtonyr/wasp-os/tree/HRS3300

To keep things simple, let's start with hardware wait time, HWT, in 0x01 which works, so I set it to 0ms (111:wait time between each conversion cycle is 0 ms). The ALS ADC resolution, ALS_RES, doesn't seem to change the HRS timings. All other registers are at their recommended values.

Now we can play with HRS_RES, bits 7:4 of RESOLUTION Register(0x16). I'm going to skip the really low resolutions, they don't work for us. There's a really nice simple story here, each extra bit needs twice as long to settle down. 16bits is too slow for us, so we should be at 15 bits or 14 bits.

Value resolution period
0100 12 bits 6ms
0101 13 bits 12ms
0110: 14 bits 24ms
0111 15 bits 48ms
1000 16 bits 96ms

We can now add back in the hardware wait time, HWT, register, here it is at 14 bits and 15 bits. I think that the three numbers in bold below are the ones that are of interest to us and I'm very inclined towards no wait time, so that's one of the bottom two numbers.

Value wait 14bits 15 bits
101 50ms 73ms 96ms
110 12.5ms 36ms 60ms
111: 0ms 24ms 48ms

There are many sets of reserved bits:

ENABLE Register(0x01) bits 2:0 - if you set bit 2 then the LED flashes rapidly even at HWT of 0ms
HRS LED Driver Set Register (0x0C) bits 7, 4 and 3:0 - if you clear bit 3 the LED turns off. p_pulse=1110,duty=50% does nothing I can see
HGAIN Register(0x17) bits 7:5 and 1:0

The simpled LED story is that the LED is on when the HRS ADC is running and off during the HWT - but see Register(0x01 bit 2, it flashes even when on solid, and Register (0x0C) bit 3. Setting some of the bits crashes my app, I haven't worked out quite what or why yet.

My app reports many 'fails' this is when the HRS ADC value changes too fast to be reasonable. The most obvious explaination is that we are using three i2c calls to read of C0DATAH, C0DATAM and C0DATAL and the data is changing between the calls. This could be a big problem. I've not worked with I2C before, but I would certainly expect some 'data valid' flag that we could wait for and so sync our reads with the chip clock.

@jcp-sd
Copy link

jcp-sd commented Oct 16, 2022

Hi @drtonyr, the more precise numbers on the ADC conversion time will be very helpful to know. Getting the polling time right will certainly avoid getting dropped, or duplicated samples (though that it is bound to happen every once in awhile no matter what, but the less often the better).

There's a really nice simple story here, each extra bit needs twice as long to settle down. 16bits is too slow for us, so we should be at 15 bits or 14 bits.

Indeed, it sure sounds like we are dealing with a voltage comparator type ADC. It's hard to know where the sweet spot is; more resolution (say 15-bit) and a slower sample rate, versus a faster sample rate with less resolution. When it comes time to rewrite the driver, it would be great to make it easy to switch between the 14 and 15-bit resolutions. Another thought there, does it make sense to write the driver in "C" language and then create Python bindings?

On the subject of the LED frequency (pulsed vs solid ON). Having it ON solid would seem to have some advantages, as I suspect the intensity would be more stable. Turning it ON/OFF seems like it would inject a small amount of additional noise into the reflectance samples (from not always providing the exact same intensity on successive pulses).

But turning it ON/OFF would certainly save power. It also might be necessary to get the ambient light measurements (when the LED is off????). Reading through the datasheet, I see several statements that suggest to me that the HRS3300 does do some sort of internal cancellation. Those statements are; On page 1 of the datasheet, under the "Features" bulleted list, it mentions ALS cancellation. In the first paragraph on page 9 it states; "The heart rate engine uses a novel technique to suppress background noise effectively."

Some unexpected events cut into my free time to work on this, but should have time again later in the week.

@drtonyr
Copy link
Contributor

drtonyr commented Oct 17, 2022

Hi @jcp-sd,

Hi @drtonyr, the more precise numbers on the ADC conversion time will be very helpful to know. Getting the polling time right will certainly avoid getting dropped, or duplicated samples (though that it is bound to happen every once in awhile no matter what, but the less often the better).

Yes and no. So my app integrates from any register changes and does outlier removal so caculates and reports to better timing that I quoted. But getting the timing closer only changes the nature of the problem, we really don't want exact timing and hit the chip exactly when the registers change on every read, that would give huge periods of garbage. What I'm saying is that exact timing just groups the errors and we may be able to cope with an odd one but not a burst.

I was wondering why the hardware doesn't use the INT pin which is presumably there to get the timing right. The PineTime schematic here has the HRS sensor shown as J2 but the wrong pin connections for us. It is possible to get the timing exactly right, you just need a timer that always comes in a bit short then you loop and record when/if the value changes. If the value doesn't change you are good, if it does then the time of change resets the timer for the next sample.

Indeed, it sure sounds like we are dealing with a voltage comparator type ADC. It's hard to know where the sweet spot is; more resolution (say 15-bit) and a slower sample rate, versus a faster sample rate with less resolution. When it comes time to rewrite the driver, it would be great to make it easy to switch between the 14 and 15-bit resolutions. Another thought there, does it make sense to write the driver in "C" language and then create Python bindings?

Integrating libheart.a is almost certainly the easiest way to get it working in a form everyone can accept. I'd like to be able to change WASP at the C level, but it's not something have any experiance in (I can do stand alone C, been doing that before ANSI C) and I'm not sure I have the time.

I've just found hrs3300.h - Hrs3300_alg_send_data() comes with a timer and that's wierd. If all of the heart rate calculation was done in libheart.a why is it asynchronous? But if it's being done in the chip, why read it and send it back to the same chip and we'd see the registers documented.

On the subject of the LED frequency (pulsed vs solid ON).

I have to report that I've had no luck whatsoever with getting a decent signal at 15 bit resolution. I've had the LED fully on, I've had 50ms wait in case the ALS needed this time. I've very very rarely had a signal that was anywhere close to clean, certainly not as clean as your initial plot. So I guess I really need to be able to reproduce what you have in order to make progress. Do you have a P8 or P8b (I'm using a P8 but could switch if it helped). And I guess you just flashed a stock image, set the debug flag and pulled the data. Seems I might have to take a step back in order to go forward.

@jcp-sd
Copy link

jcp-sd commented Oct 17, 2022

Hi @drtonyr.

I have the PineTIme hardware. Just to be more specific, I modified hrs3300.py to initialize the HRS3300 to the desired state. I modified heart.py to poll at the desired intervals. I built and flashed a new image with the changes, set the debug flag, and then pulled the data. One thing I did NOT mention that may be relevant; I collected the data in a mostly darken room to keep things simple.

I built and flashed an image using 15-bit resolution for both the ALS and HRS. It polls at the 48 ms period you estimated. A bit subjective for sure, but I have been using it for a day or so for everyday HR measurements. The data shown on the display is pretty clean, and the heart rates agree pretty well with my MI Band 5 on my other arm.

Will post again this evening about some other things I discovered when I get more time.

@jcp-sd
Copy link

jcp-sd commented Oct 19, 2022

When it comes time to rewrite the driver, it would be great to make it easy to switch between the 14 and 15-bit resolutions.

Just to clarify a bit. I was not suggesting a fancy app/gui for changing those things, or anything like that. I was hoping to just have variables for those things, so that one could change the bit resolutions (and sample periods) with a few simple edits where they are assigned a value, without having to go through the whole code to hunt down where those things are used.

It does seem very odd that one can't sample the HRS3300 using an interrupt driven approach!!! It would appear that we are stuck with the current polling approach.

But getting the timing closer only changes the nature of the problem, we really don't want exact timing and hit the chip exactly when the registers change on every read, that would give huge periods of garbage. What I'm saying is that exact timing just groups the errors and we may be able to cope with an odd one but not a burst.

I know what you are saying, but since this involves two different clocks, (one on the HRS3300, the other on the Nrf SOC), I doubt they will ever agree exactly. If the polling period doesn't match the ADC conversion time, the relative point where the polling occurs will walk forward (or backward) through the ADC conversion interval resulting in an occasional dropped (or duplicated) value. That is NOT ideal, but I don't see how it can be avoided???

Also, be careful with the idea you floated about polling at slightly shorter intervals and then waiting for the return value to change. My data collects (see plots I posted earlier in this thread), suggest that successive samples can be the same value!!!! Maybe there is another way to know when the ADC has a produced new sample???

I had done a little digging around to see if the machine.Timer class (currently used in heart.py to get accurate polling), to see if it supported 32-bit timers. In my experiments at a Python REPL (wasptool --console), the longest period I could get was 0x7fffff microseconds which is only a bit more than 8 seconds. The Nrf product brief indicates the SOC has 32 bit hardware timers, but after digging around, they are not exposed to the Python Timer method. Because of this, the current heart.py creates and starts a new machine.Timer instance after grabbing just a few samples, not very accurate.

It occurred to me that one solution is to just write a wrapper function around machine.Timer.time() and check for wrap around (current timer < last timer value). One can the keep track of the overflows and return a more or less accurate time; overflow + Timer.time(). I got that working, and have a polling method implemented in heart.py that works off of a single timer that starts when the app is enabled, and runs until the heart app is disabled, so this should be pretty accurate. I flashed another image and will test it over the next few days.

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.

None yet

4 participants