/*
 * Gray_fastled_millis_demo
 * Published at:
 *   https://wokwi.com/arduino/projects/309731114102030913
 * Description: 
 *   Example of replacing normal FastLED neopixel code that has delays with millis based code
 *   Also demonstrate two separate patterns simultaneously
 *   Also demonstrate doing other things at the same time within the sketch such as button reading and potentiometer reading
 *   Eventually you will want to do more things at once in arduino so that's where you get stuck trying to combine sketches. 
 *   But millis requires a different thinking so lets explore the conversion.
 *   I highly recommend this primer for the delay to millis learning https://learn.adafruit.com/multi-tasking-the-arduino-part-1/overview
 * 
 * Why I made this:
 *   I see a lot of people struggle with running neopixel/LED code and combining sketches and switching over
 *   from delay to millis as they want to do all the things in one sketch. 
 *   I wanted to provide a well written demo that might help people making the transition.
 *   I think coding style is very important even for demos as you never know if you will go back and use
 *   it again many years later.
 *   I know this is a bit much for a beginner but hopefully it will help intermediate coders bump up their game.
 * 
 *
 * Code by: Gray Mack
  * License: MIT License
 *          https://choosealicense.com/licenses/mit/
 * Created: 09/14/2021
 * Board: Arduino Uno or similar 
 * 
 * Connections: 
 *    NeoPixel Ws2812b strip, button, potentimeter, piezo speaker element or amplifier. See the pin constants section
 *    I use 16 led ring and 2 additional pixels. I use neopixels 
 *    (arduino)-----[0]-[1]-[2]-[3]-[4]-[5]-[6]-[7]-[8]-[9]-[10]-[11]-[12]-[13]-[14]-[15]--------[14]-[15]
 *                 | primary pattern runs on these pixels                                |      |secondary|
 * 
 * Neopixel layout: 18 leds
 * Main pattern LEDs                             0..15
 * Secondary pattern LEDs                        16..17
 * 
 * 
 * 09/14/2021 initial code creation
 * 09/15/2021 updates based on feedback from Alastair A.
 */



// ----[ included libraries ]------------------------------------
#include <FastLED.h> // LED pixel library for neopixels
#include <Bounce2.h> // button debounce library

// ----[ pin definitions ]------------------------------------
#define PIN_BUTTON 2 // input button to ground
// For led chips like Neopixels, which have a data line, ground, and power, you just
// need to define PIN_PIXEL_DATA.  For led chipsets that are SPI based (four wires - data, clock,
// ground, and power), like the LPD8806, define both PIN_PIXEL_DATA and PIN_PIXEL_CLOCK
#define PIN_PIXEL_DATA 7
//#define PIN_PIXEL_CLOCK 13
#define PIN_PIEZO_SPEAKER_ELEMENT 11
#define PIN_POTENTIOMETER A0 // 1k to 100k potentiometer wiper pin. one side connected to GND and the other to 5v


// ----[ constants ]------------------------------------
#define NUM_LEDS 32 // How many leds in your strip
#define PRIMARY_PATTERN_LEDS_BEGIN 0
#define PRIMARY_PATTERN_LEDS_END 15
#define PRIMARY_PATTERN_NUM_LEDS 16
#define SECONDARY_PATTERN_LEDS_BEGIN 16
#define SECONDARY_PATTERN_LEDS_END 17
#define SECONDARY_PATTERN_NUM_LEDS_END 2
#define NOTE_A4 440
#define NOTE_C5 523
#define NOTE_DURATION_MSEC 80
#define NUM_PRIMARY_PATTERNS 2
#define ADC_MAX_VALUE 1023 // UNO has a 10bit ADC so 0 to 2^10-1 range, other devices may have 12bit ADC
#define POT_CONTROLS_COLOR true


// ----[ global variables ]------------------------------------
CRGB leds[NUM_LEDS]; // Define the array of leds for FastLED
int PotentimeterValue = 0;
Bounce2::Button SpeedButton = Bounce2::Button();
unsigned long SecondaryPatternAnimationFrameEveryMsec = 500;  // 1000msec/500 => 2 times per second
bool EnableAnimateRailroadCrossing = true; // Enable the secondary pattern
int WhichPrimaryPattern = 0;
CRGB RunningDotColor = CRGB::Blue;


