led_effects.cpp 9.55 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

Eric Duminil's avatar
Eric Duminil committed
33
// config::led_count is not yet known, will be set later.
34
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
    Serial.print(F("Ring     : "));
76
    Serial.print(config::led_count);
Eric Duminil's avatar
Info    
Eric Duminil committed
77
78
    Serial.println(F(" LEDs."));

79
    pixels.updateLength(config::led_count);
80

81
    if (config::led_count == 12) {
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
      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;
101
    } else if (config::led_count == 16) {
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
      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."
131
      config::display_led = false;
132
    }
133
    pixels.begin();
134
    pixels.setBrightness(config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
135
    LEDsOff();
136
    sensor_console::defineIntCommand("led", turnLEDsOnOff, F("0/1 (Turns LEDs on/off)"));
137
    sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)"));
138
139
140
  }

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

  void turnLEDsOnOff(int32_t display_led) {
Eric Duminil's avatar
Eric Duminil committed
145
    //TODO: Could use strategy pattern with 2 different Effects classes.
146
147
148
    config::display_led = display_led;
    if (config::display_led) {
      Serial.println(F("LEDs are on!"));
149
    } else {
150
151
      Serial.println(F("Night mode!"));
      LEDsOff();
152
153
154
    }
  }

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

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

200
201
202
203
204
  /**
   * If enabled, slowly varies the brightness between MAX_BRIGHTNESS & MIN_BRIGHTNESS.
   */
  void breathe(int16_t co2) {
    static uint8_t breathing_offset = 0;
205
206
    uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
    uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * brightness_amplitude / 255;
207
208
    pixels.setBrightness(brightness);
    pixels.show();
209
    breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing.
210
211
  }

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

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

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

  /**
265
266
267
268
269
   * 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.
270
   */
271
  bool countdownToZero() {
272
    if (!config::display_led) {
Eric Duminil's avatar
Eric Duminil committed
273
      Serial.println(F("Night mode. Not doing anything."));
274
      delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
275
      return false;
276
277
278
279
    }
    pixels.fill(color::blue);
    pixels.show();
    int countdown;
280
    for (countdown = config::led_count; countdown >= 0 && !digitalRead(0); countdown--) {
281
282
283
284
285
      pixels.setPixelColor(countdown, color::black);
      pixels.show();
      Serial.println(countdown);
      delay(500);
    }
286
    return countdown < 0;
287
288
  }
}