Posted on 2 Comments

Breathing LEDs – cracking the algorithm behind our breathing pattern

Breathing LEDs on ThingPulse Icon64 stock firmware

When was the last time you were thinking about your breathing pattern or the algorithm that forms it (or does the pattern form the algorithm)? I guess it is safe to assume that if your answer is “never” or “it’s been ages” you will be like most readers. Also, if the term spirometry does not ring a bell with you: relax, welcome to the club. Spirometry is the measuring of breath by the way.

I remember the first time I consciously thought about my breathing pattern – or breathing patterns in general. It was around 2005 when I bought my first white polycarbonate MacBook. It included the iconic breathing sleep indicator not present anymore in newer models. The breathing LED gave this machine a human touch. Even my wife was thrilled and found this peacefully sleeping object very cute. When I compared the MacBook’s breathing pattern with mine I was stunned at how closely it matched. In hind sight, this was probably my first Apple moment.

This post is about the science behind breathing LEDs. Also, you will learn how to implement this for your Arduino and ESP8266/ESP32 projects.

Apple’s secrets

Apple would not be Apple if they had not filed a patent for this “Breathing status LED indicator” back in 2002. Hence, we can not be sure about all the details of their implementation. However, there are still a few things we do know:

In 2006 Ladyada set out to crack Apple’s secret in what she called a “kludgy reverse engineering attempt“. Kludgy or not, she managed to visualize the wave form on an oscilloscope. Unfortunately, she stopped short of providing code or a formula so we mortals could take a go at it. However, in a comment some Adam Shea suggested

It’s probably exp(sin(t)) to correct for the logarithmic response of the rest of the optical system (LED->eye->brain).

Turns out this yields very promising results. So, let’s look at the math behind it.

Let’s do the math

We should start by plotting f(x) = esin(x) to get a feeling for what we are looking at.

Compared to a simple sinusoid you will notice that the peaks are narrower i.e. more peaky. The graph compensates for this with wider troughs at the other end of the amplitude. This indeed seems to accommodate the earlier “shorter inhalation, longer exhalation” observation quite nicely.

To prepare this formula for source code driving LEDs we need to tune it somewhat. Ultimately, what we are after is some f(time) with a result set of {0…100} with those numbers representing the brightness of the LED. Hence, the amplitude should be 100 such that min(esin(x)) is 0 and max(esin(x)) is 100.

Given the original function above we can derive:

  • min(esin(x)) = e-1 = 1/e = 0.36787944
  • max(esin(x)) = e1 = e = 2.71828182

So, in order to shift the graph down to 0 we will subtract 1/e from all y values. As a interim result we get the following plot for f(x) = esin(x) - 1/e. Note how the maximum y value thus is no longer e but e - 1/e!

In order to stretch the amplitude up to 100 we now need to multiply each value with 100/(e - 1/e) = 42.54590641. Thus, the formula evolves to f(x) = (esin(x) - 1/e) * (100/(e - 1/e)).

Since x is “the time” (e.g. in milliseconds) the final challenge is to adjust it in such a way that we get 12 – 20 waves in 60 seconds i.e. breaths per minute. Multiplying x with PI/2 for example results in a period of 4 and thus a frequency of 15.

From math to code

With the formula massaged enough to represent a breathing pattern with a frequency of 15 breaths per second we can now look at how to integrate it into source code. We stated initially that we would look at how to implement this for Arduino and ESP8266/ESP32 (or any other micro controller that runs Arduino). There are obviously different ways to drive LEDs from Arduino-like devices. Hence, we are limiting the field to just two common strategies: using an LED-library and using PWM directly.

As discussed above, in the formula x represents time in in some unit. Hence, x * PI/2 becomes millis()/2000.0 * PI in the code. Also note how the sketches use constants for 1/e and desired-max-y-value/(e - 1/e) as there is no point in calculating these over and over again.

FastLED

FastLED is a fast and easy LED (animation) library for Arduino. At ThingPulse we use it to drive our LED-based products such as the Icon64 and AMo CO2 monitor. It offers a high level programming interface and abstracts driving different types of LED pixels such as Neopixel or WS2812B. A common scenario is to call some function from loop() that defines LED behavior through FastLED like in the below skeleton.

CRGB leds[LED_COUNT];

void showLed() {
  uint8_t brightness = (exp(sin(millis() / 2000.0 * PI)) - 0.368) * 42.546;
  FastLED.setBrightness(brightness);
  leds[0] = CRGB::Red;
  FastLED.show();
}

void setup() {
  FastLED.addLeds<WS2812B, DATA_PIN>(leds, LED_COUNT);
}

void loop() {
  showLed();
}

The full code is available at https://github.com/ThingPulse/esp32-icon64-a2dp/blob/master/src/main.cpp#L176.

PWM

To drive individual LEDs rather than LED pixels you can apply PWM directly to an output pin through analogWrite().

Note that the max y value in this case is not 100 as with the brightness for FastLED (see above) but 255. analogWrite() accepts values from 0-255. Hence, the second constant for the formula has to be 255/(e - 1/e).

#include <math.h>

void setup() {
  pinMode(11, OUTPUT);
}

void loop() {
  float val = (exp(sin(millis()/2000.0 * PI)) - 0.368) * 108.0;
  analogWrite(11, val);
}

Demo

The short video demonstrates the breathing algorithm as developed above back-to-back with a regular sine-based algorithm with higher frequency. What is your preference? Which one do you like better?

Conclusion

This article walks you through the math required to solve the mystery behind Apple’s iconic breathing status LED indicator. The two sample sketches for Arduino-like devices should give you a head-start for your own implementation. The demo video then raises the question if a breathing pattern algorithm be “better” then a simple sine-based algorithm.

We feel there is no right or wrong; one is not better than the other. Sure, beauty matters. Therefore, we rather claim that one be better suited for a certain use case then the other. That is the reason we eventually changed the fast sine-based pulsing icons in our Icon64 stock firmware to use the breathing algorithm in late 2021. Were we right? Let us know 🙂

Credits

Thank you Adam Shea for suggesting the formula be esin(x).

Inspiration for this article and the code in our Icon64 (revised) stock firmware came from Sean Voisen who blogged about this in 2011.

Avital Pekker published a very interesting article in 2016. He describes how he used a photo diode placed right in front of a MacBook sleep light to collect 3000 samples at 10ms. In essence it’s exactly what Ladyada did except that she measured the voltage directly rather than measuring the emitted light. Avi showed and discussed that the actual curve is some gaussian rather than esin(x).

2 thoughts on “Breathing LEDs – cracking the algorithm behind our breathing pattern

  1. I found Avital Pekker’s post (https://avital.ca/notes/a-closer-look-at-apples-breathing-light) and it was fascinating. Clearly written by a man on a mission. It’s a shame he only posted 5 times before abandoning his blog.
    At the end of the day, the goal is to produce something pleasing to the eye and you clearly accomplish that. Apple did too, but if Avital is to be believed, they took a much more complicated path to get there.
    If you search for “gaussian breathing LED” it’s clear that many others have explored this topic. Looking back, I am reminded that I used it in support of an art installation my wife did at University of Houston as part of her MFA in Scenic Design (for the Theatre). I even found a spreadsheet full of calculations I had used in my pursuit of the perfect breathing effect.
    Thanks for bringing it all back for me.

    1. Very sorry I forgot to link to Avi’s article, actually meant to.

Leave a Reply

Your email address will not be published.