-
Notifications
You must be signed in to change notification settings - Fork 219
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
base: master
Are you sure you want to change the base?
Faster heart rate display #363
Conversation
Signed-off-by: thiswillbeyourgithub <github@32mail.33mail.com>
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 |
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) |
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 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. |
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. |
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. |
Hello to all, Here are some plots of my hrs.data 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 |
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. |
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. |
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.
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.
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.
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. |
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.
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?
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!
Thank god, I was starting to worry after hitting 120 at a regular pace :)
Here's the link if needed : #357 (comment) |
Hi @jcp-sd
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. |
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! |
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. |
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
@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 |
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;
The corresponding sample rates in Hz are;
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). 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??? |
You can override built-in code with files from the the filesystem. You either have to monkey patch (meaning use the REPL to reassign methods of a class that is currently running) or just rebuild wasp-os.
The other thing you can do is just boot into safe mode and do everything from the REPL (e.g. don't run any apps, just call the driver code to configure the HRS anyway you like to try and better understand it).
…
On 3 Oct 2022 at 20:41, John Peterson KD6EKQ ***@***.***> wrote:
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).
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???
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
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: 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. |
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 I've just made progress! The docs says that the recommended value of 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? |
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. |
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).
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
Yes, I'm very much in the mindset of 'get things going then optimise'.
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
Maybe they just made the chip with lots of control in there? Also I've seen uses on Hackaday other than heart rate.
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.
|
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; 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. |
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. |
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 is a plot with HWT = 0x3 which is supposed to provide 100 milliseconds of delay between ADC conversion cycles (and it does). 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... |
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:
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. |
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) |
Yes, by 'Typical Values' I was referring to the data sheet. I see in table 1 they are also called 'Recommend Looking more closely at the same file and comparing to the data sheet I see:
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 |
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. 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. 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. 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. |
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. |
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? |
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.
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.
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 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. |
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).
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. |
Hi @jcp-sd,
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.
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.
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. |
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. |
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.
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 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. |
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 :)