Pulse heart

Pulse heart

The pulse heart shows a four-chambered heart animation.

The design goal was to come up with some sort of approximation of heart rate based on accelerometer data.

I attempted to build a machine learning model, modeling off of my own heart rate, building a bluetooth data capture system with a wireless chest strap heart rate monitor.

I spent ages trying to build a model that could be run on the ATtiny84, which is a very basic microcontroller that does not support floating point or division. I designed features based on addition and bit-shifting only, and came up with more than 20 features.

I trained the model in a python notebook, carefully recreating the features I had designed in C to behave exactly the same way in python.

The result was something like 80% accurate, which is not that bad an approximation. (There’s no way to get heart rate for real without an on-skin sensor, though it could be receieved over BLE efficiently I scoped that out of this project.)

I liked the idea of modelling my own heart and giving it to people. The badge would, in a sense, approximate what my heart would do if I were acting like you. I like the idea of creating something that not just looks pretty but feels meaningful.

The circuit uses 8 banks of leds for each chamber wall, including intersections.

Integer only heart rate model

I trained a linear regression model using only integer math.

The model was:

uint32_t(
    + 259
    + scale16(getFilterValue(mov10), 808)
    + scale16(getFilterValue(mov12), 1193)
    + scale16(getFilterValue(mov06), 619)
    + scale16(getFilterValue(mov08), 661)
    + scale16(getFilterValue(buckets[0]), 1463)
    - scale16(getFilterValue(buckets[1]), 1117)
    - scale16(getFilterValue(buckets[2]), 2313)
    - scale16(getFilterValue(buckets[3]), 522)
    - scale16(getFilterValue(buckets[4]), 3373)
    - scale16(getFilterValue(buckets[5]), 2037)
    - scale16(getFilterValue(buckets[6]), 8812)
    - scale16(getFilterValue(avgTimeSinceCross), 1594)
    ) << 8;

A simple integer low-pass filter can be constructed using only shifts and addition. This is essential for feature development without floating point:

void smoothInt(uint16_t sample, uint8_t bits, int32_t *filter) {
  int32_t local_sample = ((int32_t) sample) << 16;
  *filter += (local_sample - *filter) >> bits;
}

Another essential function which lets you effectively do division through reciprocal multiplication:

uint16_t scale16(uint16_t i, fract16 scale) {
    uint16_t result;
    result = ((uint32_t)(i) * (uint32_t)(scale)) >> 16;
    return result;
}

Although the ATtiny84 does not even have a hardware multiply operation.

Project Video