led_effects.cpp 9.43 KB
Newer Older
1
#include "led_effects.h"
2

Eric Duminil's avatar
Eric Duminil committed
3
#include "web_config.h"
4
5
6
7
8
9
10
#include "sensor_console.h"

// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
// https://github.com/adafruit/Adafruit_NeoPixel
// Documentation : http://adafruit.github.io/Adafruit_NeoPixel/html/class_adafruit___neo_pixel.html
#include "src/lib/Adafruit_NeoPixel/Adafruit_NeoPixel.h"

11
12
13
14
/*****************************************************************
 * Configuration                                                 *
 *****************************************************************/
namespace config {
15
  const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
16
  const uint16_t poor_air_quality_ppm = 1600; // Above this threshold, LED breathing effect is faster.
17
  bool display_led = true; // Will be set to false during "night mode".
18
  //NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
19
  uint16_t co2_ticks[16 + 1] = { 0, 500, 600, 700, 800, 900, 1000 }; // rest will be filled later
20
  // For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
21
  // LEDs >= 1600ppm will be pure red (hue angle 0°), LEDs in-between will be yellowish.
22
  uint16_t led_hues[16];
23
24
}

25
26
#if defined(ESP8266)
// NeoPixels on GPIO05, aka D1 on ESP8266.
27
const int NEOPIXELS_PIN = 5;
28
29
30
31
32
#elif defined(ESP32)
// NeoPixels on GPIO23 on ESP32. To avoid conflict with LoRa_SCK on TTGO.
const int NEOPIXELS_PIN = 23;
#endif

