/*
 * ESP32 Spiral Lamp
 *
 * Made from a lamp purchased from Home Depot
 *
 * This code controls a modern spiral lamp
 * which lights up whenever motion is detected.
 *
 * The lamp consists of two strands of WS2812B LEDs. Strand 1
 * is the shorter strand with 61 LEDs. Strand 2 is the
 * longer strand with 70 LEDs. FastLED is configured to
 * treat both strands as one long one.
 *
 * The ESP32 controller sleeps until motion is
 * detected, then lights up for LIGHT_DISPLAY_SECS
 * then goes back to sleep awaiting further motion.
 *
 * If the mode control pushbutton is held while power is
 * applied or when the light wakes up from sleeping,
 * the mode control state will be toggled:
 * off to on or on to off. If mode control is OFF,
 * the lamp will only light up when motion is
 * detected. If mode control is ON, the lamp will 
 * also light up every SLEEP_TIME_IN_MINS, without motion
 * being required.
 *
 * The motion control state is held in flash memory (EEPROM)
 * so it survives power cycling.
 *
 * Hardware:
 *  WEMOS D1 Mini ESP32 Module
 *  131 WS2812B NeoPixels
 *  AM312 PIR motion sensor module
 *  Pushbutton (mode control pushbutton)
 *  A IN4001 diode to separate USB power from power
 *  supply power
 *  A 5V 5 amp (or more) power supply
 *
 * Compile using the WEMOS D1 Mini ESP32 board type 
 * Uses the FastLED library
 *
 * Concept, Design and Implementation by: Craig A. Lindley
 * Version: 1.1
 * Last Update: 02/20/2025
 */

// Include FastLED library
#include <FastLED.h>

// Include library to read and write from flash memory
#include <EEPROM.h>

// Include library for adding entropy to random number generator
#include <bootloader_random.h>

// Pin driving the shorter WS2812B strand 1
#define LED_STRAND_1_PIN 19

// Count of WS2812B LEDs on strand 1
#define LED_STRAND_1_COUNT 61

// Pin driving the longer WS2812B strand 2
#define LED_STRAND_2_PIN 18

// Count of WS2812B LEDs on strand 2
#define LED_STRAND_2_COUNT 70

// Total count of LEDs
#define TOTAL_LED_COUNT (LED_STRAND_1_COUNT + LED_STRAND_2_COUNT)

// PIR sensor is connected to GPIO pin 25
// Only RTC GPIO pins can be used for waking
#define PIR_PIN GPIO_NUM_25

// Mode control toggle pushbutton pin
#define MC_PIN 22

// Mode control toggle byte is the 0th or first byte of EEPROM memory
#define TOGGLE_BYTE_ADDR 0

// Conversion factor from seconds to micro seconds
#define SECS_TO_USECS 1000000

// Timer wake up timing
#define SLEEP_TIME_IN_MINS 5
#define SLEEP_TIME_IN_SECS (SLEEP_TIME_IN_MINS * 60)

// Light display timing
#define LIGHT_DISPLAY_SECS 120
#define LIGHT_DISPLAY_MSECS (LIGHT_DISPLAY_SECS * 1000)

// A delay to keep the frame rate modest
#define FRAMES_PER_SECOND 60

/**************************************************************************/
/***         Forward references to all of the display patterns          ***/
/**************************************************************************/

void pattern0();
void pattern1();
void pattern2();
void pattern3();
void pattern4();
void pattern5();
void pattern6();
void pattern7();
void pattern8();
void pattern9();
void pattern10();
void pattern11();

/**************************************************************************/
/***                     Misc Constants and Variables                   ***/
/**************************************************************************/

// Buffer for all of the LED data
CRGB leds[TOTAL_LED_COUNT];

// Global hue values used by some of the patterns
uint8_t gHue = 0;
uint8_t hue = 0;

// Frame counter
uint16_t frameCount = 0;

/**************************************************************************/
/***                     Pattern Selection Functions                    ***/
/**************************************************************************/

// A function pointer for executing display patterns from an array
typedef void (*pt2Function)();

// Array of pointers to pattern display functions
pt2Function patternFunctions[] = {

  pattern0,
  pattern1,
  pattern2,
  pattern3,
  pattern4,
  pattern5,
  pattern6,
  pattern7,
  pattern8,
  pattern9,
  pattern10,
  pattern11,
};

// Determine the number of display patterns from the entries in the array
#define NUMBER_OF_PATTERNS (sizeof(patternFunctions) / sizeof(pt2Function))

