led_effects.cpp 7.39 KB
Newer Older
1
2
3
4
5
6
#include "led_effects.h"
/*****************************************************************
 * Configuration                                                 *
 *****************************************************************/
namespace config {
  const uint8_t max_brightness = MAX_BRIGHTNESS;
Eric Duminil's avatar
Eric Duminil committed
7
8
9
10
11
#if defined(MIN_BRIGHTNESS)
  const uint8_t min_brightness = MIN_BRIGHTNESS;
#else
  const uint8_t min_brightness = MAX_BRIGHTNESS;
#endif
12
  const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
13
  const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
14
  const uint16_t poor_air_quality_ppm = 1600; // Above this threshold, LED breathing effect is faster.
15
  //NOTE: Use a class instead? NightMode could then be another state.
Eric Duminil's avatar
Eric Duminil committed
16
  bool night_mode = true;
17
18
}

19
20
#if defined(ESP8266)
// NeoPixels on GPIO05, aka D1 on ESP8266.
21
const int NEOPIXELS_PIN = 5;
22
23
24
25
26
#elif defined(ESP32)
// NeoPixels on GPIO23 on ESP32. To avoid conflict with LoRa_SCK on TTGO.
const int NEOPIXELS_PIN = 23;
#endif

27
28
29
const int NUMPIXELS = 12;
//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
Eric Duminil's avatar
Eric Duminil committed
30
// const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm]
31
32
// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
// last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish.
Eric Duminil's avatar
Eric Duminil committed
33
34
35
36
37
// For reference, this python code can be used to generate the array
//    NUMPIXELS = 12
//    RED_LEDS = 4
//    hues = [ (2**16-1) // 3 * max(NUMPIXELS - RED_LEDS - i, 0) // (NUMPIXELS - RED_LEDS) for i in range(NUMPIXELS) ]
//    '{' + ', '.join([str(hue) + ('U' if hue else '') for hue in hues]) + '}; // [hue angle]'
Eric Duminil's avatar
Eric Duminil committed
38
const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
Eric Duminil's avatar
Eric Duminil committed
39
// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
40
41
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);

42
namespace led_effects {
43
44
45
46
47
48
49
50
  //On-board LED on D4, aka GPIO02
  const int ONBOARD_LED_PIN = 2;

  void setupOnBoardLED() {
    pinMode(ONBOARD_LED_PIN, OUTPUT);
  }

  void onBoardLEDOff() {
Eric Duminil's avatar
Eric Duminil committed
51
52
    //NOTE: OFF is LOW on ESP32 and HIGH on ESP8266 :-/
#ifdef ESP8266
53
    digitalWrite(ONBOARD_LED_PIN, HIGH);
Eric Duminil's avatar
Eric Duminil committed
54
55
56
#else
    digitalWrite(ONBOARD_LED_PIN, LOW);
#endif
57
58
59
  }

