216 lines
7.3 KiB
C++
216 lines
7.3 KiB
C++
|
#include "led_effects.h"
|
||
|
/*****************************************************************
|
||
|
* Configuration *
|
||
|
*****************************************************************/
|
||
|
namespace config {
|
||
|
const uint8_t max_brightness = MAX_BRIGHTNESS;
|
||
|
const uint8_t min_brightness = MIN_BRIGHTNESS;
|
||
|
const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
|
||
|
}
|
||
|
|
||
|
/*****************************************************************
|
||
|
* Configuration (calculated from above values) *
|
||
|
*****************************************************************/
|
||
|
namespace config //NOTE: Use a class instead? NightMode could then be another state.
|
||
|
{
|
||
|
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
|
||
|
bool night_mode = false;
|
||
|
}
|
||
|
|
||
|
#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
|
||
|
|
||
|
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]
|
||
|
// 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]
|
||
|
// 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.
|
||
|
// 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]'
|
||
|
const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
|
||
|
// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
|
||
|
Adafruit_NeoPixel pixels(NUMPIXELS, 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 setupRing() {
|
||
|
pixels.begin();
|
||
|
pixels.setBrightness(config::max_brightness);
|
||
|
LEDsOff();
|
||
|
}
|
||
|
|
||
|
void toggleNightMode() {
|
||
|
config::night_mode = !config::night_mode;
|
||
|
if (config::night_mode) {
|
||
|
Serial.println(F("NIGHT MODE!"));
|
||
|
LEDsOff();
|
||
|
} 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;
|
||
|
}
|
||
|
static uint16_t kitt_offset = 0;
|
||
|
pixels.clear();
|
||
|
for (int j = config::kitt_tail; j >= 0; j--) {
|
||
|
int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // 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 * 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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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();
|
||
|
}
|
||
|
|
||
|
void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) {
|
||
|
if (config::night_mode) {
|
||
|
return;
|
||
|
}
|
||
|
static uint16_t wheel_offset = 0;
|
||
|
unsigned long t0 = millis();
|
||
|
pixels.setBrightness(config::max_brightness);
|
||
|
while (millis() - t0 < duration_ms) {
|
||
|
for (int i = 0; i < NUMPIXELS; i++) {
|
||
|
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
|
||
|
wheel_offset += hue_increment;
|
||
|
}
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void breathe(int16_t co2) {
|
||
|
if (!config::night_mode) {
|
||
|
static uint16_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 += 3; // breathing speed. +3 looks like slow human breathing.
|
||
|
}
|
||
|
delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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) {
|
||
|
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 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;
|
||
|
}
|
||
|
}
|