/**************************************************************************/
/***                          Pattern 0 Function                        ***/
/**************************************************************************/

void pattern0() {

  // FastLED's built-in rainbow generator
  fill_rainbow(leds, TOTAL_LED_COUNT, gHue, 12);
}

/**************************************************************************/
/***                          Pattern 1 Function                        ***/
/**************************************************************************/

void pattern1() {

  // FastLED's built-in rainbow generator plus some random sparkly glitter
  fill_rainbow(leds, TOTAL_LED_COUNT, gHue, 12);

  // Add some glitter
  if (random8() < 80) {
    leds[random16(TOTAL_LED_COUNT)] += CRGB::White;
  }
}

/**************************************************************************/
/***                          Pattern 2 Function                        ***/
/**************************************************************************/

void pattern2() {

  // Confetti
  // Random colored speckles that blink in and fade smoothly
  fadeToBlackBy(leds, TOTAL_LED_COUNT, 10);
  int pos = random16(TOTAL_LED_COUNT);
  leds[pos] += CHSV(gHue + random8(64), 200, 255);
}

/**************************************************************************/
/***                          Pattern 3 Function                        ***/
/**************************************************************************/

void pattern3() {

  // Sinelon
  // A colored dot sweeping back and forth, with fading trails
  fadeToBlackBy(leds, TOTAL_LED_COUNT, 20);
  int pos = beatsin16(13, 0, TOTAL_LED_COUNT - 1);
  leds[pos] += CHSV(gHue, 255, 192);
}

/**************************************************************************/
/***                          Pattern 4 Function                        ***/
/**************************************************************************/

void pattern4() {

  // BPM
  // Colored stripes pulsing at a defined Beats-Per-Minute (BPM)
  uint8_t BeatsPerMinute = 62;
  CRGBPalette16 palette = PartyColors_p;
  uint8_t beat = beatsin8(BeatsPerMinute, 64, 255);
  for (int i = 0; i < TOTAL_LED_COUNT; i++) {
    leds[i] = ColorFromPalette(palette, gHue + (i * 2), beat - gHue + (i * 10));
  }
}

/**************************************************************************/
/***                          Pattern 5 Function                        ***/
/**************************************************************************/

void pattern5() {

  // Juggle
  // Eight colored dots, weaving in and out of sync with each other
  fadeToBlackBy(leds, TOTAL_LED_COUNT, 20);
  uint8_t dothue = 0;
  for (int i = 0; i < 8; i++) {
    leds[beatsin16(i + 7, 0, TOTAL_LED_COUNT - 1)] |= CHSV(dothue, 200, 255);
    dothue += 32;
  }
}

/**************************************************************************/
/***                          Pattern 6 Functions                       ***/
/**************************************************************************/

// Fire2012 by Mark Kriegsman, July 2012
//
// This basic one-dimensional 'fire' simulation works roughly as follows:
// There's a underlying array of 'heat' cells, that model the temperature
// at each point along the line.  Every cycle through the simulation,
// four steps are performed:
//  1) All cells cool down a little bit, losing heat to the air
//  2) The heat from each cell drifts 'up' and diffuses a little
//  3) Sometimes randomly new 'sparks' of heat are added at the bottom
//  4) The heat from each cell is rendered as a color into the leds array
//     The heat-to-color mapping uses a black-body radiation approximation.
//
// Temperature is in arbitrary units from 0 (cold black) to 255 (white hot).
//
// This simulation scales it self a bit depending on TOTAL_LED_COUNT; it should look
// "OK" on anywhere from 20 to 100 LEDs without too much tweaking.
//
// I recommend running this simulation at anywhere from 30-100 frames per second,
// meaning an interframe delay of about 10-35 milliseconds.
//
// Looks best on a high-density LED setup (60+ pixels/meter).
//
// There are two main parameters you can play with to control the look and
// feel of your fire: COOLING (used in step 1 above), and SPARKING (used
// in step 3 above).
//
// COOLING: How much does the air cool as it rises?
// Less cooling = taller flames.  More cooling = shorter flames.
// Default 50, suggested range 20-100
#define COOLING 75

// SPARKING: What chance (out of 255) is there that a new spark will be lit?
// Higher chance = more roaring fire.  Lower chance = more flickery fire.
// Default 120, suggested range 50-200.
#define SPARKING 60

