237 lines
8.1 KiB
C++
237 lines
8.1 KiB
C++
#include "led_effects.h"
|
|
/*****************************************************************
|
|
* Configuration *
|
|
*****************************************************************/
|
|
namespace config {
|
|
const uint8_t max_brightness = MAX_BRIGHTNESS;
|
|
#if defined(MIN_BRIGHTNESS)
|
|
const uint8_t min_brightness = MIN_BRIGHTNESS;
|
|
#else
|
|
const uint8_t min_brightness = MAX_BRIGHTNESS;
|
|
#endif
|
|
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
|
|
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 night_mode = false; //NOTE: Use a class instead? NightMode could then be another state.
|
|
|
|
#if !defined(LED_COUNT)
|
|
# define LED_COUNT 12
|
|
#endif
|
|
|
|
const uint16_t led_count = LED_COUNT;
|
|
|
|
#if LED_COUNT == 12
|
|
//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
|
|
const uint16_t co2_ticks[led_count + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
|
|
// 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.
|
|
const uint16_t led_hues[led_count] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
|
|
#elif LED_COUNT == 16
|
|
const uint16_t co2_ticks[led_count + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1100, 1200,
|
|
1300, 1400, 1500, 1600, 1700, 1800, 2000, 2200 }; // [ppm]
|
|
const uint16_t led_hues[led_count] = {21845U, 19859U, 17873U, 15887U, 13901U, 11915U, 9929U, 7943U,
|
|
5957U, 3971U, 1985U, 0, 0, 0, 0, 0}; // [hue angle]
|
|
#else
|
|
# error "Only 12 and 16 LEDs rings are currently supported."
|
|
#endif
|
|
}
|
|
|
|
#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
|
|
|
|
Adafruit_NeoPixel pixels(config::led_count, 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::night_mode = true; // In order to avoid overwriting the desired color next time CO2 is displayed
|
|
pixels.setBrightness(255);
|
|
pixels.fill(color);
|
|
pixels.show();
|
|
}
|
|
|
|
void setupRing() {
|
|
pixels.begin();
|
|
pixels.setBrightness(config::max_brightness);
|
|
LEDsOff();
|
|
sensor_console::defineCommand("night_mode", toggleNightMode, F("(Toggles night mode on/off)"));
|
|
sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)"));
|
|
}
|
|
|
|
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) {
|
|
using namespace config;
|
|
delay(80);
|
|
if (night_mode) {
|
|
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;
|
|
uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * config::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::night_mode) {
|
|
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::brightness_amplitude > 0) {
|
|
breathe(co2);
|
|
}
|
|
}
|
|
|
|
void showRainbowWheel(uint16_t duration_ms) {
|
|
if (config::night_mode) {
|
|
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::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.
|
|
* 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::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 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;
|
|
}
|
|
}
|