#include "led_effects.h" #include "web_config.h" #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" /***************************************************************** * Configuration * *****************************************************************/ namespace config { const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel const uint16_t poor_air_quality_ppm = 1600; // Above this threshold, LED breathing effect is faster. bool display_led = true; // Will be set to false during "night mode". //NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index. uint16_t co2_ticks[16 + 1] = { 0, 500, 600, 700, 800, 900, 1000 }; // rest will be filled later // For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°), // LEDs >= 1600ppm will be pure red (hue angle 0°), LEDs in-between will be yellowish. uint16_t led_hues[16]; } #if defined(ESP8266) // NeoPixels on GPIO05, aka D1 on ESP8266. const int NEOPIXELS_PIN = 5; #elif defined(ESP32) // NeoPixels on GPIO23 on ESP32. To avoid conflict with LoRa_SCK on TTGO. const int NEOPIXELS_PIN = 23; #endif // config::led_count is not yet known Adafruit_NeoPixel pixels(0, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800); namespace led_effects { //On-board LED on D4, aka GPIO02 const int ONBOARD_LED_PIN = 2; void setupOnBoardLED() { pinMode(ONBOARD_LED_PIN, OUTPUT); } void onBoardLEDOff() { //NOTE: OFF is LOW on ESP32 and HIGH on ESP8266 :-/ #ifdef ESP8266 digitalWrite(ONBOARD_LED_PIN, HIGH); #else digitalWrite(ONBOARD_LED_PIN, LOW); #endif } void onBoardLEDOn() { #ifdef ESP8266 digitalWrite(ONBOARD_LED_PIN, LOW); #else digitalWrite(ONBOARD_LED_PIN, HIGH); #endif } void LEDsOff() { pixels.clear(); pixels.show(); onBoardLEDOff(); } void showColor(int32_t color) { config::display_led = false; // In order to avoid overwriting the desired color next time CO2 is displayed pixels.setBrightness(255); pixels.fill(color); pixels.show(); } void setupRing() { Serial.print(F("Ring : ")); Serial.print(*config::led_count); Serial.println(F(" LEDs.")); 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." } pixels.begin(); pixels.setBrightness(*config::max_brightness); LEDsOff(); sensor_console::defineIntCommand("led", turnLEDsOnOff, F("0/1 (Turns LEDs on/off)")); sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)")); } void toggleNightMode() { 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!")); } else { Serial.println(F("Night mode!")); LEDsOff(); } } //NOTE: basically one iteration of KITT wheel void showWaitingLED(uint32_t color) { using namespace config; delay(80); if (!display_led) { return; } static uint16_t kitt_offset = 0; pixels.clear(); for (int j = kitt_tail; j >= 0; j--) { int ledNumber = abs((kitt_offset - j + *led_count) % (2 * *led_count) - *led_count) % *led_count; // Triangular function pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255); } pixels.show(); kitt_offset++; } // 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 * *config::led_count; ++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 >= config::co2_ticks[ledId + 1]) { return 255; } else { if (2 * co2 >= config::co2_ticks[ledId] + config::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; } } } /** * If enabled, slowly varies the brightness between MAX_BRIGHTNESS & MIN_BRIGHTNESS. */ void breathe(int16_t co2) { static uint8_t breathing_offset = 0; uint8_t brightness_amplitude = *config::max_brightness - *config::min_brightness; uint16_t brightness = *config::min_brightness + pixels.sine8(breathing_offset) * brightness_amplitude / 255; pixels.setBrightness(brightness); pixels.show(); breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing. } /** * Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS. */ void displayCO2color(uint16_t co2) { if (!config::display_led) { return; } pixels.setBrightness(*config::max_brightness); for (int ledId = 0; ledId < *config::led_count; ++ledId) { uint8_t brightness = getLedBrightness(co2, ledId); pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness)); } pixels.show(); if (*config::max_brightness > *config::min_brightness) { breathe(co2); } } void showRainbowWheel(uint16_t duration_ms) { if (!config::display_led) { return; } static uint16_t wheel_offset = 0; static uint16_t sine_offset = 0; unsigned long t0 = millis(); pixels.setBrightness(*config::max_brightness); while (millis() - t0 < duration_ms) { for (int i = 0; i < *config::led_count; i++) { pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / *config::led_count + wheel_offset)); wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2; } pixels.show(); delay(10); } } void redAlert() { if (!config::display_led) { onBoardLEDOn(); delay(500); onBoardLEDOff(); delay(500); return; } for (int i = 0; i < 10; i++) { pixels.setBrightness(static_cast(*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. * 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. */ bool countdownToZero() { if (!config::display_led) { Serial.println(F("Night mode. Not doing anything.")); delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed. return false; } pixels.fill(color::blue); pixels.show(); int countdown; for (countdown = *config::led_count; countdown >= 0 && !digitalRead(0); countdown--) { pixels.setPixelColor(countdown, color::black); pixels.show(); Serial.println(countdown); delay(500); } return countdown < 0; } }