void pattern6() {

  // Array of temperature readings at each simulation cell
  static uint8_t heat[TOTAL_LED_COUNT];

  // Step 1.  Cool down every cell a little
  for (int i = 0; i < TOTAL_LED_COUNT; i++) {
    heat[i] = qsub8(heat[i], random8(0, ((COOLING * 10) / TOTAL_LED_COUNT) + 2));
  }

  // Step 2.  Heat from each cell drifts 'up' and diffuses a little
  for (int k = TOTAL_LED_COUNT - 1; k >= 2; k--) {
    heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
  }

  // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
  if (random8() < SPARKING) {
    int y = random8(7);
    heat[y] = qadd8(heat[y], random8(160, 255));
  }

  // Step 4.  Map from heat cells to LED colors
  for (int j = 0; j < TOTAL_LED_COUNT; j++) {
    leds[j] = HeatColor(heat[j]);
  }
}

/**************************************************************************/
/***                          Pattern 7 Functions                       ***/
/**************************************************************************/

// In this animation, there are four "layers" of waves of light.
//
// Each layer moves independently, and each is scaled separately.
//
// All four wave layers are added together on top of each other, and then
// another filter is applied that adds "whitecaps" of brightness where the
// waves line up with each other more.  Finally, another pass is taken
// over the led array to 'deepen' (dim) the blues and greens.
//
// The speed and scale and motion each layer varies slowly within independent
// hand-chosen ranges, which is why the code has a lot of low-speed 'beatsin8' functions
// with a lot of oddly specific numeric ranges.
//
// These three custom blue-green color palettes were inspired by the colors found in
// the waters off the southern coast of California, https://goo.gl/maps/QQgd97jjHesHZVxQ7
//
CRGBPalette16 pacifica_palette_1 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
                                     0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 };
CRGBPalette16 pacifica_palette_2 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,
                                     0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F };
CRGBPalette16 pacifica_palette_3 = { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33,
                                     0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF };

// Add one layer of waves into the led array
void pacifica_one_layer(CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) {
  uint16_t ci = cistart;
  uint16_t waveangle = ioff;
  uint16_t wavescale_half = (wavescale / 2) + 20;
  for (uint16_t i = 0; i < TOTAL_LED_COUNT; i++) {
    waveangle += 250;
    uint16_t s16 = sin16(waveangle) + 32768;
    uint16_t cs = scale16(s16, wavescale_half) + wavescale_half;
    ci += cs;
    uint16_t sindex16 = sin16(ci) + 32768;
    uint8_t sindex8 = scale16(sindex16, 240);
    CRGB c = ColorFromPalette(p, sindex8, bri, LINEARBLEND);
    leds[i] += c;
  }
}

// Add extra 'white' to areas where the four layers of light have lined up brightly
void pacifica_add_whitecaps() {
  uint8_t basethreshold = beatsin8(9, 55, 65);
  uint8_t wave = beat8(7);

  for (uint16_t i = 0; i < TOTAL_LED_COUNT; i++) {
    uint8_t threshold = scale8(sin8(wave), 20) + basethreshold;
    wave += 7;
    uint8_t l = leds[i].getAverageLight();
    if (l > threshold) {
      uint8_t overage = l - threshold;
      uint8_t overage2 = qadd8(overage, overage);
      leds[i] += CRGB(overage, overage2, qadd8(overage2, overage2));
    }
  }
}

// Deepen the blues and greens
void pacifica_deepen_colors() {
  for (uint16_t i = 0; i < TOTAL_LED_COUNT; i++) {
    leds[i].blue = scale8(leds[i].blue, 145);
    leds[i].green = scale8(leds[i].green, 200);
    leds[i] |= CRGB(2, 5, 7);
  }
}