33
34
// config::led_count is not yet known
Adafruit_NeoPixel pixels(0, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
35

36
namespace led_effects {
37
38
39
40
41
42
43
44
  //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
45
46
    //NOTE: OFF is LOW on ESP32 and HIGH on ESP8266 :-/
#ifdef ESP8266
47
    digitalWrite(ONBOARD_LED_PIN, HIGH);
Eric Duminil's avatar
Eric Duminil committed
48
49
50
#else
    digitalWrite(ONBOARD_LED_PIN, LOW);
#endif
51
52
53
  }

  void onBoardLEDOn() {
Eric Duminil's avatar
Eric Duminil committed
54
#ifdef ESP8266
55
    digitalWrite(ONBOARD_LED_PIN, LOW);
Eric Duminil's avatar
Eric Duminil committed
56
57
58
#else
    digitalWrite(ONBOARD_LED_PIN, HIGH);
#endif
59
60
  }

Eric Duminil's avatar
Eric Duminil committed
61
62
63
64
65
66
  void LEDsOff() {
    pixels.clear();
    pixels.show();
    onBoardLEDOff();
  }

Eric Duminil's avatar
Eric Duminil committed
67
  void showColor(int32_t color) {
68
    config::display_led = false; // In order to avoid overwriting the desired color next time CO2 is displayed
Eric Duminil's avatar
Eric Duminil committed
69
70
71
72
73
    pixels.setBrightness(255);
    pixels.fill(color);
    pixels.show();
  }

74
  void setupRing() {
Eric Duminil's avatar
Info    
Eric Duminil committed
75
76
77
78
    Serial.print(F("Ring     : "));
    Serial.print(*config::led_count);
    Serial.println(F(" LEDs."));

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
    pixels.updateLength(*config::led_count);

    if (*config::led_count == 12) {
      config::co2_ticks[7] = 1200;
      config::co2_ticks[8] = 1400;
      config::co2_ticks[9] = 1600;
      config::co2_ticks[10] = 1800;
      config::co2_ticks[11] = 2000;
      config::co2_ticks[12] = 2200;

      config::led_hues[0] = 21845U;
      config::led_hues[1] = 19114U;
      config::led_hues[2] = 16383U;
      config::led_hues[3] = 13653U;
      config::led_hues[4] = 10922U;
      config::led_hues[5] = 8191U;
      config::led_hues[6] = 5461U;
      config::led_hues[7] = 2730U;
      config::led_hues[8] = 0;
      config::led_hues[9] = 0;
      config::led_hues[10] = 0;
      config::led_hues[11] = 0;
    } else if (*config::led_count == 16) {
      config::co2_ticks[7] = 1100;
      config::co2_ticks[8] = 1200;
      config::co2_ticks[9] = 1300;
      config::co2_ticks[10] = 1400;
      config::co2_ticks[11] = 1500;
      config::co2_ticks[12] = 1600;
      config::co2_ticks[13] = 1700;
      config::co2_ticks[14] = 1800;
      config::co2_ticks[15] = 2000;
      config::co2_ticks[16] = 2200;

      config::led_hues[0] = 21845U;
      config::led_hues[1] = 19859U;
      config::led_hues[2] = 17873U;
      config::led_hues[3] = 15887U;
      config::led_hues[4] = 13901U;
      config::led_hues[5] = 11915U;
      config::led_hues[6] = 9929U;
      config::led_hues[7] = 7943U;
      config::led_hues[8] = 5957U;
      config::led_hues[9] = 3971U;
      config::led_hues[10] = 1985U;
      config::led_hues[11] = 0;
      config::led_hues[12] = 0;
      config::led_hues[13] = 0;
      config::led_hues[14] = 0;
      config::led_hues[15] = 0;
    } else {
      // "Only 12 and 16 LEDs rings are currently supported."
    }
132
    pixels.begin();
Eric Duminil's avatar
Eric Duminil committed
133
    pixels.setBrightness(*config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
134
    LEDsOff();
135
    sensor_console::defineIntCommand("led", turnLEDsOnOff, F("0/1 (Turns LEDs on/off)"));
136
    sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)"));
137
138
139
  }

  void toggleNightMode() {
140
141
142
143
144
145
146
    turnLEDsOnOff(!config::display_led);
  }

  void turnLEDsOnOff(int32_t display_led) {
    config::display_led = display_led;
    if (config::display_led) {
      Serial.println(F("LEDs are on!"));
147
    } else {
148
149
      Serial.println(F("Night mode!"));
      LEDsOff();
150
151
152
    }
  }

153
//NOTE: basically one iteration of KITT wheel
154
  void showWaitingLED(uint32_t color) {
155
    using namespace config;
156
    delay(80);
157
    if (!display_led) {
158
159
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
160
    static uint16_t kitt_offset = 0;
161
    pixels.clear();
162
    for (int j = kitt_tail; j >= 0; j--) {
163
      int ledNumber = abs((kitt_offset - j + *led_count) % (2 * *led_count) - *led_count) % *led_count; // Triangular function
164
165
166
      pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
    }
    pixels.show();
Eric Duminil's avatar
Eric Duminil committed
167
    kitt_offset++;
168
169
  }

170
171
172
// 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.
173
  void showKITTWheel(uint32_t color, uint16_t duration_s) {
Eric Duminil's avatar
Eric Duminil committed
174
    pixels.setBrightness(*config::max_brightness);
175
    for (int i = 0; i < duration_s * *config::led_count; ++i) {
176
177
178
179
180
181
182
183
184
      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) {
185
    if (co2 >= config::co2_ticks[ledId + 1]) {
186
187
      return 255;
    } else {
188
      if (2 * co2 >= config::co2_ticks[ledId] + config::co2_ticks[ledId + 1]) {
189
190
191
192
193
194
195
196
197
        // 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;
      }
    }
  }

198
199
200
201
202
  /**
   * 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
203
204
    uint8_t brightness_amplitude = *config::max_brightness - *config::min_brightness;
    uint16_t brightness = *config::min_brightness + pixels.sine8(breathing_offset) * brightness_amplitude / 255;
205
206
    pixels.setBrightness(brightness);
    pixels.show();
207
    breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing.
208
209
  }

210
211
212
213
  /**
   * Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS.
   */
  void displayCO2color(uint16_t co2) {
214
    if (!config::display_led) {
215
216
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
217
    pixels.setBrightness(*config::max_brightness);
218
    for (int ledId = 0; ledId < *config::led_count; ++ledId) {
219
      uint8_t brightness = getLedBrightness(co2, ledId);
220
      pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
221
222
    }
    pixels.show();
Eric Duminil's avatar
Eric Duminil committed
223
    if (*config::max_brightness > *config::min_brightness) {
224
225
      breathe(co2);
    }
226
227
  }

228
  void showRainbowWheel(uint16_t duration_ms) {
229
    if (!config::display_led) {
230
231
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
232
    static uint16_t wheel_offset = 0;
233
    static uint16_t sine_offset = 0;
Eric Duminil's avatar
Eric Duminil committed
234
    unsigned long t0 = millis();
Eric Duminil's avatar
Eric Duminil committed
235
    pixels.setBrightness(*config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
236
    while (millis() - t0 < duration_ms) {
237
238
      for (int i = 0; i < *config::led_count; i++) {
        pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / *config::led_count + wheel_offset));
239
        wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2;
240
241
242
243
244
245
246
      }
      pixels.show();
      delay(10);
    }
  }

  void redAlert() {
247
    if (!config::display_led) {
248
249
250
251
252
253
254
      onBoardLEDOn();
      delay(500);
      onBoardLEDOff();
      delay(500);
      return;
    }
    for (int i = 0; i < 10; i++) {
Eric Duminil's avatar
Eric Duminil committed
255
      pixels.setBrightness(static_cast<int>(*config::max_brightness * (1 - i * 0.1)));
256
257
258
259
260
261
262
      delay(50);
      pixels.fill(color::red);
      pixels.show();
    }
  }

  /**
263
264
265
266
267
   * Displays a complete blue circle, and starts removing LEDs one by one.
   * Does nothing in night mode and returns false then. Returns true if
   * the countdown has finished. Can be used for calibration, e.g. when countdown is 0.
   * NOTE: This function is blocking and returns only after the button has
   * been released or after every LED has been turned off.
268
   */
269
  bool countdownToZero() {
270
    if (!config::display_led) {
Eric Duminil's avatar
Eric Duminil committed
271
      Serial.println(F("Night mode. Not doing anything."));
272
      delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
273
      return false;
274
275
276
277
    }
    pixels.fill(color::blue);
    pixels.show();
    int countdown;
278
    for (countdown = *config::led_count; countdown >= 0 && !digitalRead(0); countdown--) {
279
280
281
282
283
      pixels.setPixelColor(countdown, color::black);
      pixels.show();
      Serial.println(countdown);
      delay(500);
    }
284
    return countdown < 0;
285
286
  }
}