// ----[ code ]------------------------------------

void setup() 
{
  Serial.begin(9600);
  Serial.println("Arduino reset");
  LEDS.addLeds<WS2812,PIN_PIXEL_DATA,GRB>(leds,NUM_LEDS); // my pixels are RGB but wokwi pixels are GRB
  // LEDS.addLeds<LPD6803, PIN_PIXEL_DATA, PIN_PIXEL_CLOCK, RGB>(leds, NUM_LEDS); // example for data and clock style pixels
  LEDS.setBrightness(255); // half brightness
  LEDS.setMaxPowerInVoltsAndMilliamps(5, 400);
  PotentimeterValue = analogRead(PIN_POTENTIOMETER);
  SpeedButton.attach(PIN_BUTTON, INPUT_PULLUP); // use internal pull-ups
  SpeedButton.interval(5); // debounce interval Msec
  SpeedButton.setPressedState(LOW); // indicate that the low state corresponds to physically pressing the button
}


void loop()
{
   // we try to keep loop simple so it tells the story of what we are doing through function calls
   // call the reentrant functions as often as we can, each will only act when its timer is ready, otherwise they just return with no action
   WhichPrimaryPattern = GetCurrentPrimaryPattern(WhichPrimaryPattern); // is it time to change patterns?
   switch(WhichPrimaryPattern) // handle 0 to NUM_PRIMARY_PATTERNS-1 patterns
   {
      case 0: AnimateRunningDot(); break;
      case 1: AnimateRandomPixel(); break;
      default: AnimateRunningDot(); break; // anything else goes back to the first pattern
   }
   AnimateRailroadCrossing();
   ReadButtons();
   PlaySound(); // every 3 minutes (or if a button was pushed then ReadButtons plays a sound)
   ReadPotentimeter(); // every 10sec
   ConsoleWritePotentimeterValue(); // every 120sec
}


void AnimateRunningDot()
{ // light runs down the pixels bucket brigade style
/*
  void loop()
  {
    for (int ledDotPosition = PRIMARY_PATTERN_LEDS_BEGIN; ledDotPosition < PRIMARY_PATTERN_NUM_LEDS; ledDotPosition++) 
    {
       leds[ledDotPosition] = CRGB::Blue;
       FastLED.show(); //start the leds
       leds[ledDotPosition] = CRGB::Black; //clear the led
       delay(50); //Wait before moving to next let
    }
  }
Lets rewrite this as a re-entrant function. We have to change the structure and loose the for loop.
Lets make ledDotPositiona a 'static' variable which will retain its value between calls as if it were global.
*/
   const unsigned long ANIMATION_FRAME_EVERY_MSEC = 50;  // 1000msec/50 => 20 times per second
   static unsigned long AnimationMillis = 0;
   static int ledDotPosition = 0;
      
   if(millis() - AnimationMillis < ANIMATION_FRAME_EVERY_MSEC) return; 
   // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   
   leds[ledDotPosition] = RunningDotColor;
   FastLED.show();
   leds[ledDotPosition] = CRGB::Black;
   if(++ledDotPosition > PRIMARY_PATTERN_LEDS_END) ledDotPosition = PRIMARY_PATTERN_LEDS_BEGIN; // increment the value but don't let it go over the number of leds
}

//usage //FillPixelData(PRIMARY_PATTERN_LEDS_BEGIN, PRIMARY_PATTERN_LEDS_END, CRGB::Black);
//void FillPixelData(int start, int end, const struct CRGB & color)
//{
//    for(int i=start; i<=end; i++) leds[i] = color;
//}


