//------------------------------------------------------
// Color Combination Lock
// RSP 2026.08.26
// Target: Pro Mini 5V
//
// Diabolical color lock:
//    The combination is colors in sequence.
//    Starts out all red; press any button to start.
//    Press the next color in sequence if it appears; if it doesn't appear, press any color.
//    When the combination is entered correctly, all flash green.
//    If the code is wrong up, it continues to prompt randomly.
//    To restart, press any two buttons simultaneously.
//
// To set combination:
//    Use serial monitor (9600)
//    Enter "combination=x"
//      e.g. combination=rgbw
//    Color letters are r,g,b,w,m,a,y (red,grn,blu,whi,mag,aqua,yel).
//------------------------------------------------------

#include <Adafruit_NeoPixel.h>

// 4 color buttons
#define N_BUTTONS 4
const byte BTN_PIN[N_BUTTONS] = { 5,6,7,8 };

#define PIX_PIN   4
#define NUMPIXELS N_BUTTONS
// 5x5mm SMT LED strip
Adafruit_NeoPixel pixels(NUMPIXELS, PIX_PIN, NEO_RGB + NEO_KHZ800);

// door latch
#define LATCH_PIN 13

//------------------------------------------------------
#include <EEPROM.h>

#define MAX_DIGITS 10
struct
{ byte digits;
  char combination[MAX_DIGITS];
} config;

void initConfig()
{ // set default combination
  config.digits = 4;
  strncpy( config.combination, "rgbw", 4 );
  EEPROM.put(0,config);
}

//------------------------------------------------------
// coroutine macros

#define coBegin { switch(_state_) { case 0:;
#define coEnd _state_ = 0; }}
#define coDelay(msec) { _state_ = __LINE__; _tm_=millis(); return; case __LINE__: if (millis()-_tm_ < msec) return; }

//------------------------------------------------------

void setup() 
{
  Serial.begin(9600);
  for (byte i=0; i < N_BUTTONS; i++) pinMode( BTN_PIN[i], INPUT_PULLUP );
  pinMode(LATCH_PIN,OUTPUT); digitalWrite(LATCH_PIN,LOW);
  pixels.begin();
  pixels.setBrightness(40);
  // get combination from EEPROM & validate
  EEPROM.get(0,config);
  if (config.digits > MAX_DIGITS)
    initConfig(); // new EEPROM
  else 
    for (byte i=0; i < config.digits; i++) if (lookupColor(config.combination[i]) == 0) { initConfig(); break; }
  Serial.print("combination="); for (byte i=0; i < config.digits; i++) Serial.write(config.combination[i]); Serial.println();    
}

//------------------------------------------------------

// 7 "digit" colors:
//   r = red
//   g = green
//   b = blue
//   w = white
//   m = magenta
//   a = aqua
//   y = yellow
char colors[7] = { 'r','g','b','w','m','a','y' };
char pattern[N_BUTTONS]; // displayed colors to choose from

uint32_t lookupColor( byte ch ) // LED RGB color for color letter
{
  switch (ch)
  { case 'r': return 0x00FF00;
    case 'g': return 0xFF0000;
    case 'b': return 0x0000FF;
    case 'w': return 0xFFFFFF;
    case 'm': return 0xFF00FF;
    case 'a': return 0x00FFFF;
    case 'y': return 0xFFFF00;
    default : return 0x000000;
  }
}

void shuffle() // build pattern[] of random colors
{
  char perm[7]; for (byte i=0; i < 7; i++) perm[i] = colors[i];
  for (byte i=0; i < N_BUTTONS; i++)
  { byte z; do { z = random(7); if (perm[z]) break; } while(1);
    pattern[i] = perm[z];
    perm[z] = 0;
  }
}

void showPattern() // display the pattern
{
  for (byte i=0; i < N_BUTTONS; i++) pixels.setPixelColor(i, lookupColor(pattern[i]) );
  pixels.show();
}

//------------------------------------------------------

boolean  restart  = false;   // timeout or multiple button restart
uint32_t alive_tm = 0;
#define TIMEOUT_MSEC 15000UL // auto reset after inactivity