void pattern7() {

  // Pacifica
  // Increment the four "color index start" counters, one for each wave layer.
  // Each is incremented at a different speed, and the speeds vary over time.
  static uint16_t sCIStart1, sCIStart2, sCIStart3, sCIStart4;
  static uint32_t sLastms = 0;
  uint32_t ms = GET_MILLIS();
  uint32_t deltams = ms - sLastms;
  sLastms = ms;
  uint16_t speedfactor1 = beatsin16(3, 179, 269);
  uint16_t speedfactor2 = beatsin16(4, 179, 269);
  uint32_t deltams1 = (deltams * speedfactor1) / 256;
  uint32_t deltams2 = (deltams * speedfactor2) / 256;
  uint32_t deltams21 = (deltams1 + deltams2) / 2;
  sCIStart1 += (deltams1 * beatsin88(1011, 10, 13));
  sCIStart2 -= (deltams21 * beatsin88(777, 8, 11));
  sCIStart3 -= (deltams1 * beatsin88(501, 5, 7));
  sCIStart4 -= (deltams2 * beatsin88(257, 4, 6));

  // Clear out the LED array to a dim background blue-green
  fill_solid(leds, TOTAL_LED_COUNT, CRGB(2, 6, 10));

  // Render each of four layers, with different scales and speeds, that vary over time
  pacifica_one_layer(pacifica_palette_1, sCIStart1, beatsin16(3, 11 * 256, 14 * 256), beatsin8(10, 70, 130), 0 - beat16(301));
  pacifica_one_layer(pacifica_palette_2, sCIStart2, beatsin16(4, 6 * 256, 9 * 256), beatsin8(17, 40, 80), beat16(401));
  pacifica_one_layer(pacifica_palette_3, sCIStart3, 6 * 256, beatsin8(9, 10, 38), 0 - beat16(503));
  pacifica_one_layer(pacifica_palette_3, sCIStart4, 5 * 256, beatsin8(8, 10, 28), beat16(601));

  // Add brighter 'whitecaps' where the waves lines up more
  pacifica_add_whitecaps();

  // Deepen the blues and greens a bit
  pacifica_deepen_colors();
}

/**************************************************************************/
/***                          Pattern 8 Functions                       ***/
/**************************************************************************/

// The speed at which the bubbles travel upward
double speeds[3];

// The position along the lamp arms
double positions[3];

// Update position of bubble at index
void updateBubblePosition(int index) {
  positions[index] += speeds[index];
  if (((int)positions[index]) >= 60) {
    positions[index] = 0.0;
    speeds[index] = 0.0;
  }
}

void calculateBubbleSpeed(int index) {
  int rn = random(1, 5);
  speeds[index] = rn / 12.0;
}
// Paint water background
// The first 61 LEDs in both arms of the lamp
// are painted the same. The last 10 of the
// longer arm are painted differently.
void paintWaterBackground() {
  for (int i = 0; i < 53; i++) {
    leds[i] = CRGB(0x0000FF);
    leds[i + LED_STRAND_1_COUNT] = CRGB(0x0000FF);
  }
  for (int i = 53; i < LED_STRAND_1_COUNT; i++) {
    leds[i] = CRGB(0x1E5B99);
    leds[i + LED_STRAND_1_COUNT] = CRGB(0x1E5B99);
  }
  for (int i = 120; i < TOTAL_LED_COUNT; i++) {
    leds[i] = CRGB(0xE0F7FA);
  }
}

// Light specified LED to white in each arm
// led is 0 .. 60
void lightBubble(int led) {
  leds[led] = CRGB(0xFFFFFF);
  leds[led + LED_STRAND_1_COUNT] = CRGB(0xFFFFFF);
}

// Only allow a reset and new bubbles after
// all previous bubble have topped out
bool qualifiedReset() {
  if (frameCount != 0) {
    return false;
  }

  if (speeds[0] > 0.1) {
    if (positions[0] < 60) {
      return false;
    }
  }
  if (speeds[1] > 0.1) {
    if (positions[1] < 60) {
      return false;
    }
  }
  if (speeds[2] > 0.1) {
    if (positions[2] < 60) {
      return false;
    }
  }
  return true;
}

// Bubbles
void pattern8() {
  paintWaterBackground();

  // Time for reset and new bubles ?
  if (qualifiedReset()) {
    // Do initialization
    speeds[0] = speeds[1] = speeds[2] = 0.0;
    positions[0] = positions[1] = positions[2] = 0.0;

    // Count is 1, 2 or 3 bubbles
    int bubbleCount = 1 + random(3);
    switch (bubbleCount) {
      case 1:
        calculateBubbleSpeed(0);
        break;
      case 2:
        calculateBubbleSpeed(0);
        calculateBubbleSpeed(1);
        break;
      case 3:
        calculateBubbleSpeed(0);
        calculateBubbleSpeed(1);
        calculateBubbleSpeed(2);
        break;
    }
  }
  // Update bubble positions
  if (speeds[0] > 0.1) {
    updateBubblePosition(0);
    lightBubble((int)positions[0]);
  }
  if (speeds[1] > 0.1) {
    updateBubblePosition(1);
    lightBubble((int)positions[1]);
  }
  if (speeds[2] > 0.1) {
    updateBubblePosition(2);
    lightBubble((int)positions[2]);
  }
}

/**************************************************************************/
/***                          Pattern 9 Functions                       ***/
/**************************************************************************/

void pattern9() {

  // Random colored speckles that blink in and fade smoothly
  fadeToBlackBy(leds, TOTAL_LED_COUNT, 10);
  int pos = random16(TOTAL_LED_COUNT);
  leds[pos] = CHSV(random(256), 255, random(128, 256));
}