void AnimateRandomPixel()
{ // light one random pixel at a time to a random color
/*
  int previousPix = PRIMARY_PATTERN_LEDS_BEGIN;
  void loop()
   {
      int newPix = random(PRIMARY_PATTERN_LEDS_BEGIN, PRIMARY_PATTERN_LEDS_END+1);
      CRGB newColor = CHSV(random(0, 256), 255, 255);
      leds[previousPix] = CRGB::Black;
      leds[newPix] = newColor;
      FastLED.show();
      previousPix = newPix;
      delay(30);
   }
   Rewriting as a re-entrant function. Lets make previousPix static to hold the value between calls
*/
   const unsigned long ANIMATION_FRAME_EVERY_MSEC = 25;  // 1000msec/25 => 40 times per second
   static unsigned long AnimationMillis = 0; // this is a different variable space than the one in used in AnimateRunningDot though it has the same name
   static int previousPix = PRIMARY_PATTERN_LEDS_BEGIN;
   
   if(millis() - AnimationMillis < ANIMATION_FRAME_EVERY_MSEC) return; 
   // function will do nothing and just return until its time change the LEDs.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time

   int newPix = random(PRIMARY_PATTERN_LEDS_BEGIN, PRIMARY_PATTERN_LEDS_END+1);
   CRGB newColor = CHSV(random(0, 256), 255, 255); // This is a FastLED function that lets us convert 0-255 value to a color in the hue rainbow and we want a random color https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
   leds[previousPix] = CRGB::Black;
   leds[newPix] = newColor;
   FastLED.show();
   previousPix = newPix;
}


void AnimateRailroadCrossing()
{ // Alternate blinking the LEDs
/*
  void loop()
  {
    leds[SECONDARY_PATTERN_LEDS_BEGIN] = CRGB::Red;
    leds[SECONDARY_PATTERN_LEDS_END] = CRGB::Black;
    FastLED.show();
    delay(500);
    leds[SECONDARY_PATTERN_LEDS_BEGIN] = CRGB::Black;
    leds[SECONDARY_PATTERN_LEDS_END] = CRGB::Red;
    FastLED.show();
    delay(500);
  }   
  Rewriting as a re-entrant function. Lets use a boolean to keep track of if we are lighting the left or right one
*/
   
   static unsigned long AnimationMillis = 0; // this is a different variable space than the one in used in AnimateRunningDot though it has the same name
   static bool Left = true;
      
   if(millis() - AnimationMillis < SecondaryPatternAnimationFrameEveryMsec) return; 
   // In other functions I used a local constant but here SecondaryPatternAnimationFrameEveryMsec is global so the speed can be changed at runtime by other code when button pressed
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time

   if(EnableAnimateRailroadCrossing)
   {
      if(Left)
      {
        leds[SECONDARY_PATTERN_LEDS_BEGIN] = CRGB::Red;
        leds[SECONDARY_PATTERN_LEDS_END] = CRGB::Black;
      }
      else
      {
        leds[SECONDARY_PATTERN_LEDS_BEGIN] = CRGB::Black;
        leds[SECONDARY_PATTERN_LEDS_END] = CRGB::Red;
      }
   }
   else // disable mode (both pixels off)
   {
     leds[SECONDARY_PATTERN_LEDS_BEGIN] = CRGB::Black;
     leds[SECONDARY_PATTERN_LEDS_END] = CRGB::Black;
   }
   FastLED.show();
   Left = !Left;
}


// Utilize Bounce2 library to debounced and read a button as an event. This will change the speed of the secondary pattern
void ReadButtons()
{
  SpeedButton.update();
  if(SpeedButton.pressed())
  {
    Serial.println("Button pressed, playing tone C5");
    tone(PIN_PIEZO_SPEAKER_ELEMENT, NOTE_C5, NOTE_DURATION_MSEC); // background tone
    switch(SecondaryPatternAnimationFrameEveryMsec)
    {  // cycle through a set of flash speeds 100ms->500ms->2000ms->150ms(lights disabled)->back to 100ms
      case 100:
         EnableAnimateRailroadCrossing = true;
         SecondaryPatternAnimationFrameEveryMsec = 500; 
         break;
      case 500: 
         EnableAnimateRailroadCrossing = true;
         SecondaryPatternAnimationFrameEveryMsec = 2000; 
         break;
      case 2000: 
         EnableAnimateRailroadCrossing = true;
         SecondaryPatternAnimationFrameEveryMsec = 150; 
         break;
      case 150: 
         EnableAnimateRailroadCrossing = false; // disabled
         SecondaryPatternAnimationFrameEveryMsec = 100; 
         break;
      default: // anything else goes back to first option
         EnableAnimateRailroadCrossing = true;
         SecondaryPatternAnimationFrameEveryMsec = 100; 
         break;
    }
  }
}