int8_t detectButton() // detect button presses
{
  // make the combination color positions different every time
  random();
  // serial console
  if (Serial.available())
  { String input = Serial.readStringUntil('\r'); 
           input.toLowerCase();
    if (input.startsWith("combination="))
      do // until something goes wrong
      { String new_combo = input.substring(12);
        Serial.print("new combination entered: "); Serial.println(new_combo);
        if (new_combo.length() > MAX_DIGITS) { Serial.println("TOO MANY DIGITS"); break; }
        if (new_combo.length() == 0) { Serial.println("MISSING COMBINATION"); break; }
        for (byte i=0; i < new_combo.length(); i++)
          if (lookupColor( new_combo.charAt(i) ) == 0) { Serial.print("INVALID COLOR: "); Serial.println(new_combo.charAt(i)); break; }
        // store new combination
        config.digits = new_combo.length();
        strncpy( config.combination, new_combo.c_str(), config.digits );
        EEPROM.put(0,config);
        Serial.print("COMBINATION SAVED: "); for (byte i=0; i < config.digits; i++) Serial.write(config.combination[i]); Serial.println(); 
      } while (0);
  }
  // detect all buttons released OR simultaneous buttons pressed
  byte npressed = 0;
  byte n;
  for (byte i=0; i < N_BUTTONS; i++) if (!digitalRead(BTN_PIN[i])) { n = i;  npressed++; }
  if (npressed) alive_tm = millis();
  if (npressed == 1) return n;
  if (npressed  > 1) restart = true;
  // automatically reset after inactivity
  if (millis()-alive_tm > TIMEOUT_MSEC) restart = true;
  return -1; // no buttons pressed
}

void debounceButtons() // debounce button release
{
  for (uint32_t tm=millis(); millis()-tm < 50; ) 
  { byte npressed = 0;
    for (byte i=0; i < N_BUTTONS; i++) if (!digitalRead(BTN_PIN[i])) npressed++;
    if (npressed > 1) { restart = true; return; }
    if (npressed) tm = millis();
  }
  alive_tm = millis();
}

void myDelay( uint32_t msec ) // delay with restart button sensing
{
  for (uint32_t tm=millis(); millis()-tm < msec; ) detectButton();
}

//------------------------------------------------------

void loop() 
{
  // make sure door is locked 
  digitalWrite(LATCH_PIN,LOW);
  // inactivity or simultaneous buttons restarts the lock
  restart = false;
  // start out all red
  pixels.fill( 0x00FF00 );
  pixels.show();
  // wait for any button to start
  debounceButtons(); if (restart) return; // wait for no buttons pressed
  for (; detectButton() == -1; ) if (restart) return; 
  // input all digits
  for (byte combo_index=0; combo_index < config.digits; combo_index++)
  { // create an input pattern of colors
    shuffle(); // builds pattern[]
    pixels.clear(); pixels.show(); myDelay(1000); if (restart) return; // dramatic pause
    showPattern();
    // wait for color button press
    int8_t b;
    debounceButtons();  if (restart) return;  // wait for no buttons pressed
    for (b = -1; b == -1; b = detectButton() ) if (restart) return; 
    if (pattern[b] == config.combination[combo_index]) continue;
    // didn't match, but that's okay if the color wasn't present
    for (byte i=0; i < N_BUTTONS; i++)
      if (pattern[i] == config.combination[combo_index]) // color is present but user pressed something else
        // user entered wrong combo, present random patterns from now on
        do // forever
        { shuffle();
          pixels.clear(); pixels.show(); myDelay(1000); if (restart) return; // dramatic pause
          showPattern();
          debounceButtons();  if (restart) return;  // wait for no buttons pressed
          for (; detectButton() == -1; ) if (restart) return; // don't care what button is pressed, they're all wrong now
        } while (1);
    combo_index--; // retry this digit
  }
  // flash green until any button is hit
  pixels.clear(); pixels.show(); myDelay(1000); if (restart) return; // dramatic pause
  debounceButtons(); // wait for no buttons pressed
  for (winnerTask(true); detectButton() == -1; ) { alive_tm = millis();  winnerTask(false);  if (restart) return; }
}

void winnerTask(boolean reset) // flash win indicator for 30 seconds
{
  static uint32_t _state_, _tm_; // required coroutine variables
  static uint8_t  loops;
  if (reset) { _state_ = 0;  return; }
  coBegin
    digitalWrite(LATCH_PIN,HIGH); // open the door
    for (loops=0; loops < 30; loops++)
    { pixels.fill( 0xFF0000 ); pixels.show();
      coDelay(500)
      pixels.clear(); pixels.show();
      coDelay(500)
    }
    restart = true;
  coEnd
}