  void onBoardLEDOn() {
Eric Duminil's avatar
Eric Duminil committed
60
#ifdef ESP8266
61
    digitalWrite(ONBOARD_LED_PIN, LOW);
Eric Duminil's avatar
Eric Duminil committed
62
63
64
#else
    digitalWrite(ONBOARD_LED_PIN, HIGH);
#endif
65
66
  }

Eric Duminil's avatar
Eric Duminil committed
67
68
69
70
71
72
  void LEDsOff() {
    pixels.clear();
    pixels.show();
    onBoardLEDOff();
  }

73
74
75
  void setupRing() {
    pixels.begin();
    pixels.setBrightness(config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
76
    LEDsOff();
77
78
79
80
81
82
  }

  void toggleNightMode() {
    config::night_mode = !config::night_mode;
    if (config::night_mode) {
      Serial.println(F("NIGHT MODE!"));
Eric Duminil's avatar
Eric Duminil committed
83
      LEDsOff();
84
85
86
87
88
89
90
91
92
93
94
    } else {
      Serial.println(F("DAY MODE!"));
    }
  }

  //NOTE: basically one iteration of KITT wheel
  void showWaitingLED(uint32_t color) {
    delay(80);
    if (config::night_mode) {
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
95
    static uint16_t kitt_offset = 0;
96
97
    pixels.clear();
    for (int j = config::kitt_tail; j >= 0; j--) {
Eric Duminil's avatar
Eric Duminil committed
98
      int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
99
100
101
      pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
    }
    pixels.show();
Eric Duminil's avatar
Eric Duminil committed
102
    kitt_offset++;
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
  }

  // Start K.I.T.T. led effect. Red color as default.
  // Simulate a moving LED with tail. First LED starts at 0, and moves along a triangular function. The tail follows, with decreasing brightness.
  // Takes approximately 1s for each direction.
  void showKITTWheel(uint32_t color, uint16_t duration_s) {
    pixels.setBrightness(config::max_brightness);
    for (int i = 0; i < duration_s * NUMPIXELS; ++i) {
      showWaitingLED(color);
    }
  }

  /*
   * For a given CO2 level and ledId, which brightness should be displayed? 0 for off, 255 for on. Something in-between for partial LED.
   * For example, for 1500ppm, every LED between 0 and 7 (500 -> 1400ppm) should be on, LED at 8 (1600ppm) should be half-on.
   */
  uint8_t getLedBrightness(uint16_t co2, int ledId) {
    if (co2 >= CO2_TICKS[ledId + 1]) {
      return 255;
    } else {
      if (2 * co2 >= CO2_TICKS[ledId] + CO2_TICKS[ledId + 1]) {
        // Show partial LED if co2 more than halfway between ticks.
        return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10%
      } else {
        // LED off because co2 below previous tick
        return 0;
      }
    }
  }

133
134
135
136
137
  /**
   * If enabled, slowly varies the brightness between MAX_BRIGHTNESS & MIN_BRIGHTNESS.
   */
  void breathe(int16_t co2) {
    static uint8_t breathing_offset = 0;
Eric Duminil's avatar
Eric Duminil committed
138
    uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * config::brightness_amplitude / 255;
139
140
    pixels.setBrightness(brightness);
    pixels.show();
141
    breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing.
142
143
  }

144
145
146
147
148
149
150
151
152
153
154
155
156
  /**
   * Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS.
   */
  void displayCO2color(uint16_t co2) {
    if (config::night_mode) {
      return;
    }
    pixels.setBrightness(config::max_brightness);
    for (int ledId = 0; ledId < NUMPIXELS; ++ledId) {
      uint8_t brightness = getLedBrightness(co2, ledId);
      pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness));
    }
    pixels.show();
Eric Duminil's avatar
Eric Duminil committed
157
    if (config::brightness_amplitude > 0) {
158
159
      breathe(co2);
    }
160
161
  }

Eric Duminil's avatar
Eric Duminil committed
162
  void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) {
163
164
165
    if (config::night_mode) {
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
166
    static uint16_t wheel_offset = 0;
Eric Duminil's avatar
Eric Duminil committed
167
    unsigned long t0 = millis();
168
    pixels.setBrightness(config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
169
    while (millis() - t0 < duration_ms) {
170
      for (int i = 0; i < NUMPIXELS; i++) {
Eric Duminil's avatar
Eric Duminil committed
171
172
        pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
        wheel_offset += hue_increment;
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
      }
      pixels.show();
      delay(10);
    }
  }

  void redAlert() {
    if (config::night_mode) {
      onBoardLEDOn();
      delay(500);
      onBoardLEDOff();
      delay(500);
      return;
    }
    for (int i = 0; i < 10; i++) {
      pixels.setBrightness(static_cast<int>(config::max_brightness * (1 - i * 0.1)));
      delay(50);
      pixels.fill(color::red);
      pixels.show();
    }
  }

  /**
   * Displays a complete blue circle, and starts removing LEDs one by one. Returns the number of remaining LEDs.
   * Can be used for calibration, e.g. when countdown is 0. Does not work in night mode.
   */
  int countdownToZero() {
    if (config::night_mode) {
Eric Duminil's avatar
Eric Duminil committed
201
      Serial.println(F("Night mode. Not doing anything."));
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
      delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
      return 1;
    }
    pixels.fill(color::blue);
    pixels.show();
    int countdown;
    for (countdown = NUMPIXELS; countdown >= 0 && !digitalRead(0); countdown--) {
      pixels.setPixelColor(countdown, color::black);
      pixels.show();
      Serial.println(countdown);
      delay(500);
    }
    return countdown;
  }
}