/**************************************************************************/
/***                          Pattern 10 Functions                       ***/
/**************************************************************************/

void pattern10() {

  int pos = random16(TOTAL_LED_COUNT);
  leds[pos] = CHSV(random(256), 255, random(196, 256));
}

/**************************************************************************/
/***                          Pattern 11 Functions                       ***/
/**************************************************************************/

void pattern11() {

  // Time for initialization ?
  if ((frameCount == 0) && (((int)positions[0]) >= 60)) {
    // Yes it is
    speeds[0] = 0.25;
    positions[0] = 0.0;
    hue = gHue;
  }
  fadeToBlackBy(leds, TOTAL_LED_COUNT, 10);

  positions[0] += speeds[0];

  leds[(int)positions[0]] = CHSV(hue, 255, random(196, 256));
  leds[(int)positions[0] + LED_STRAND_1_COUNT] = CHSV(hue, 255, random(196, 256));

  hue++;
}

/**************************************************************************/
/***                            Program Setup                           ***/
/**************************************************************************/

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\nWaking Up");

  // Instantiate the WS2812B driver for strand 1
  FastLED.addLeds<WS2812B, LED_STRAND_1_PIN, GRB>(leds, 0, LED_STRAND_1_COUNT);

  // Instantiate the WS2812B driver for strand 2
  FastLED.addLeds<WS2812B, LED_STRAND_2_PIN, GRB>(leds, LED_STRAND_1_COUNT, LED_STRAND_2_COUNT);

  // Set overall brightness of strands
  FastLED.setBrightness(96);

  // Limit current draw
  FastLED.setMaxPowerInVoltsAndMilliamps(5, 5000);

  // Increase randomness
  bootloader_random_enable();

  // Initialize EEPROM library, allocate 1 byte for use storing mode
  EEPROM.begin(1);

  // Configure Mode control toggle pin
  pinMode(MC_PIN, INPUT_PULLUP);

  delay(50);

  // Is the mode control pushbutton active ?
  // If so toggle saved state
  if (digitalRead(MC_PIN) == LOW) {
    // Serial.println("MC active");

    // Switch is active so toggle saved state
    if (EEPROM.read(TOGGLE_BYTE_ADDR) == 0) {
      // Write a value of 1
      EEPROM.write(TOGGLE_BYTE_ADDR, 1);
    } else {
      // Write a value of 0
      EEPROM.write(TOGGLE_BYTE_ADDR, 0);
    }
    // Commit the new value
    EEPROM.commit();
  }

  // Configure GPIO input pin to read sensor
  pinMode(PIR_PIN, INPUT);

  // Configure ESP32 to wake when PIR sensor detects motion
  esp_sleep_enable_ext0_wakeup(PIR_PIN, HIGH);

  // Pick random pattern index
  int pi = random(NUMBER_OF_PATTERNS);
  // pi = 11;  // For testing

  // Reset frame count
  frameCount = 0;

  // Do initialization
  speeds[0] = speeds[1] = speeds[2] = 0.0;
  positions[0] = positions[1] = positions[2] = 60.0;

  // Determine time in the future for pattern display to end
  unsigned long futureTime = millis() + LIGHT_DISPLAY_MSECS;

  // Run until time display time is up
  while (futureTime > millis()) {

    // Advance frame count
    frameCount++;
    if (frameCount >= LED_STRAND_1_COUNT) {
      frameCount = 0;
    }

    // Slowly cycle the hue through the rainbow
    EVERY_N_MILLISECONDS(20) {
      gHue++;
    }

    // Execute the selected pattern
    (*patternFunctions[pi])();

    // Display the pattern created above
    FastLED.show();

    // Insert a delay to keep the frame rate modest
    FastLED.delay(1000 / FRAMES_PER_SECOND);
  }

  // Turn all the LEDs off
  FastLED.clear();
  FastLED.show();

  // Possibly configure ESP32 to wake up from timer as well
  if (EEPROM.read(TOGGLE_BYTE_ADDR) == 1) {
    // Serial.println("Auto");
    esp_sleep_enable_timer_wakeup(SLEEP_TIME_IN_SECS * SECS_TO_USECS);
  }

  Serial.println("Going to Sleep");

  // Put the ESP32 to sleep
  esp_deep_sleep_start();
}

/**************************************************************************/
/***                             Program Loop                           ***/
/**************************************************************************/

// Will never get here
void loop() {
}