// use the tone library to play a beep every so often as a background sound effect
void PlaySound()
{
   const unsigned long PLAY_SOUND_EVERY_MSEC = 3UL * 60000UL;  // every 3 minutes
   static unsigned long lastSoundMillis = 0;
   
   if(millis() - lastSoundMillis < PLAY_SOUND_EVERY_MSEC) return; // function will do nothing and just return until its time
   
   lastSoundMillis = millis(); // update the time so that we won't be called again until its time
   // note: A better way to stay more timing accurate is to add the constant wait time back to the variable that holds the time 
   // rather than get the latest millis, example: lastSoundMillis += PLAY_SOUND_EVERY_MSEC;
   // though that is problematic if for some reason the setup or loop processing ever take longer than the frequency 
   // of calling the function so drifting the time a little bit by using the latest millis is safer.


   tone(PIN_PIEZO_SPEAKER_ELEMENT, NOTE_A4, NOTE_DURATION_MSEC); // background tone
   Serial.println("Playing tone A4");
}


// Read a potentiometer into the global PotentimeterValue. 
// For efficiency, since we intend the user to only adjust this occasionally, 
// we aren't trying for fast realtime reactive reading but it can easily be read faster if needed down to a few milliseconds
// We could also add smoothing or slow the reaction of change, scale its value, only change the value when a button is pushed, etc
void ReadPotentimeter()
{
   const unsigned long READ_POT_EVERY_MSEC = 10000UL;  // every 10 seconds
   static unsigned long potReadMillis = 0;
   
   if(millis() - potReadMillis < READ_POT_EVERY_MSEC) return; // function will do nothing and just return until its time
   
   potReadMillis = millis(); // update the time so that we won't be called again until its time

   PotentimeterValue = analogRead(PIN_POTENTIOMETER);
   if(POT_CONTROLS_COLOR) RunningDotColor = CHSV(map(PotentimeterValue, 0, ADC_MAX_VALUE, 0, 255), 255, 255);
}


//Occasionally provide a readout of the value
void ConsoleWritePotentimeterValue()
{
   const unsigned long CONSOLE_WRITE_EVERY_MSEC = 120000UL;  // every 120 seconds
   static unsigned long lastConsoleWritedMillis = 0;
   
   if(millis() - lastConsoleWritedMillis < CONSOLE_WRITE_EVERY_MSEC) return; // function will do nothing and just return until its time
   
   lastConsoleWritedMillis = millis(); // update the time so that we won't be called again until its time

   Serial.print("Pot value: ");
   Serial.println(PotentimeterValue);
}


// Provide the current pattern number as input and we will return the same value or change to a new value. 
// Could have also made this a void function that changes the global value instead
int GetCurrentPrimaryPattern(int currentPattern)
{
   const unsigned long SWITCH_PATTERNS_EVERY_MSEC = 60000UL; // every 1 minute
   static unsigned long LastPatternSwitchMillis = 0;
   if(millis() - LastPatternSwitchMillis < SWITCH_PATTERNS_EVERY_MSEC) return currentPattern; // function will do nothing and just return until its time
   
   LastPatternSwitchMillis = millis(); // update the time so that we won't be called again until its time
   Serial.println("Next pattern");
   return (currentPattern+1) % NUM_PRIMARY_PATTERNS;
   // This code just picks the next pattern when the timer expires so the modulus % 2 will cycle it back to zero as 0->1->0->1->.... changing every minute
}


// exercises for the reader
// make the buttons tone change basted on the pot setting
// make the pot to color change have a better response time
// add another pot to change the speed of the running dot pattern
// add another button that changes the patters
// add a third primary pattern such as all fade on then off or some pattern from https://github.com/FastLED/FastLED/blob/master/examples/DemoReel100/DemoReel100.ino
// instead of occasionally playing a note, add a melody in the background by combining this code https://www.arduino.cc/en/Tutorial/BuiltInExamples/toneMelody into the sketch
// build it in the real world and light a model or costume