led_effects.cpp 7.29 KB
Newer Older
1
2
3
4
5
6
#include "led_effects.h"
/*****************************************************************
 * Configuration                                                 *
 *****************************************************************/
namespace config {
  const uint8_t max_brightness = MAX_BRIGHTNESS;
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
14
  const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
  //NOTE: Use a class instead? NightMode could then be another state.
15
16
17
  bool night_mode = false;
}

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

26
27
28
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
29
// 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]
30
31
// 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
32
33
34
35
36
// 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
37
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
38
// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
39
40
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);

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

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

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

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

  void toggleNightMode() {
    config::night_mode = !config::night_mode;
    if (config::night_mode) {
      Serial.println(F("NIGHT MODE!"));
Eric Duminil's avatar
Eric Duminil committed
82
      LEDsOff();
83
84
85
86
87
88
89
90
91
92
93
    } 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
94
    static uint16_t kitt_offset = 0;
95
96
    pixels.clear();
    for (int j = config::kitt_tail; j >= 0; j--) {
Eric Duminil's avatar
Eric Duminil committed
97
      int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
98
99
100
      pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
    }
    pixels.show();
Eric Duminil's avatar
Eric Duminil committed
101
    kitt_offset++;
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
  }

  // 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;
      }
    }
  }

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


145
146
147
148
149
150
151
152
153
154
155
156
157
  /**
   * 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();
158
159
160
    if (config::brightness_amplitude > 0){
      breathe(co2);
    }
161
162
  }

Eric Duminil's avatar
Eric Duminil committed
163
  void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) {
164
165
166
    if (config::night_mode) {
      return;
    }
Eric Duminil's avatar
Eric Duminil committed
167
    static uint16_t wheel_offset = 0;
Eric Duminil's avatar
Eric Duminil committed
168
    unsigned long t0 = millis();
169
    pixels.setBrightness(config::max_brightness);
Eric Duminil's avatar
Eric Duminil committed
170
    while (millis() - t0 < duration_ms) {
171
      for (int i = 0; i < NUMPIXELS; i++) {
Eric Duminil's avatar
Eric Duminil committed
172
173
        pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
        wheel_offset += hue_increment;
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
201
      }
      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
202
      Serial.println(F("Night mode. Not doing anything."));
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
      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;
  }
}