Skip to content

Conversation

@watertrainer
Copy link

This implements a simple Ring Buffer for the Preprocessed Heart Rate Data, so that actually processing it can always take the last 200 datapoints. Before, after 200 datapoints were collected, all the data was discarded and new data was collected. This had, imo, a few issues.

  1. High frequenzy Heart Rate calculation/measurment wasn't possible (there only was one value very 4 seconds) (may be relevant for Fitness data protocol #749), this may also be useful to average Heart Rate values in the future and reduce noise (Heart rate measurement accuracy #532)
  2. There was no very recent "on demand" Heart Rate avaiable - it always was lacking behind somewhat. This makes it much easier to implement e.g. an instant Heart Rate Update on screen wake.
  3. Everytime 200 datapoints were collected, the Heart Rate HAD to be calculated and saved in the HeartRateController, because on the next tick the whole data array was invalid again. With this the PPG can always provide a HeartRate if required

Note that this Pull Request doesn't change any of the user experience/behaviour of the watch yet. A new Heart Rate value is still only calculated and provided every 200 datapoints (4 Seconds at 25Hz, which is the speed I think, although I read here, that each cycle takes 12,5ms, which would mean 80Hz). This pull request would just make future implementations for working with HeartRate data much easier in my opinion.

I also cleaned up and fixed all the clang-tidy complaints in the file I wrote code in, I don't know if that belongs in a different request.
This is also basically my first cpp code, so any help/critique regarding code style etc. is greatly appreciated.

watertrainer and others added 5 commits April 5, 2022 22:22
we can replace dataIndex in ProcessHeartRate, because it was only being called if dataIndex is 200 anyway (see line 61-67) and the Trough function expects its second argument to be the size and not an index.
changed data Type of heartrate to int, it is only used as an int anyway
@watertrainer
Copy link
Author

The change also requires very little extra processing power, one add and one modulo more per array access, the array access is much more expensive in that case.

@medeyko
Copy link
Contributor

medeyko commented Apr 16, 2022

The change also requires very little extra processing power, one add and one modulo more per array access, the array access is much more expensive in that case.

I guess nothing prevents us from making UPDATE_HEARTRATE_AFTER and DATA_SIZE powers of two, because i think that 200 is an arbitrary number. Most compilers will automatically optimize the "modulo" operation into an "and" operation.

@watertrainer
Copy link
Author

watertrainer commented Apr 16, 2022

DATA_SIZE is not arbitrary I think, because the range of valid bpms depends on how many datapoints there are. That's how I read this comment.
it seems like the calculated bpm depends on the index and changing the max index could mess with that?
I just set UPDATE_HEARTRATE_AFTER to 200 so no behaviour is changed, but that is an arbitrary number

@medeyko
Copy link
Contributor

medeyko commented Apr 16, 2022

As far as I understand (upd: it was a wrong understanding, as explained below), this is because the heart rate is unlikely to exceed 200 bpm.
However, it can. According to this article, https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3273956/ they registered 600 bpm in a case of an extreme non-fatal tachyarrhythmia.
220 bpm is recorded for healthy athletes.
So I don't see the 256 constant is any worse than 200.

Also it's to count beats in a minute to measure bpm. One can count beats in an arbitrary time period, and then multiply by an adjusting constant.

@watertrainer
Copy link
Author

The fast bpms would show up in the low indices I think.
If the algorithm works any similar to this oneand I understood it correctly.
It looks for the first peak in the PPG signal an then just assumes that all othe peaks are evenly spaced after that (with some noise reduction etc.) A very quick peak would be in the first few indices and only for very slow bpms more indices would be needed (the data points are collected at (24Hz)[https://github.com/daniel-thompson/wasp-os/blob/f29a6013f5580fd12955563702981707543e23f1/wasp/ppg.py#L109] so for 200 that would be a last peak at 4 seconds and 12 bpm with the slowest heartbeats being around 25 that's even too high of a number.

I think the number 200 is chosen because in the process function of PPG the max resulting index could be around 200 (see here, t3+4 would be the highest index (which I got at a max of 210, don't really know why)

Making it bigger won't break anything, it might not be very useful though and uses a few bytes of memory. We should maybe still do it though because if t3+4 goes to 210 the heart rate measurement will be completely off (and it will access memory outside of the array before this pr).

Copy link
Contributor

@Riksu9000 Riksu9000 left a comment

Choose a reason for hiding this comment

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

A couple observations.

The small changes to dataIndex are hard to notice and review. They're also not really necessary.

In ProcessHeartRate(), dataIndex and DATA_SIZE would be the same value without the slight dataIndex change. DATA_SIZE may be slightly more correct, but only before the actual behaviour is changed.

But the more I think about it, this PR doesn't actually change much apart from adding getRingIndex() does it? Maybe it would be better to add and start using the ring buffer in one PR. As it is, these changes are hard to justify. The code improvements would be a nice separate PR.

lpf {0.11595249, 0.23190498, 0.11595249, -0.72168143, 0.18549138} {
}

int Ppg::Compare(int8_t* d1, int shift, size_t count) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this function changed, apart from adding getRingIndex()?

Copy link
Author

Choose a reason for hiding this comment

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

in the original implementation the pointer was moved forward and then accessed as a 0-based array. That wouldn't work in the ring buffer, because the actual index has to be known to modulo it.

@watertrainer
Copy link
Author

First of all thanks for the review :)

I agree that the PR doesn't really have a justification to be merged yet, I just implemented it because it might be useful for other future features, which I wanted to start working on (e.g. #1094).
And your right the only real thing changed is getRingIndex, I could have written that before ^^>

At the moment dataIndex and DATA_SIZE don't have a difference in the Process method, but only because it's called when dataIndex is 200. That's the whole "point" of the PR, that Process can be called with any value of dataIndex and return a valid HR, not as before, where dataIndex HAD to be equal to DATA_SIZE or else the whole function wouldn't have worked anymore.

@Mindavi
Copy link

Mindavi commented May 6, 2022

This definitely seems useful for e.g. higher frequency updates on the display.

@khimaros
Copy link

khimaros commented Nov 3, 2022

@watertrainer are you still planning to work on this? i'm available to help.

@watertrainer
Copy link
Author

Yes, if you have any ideas how to make this useful for the user I'd be more than happy to work on it together.

@watertrainer watertrainer closed this by deleting the head repository Nov 9, 2023
@khimaros
Copy link

khimaros commented Nov 9, 2023

@watertrainer has this been solved elsewhere?

@watertrainer
Copy link
Author

not that I'm aware of, although there were some heart rate changes a few versions ago, not to sure haven't been following to closely

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.

5 participants