/*

Name    : Adyen IPP Staging Terminal State Indicator
Version : 0.8 -0 AVR ONLY !
Date    : 2025-03-22
Author  : Bas van Ritbergen <[email protected]>   
*/


// Basic defaults 
#define CONFIG_IDENTIFIER "ADYENLED"
#define IDENTIFIER_DEFAULT "STGSTRIP"
#define NUM_LEDS_PER_STRIP_DEFAULT 48
#define NUM_STRIPS_DEFAULT 3
#define NUM_GROUPS_PER_STRIP_DEFAULT 6
#define SPACER_WIDTH_DEFAULT 1
#define MAX_TERMINALS 24
#define BLINK_INTERVAL 200
#define COLOR_STATE_1 CRGB::Green
#define COLOR_STATE_2 CRGB::DarkOrange
#define COLOR_STATE_3 CRGB::Red
#define COLOR_STATE_4 0x000080

#define DEBUG true

//  ###########################################################################
// No configurable items below

#define VERSION 1.8

// Define max length for Input/Ouput buffers:
#define MAX_INPUT_LEN 16
#define MAX_OUTPUT_LEN 50

// Auto-set MCU type 
 #define AVR 1
 #define MCU "AVR"

// We definitely need these libraries
#include <Arduino.h>
#include <FastLED.h>

#include <avr/wdt.h>
#include <EEPROM.h>
#define HAS_EEPROM true
#ifndef BOARD_NAME
  #define BOARD_NAME "Unknown AVR"
#endif

// Figure MCU type/serial
#include <MicrocontrollerID.h>

// Declare LedStrip control arrays
//CRGB leds[NUM_STRIPS_DEFAULT+1][NUM_LEDS_PER_STRIP_DEFAULT];
CRGB leds[NUM_STRIPS_DEFAULT+1][288];

// Config data is conviently stored in a struct (to easy store and retrieve from EEPROM/Flash)
// Set defaults, they will be overwritten by load from EEPROM
struct LedData {
  //char *identifier = nullptr;
  char identifier[16] = IDENTIFIER_DEFAULT;
  uint16_t numLedsPerStrip = NUM_LEDS_PER_STRIP_DEFAULT;
  uint8_t numStrips = NUM_STRIPS_DEFAULT;
  uint8_t numGroupsPerStrip = NUM_GROUPS_PER_STRIP_DEFAULT;
  uint8_t spacerWidth = SPACER_WIDTH_DEFAULT;
  uint16_t blinkinterval = BLINK_INTERVAL;
  CRGB state_color[5] = {CRGB::Black, COLOR_STATE_1, COLOR_STATE_2, COLOR_STATE_3, COLOR_STATE_4 };
} LedConfig = {};


// Input data from serial is stores in an array for further processing.
char inputBuffer[MAX_INPUT_LEN + 1]; // +1 for null terminator
uint8_t bufferIndex = 0;


// For blinking feature we need some extra global parameters
bool BlinkState[MAX_TERMINALS] = {0};
CRGB BlinkColor[MAX_TERMINALS] = {0};
bool blink=false;
uint32_t lastToggleTimes;

// HelpText
const char HelpText[705] PROGMEM = 
  "  Syntax:\n"
  "    H                    This help\n"
  "    T<TermID>:<State>    Set Terminal state. TermID: 1-48 and State: 0-9\n"
  "    X                    Set all states to off (same as sending'A:0'\n"
  "    A:<State>            Set state for all Terminals, state (0-9)\n"
  "    D                    Show current configuration\n"
  "    C<iltswb>:value      Set config for [i]dentifier, [l]eds per shelve, [t]erminals per shelve, amount of [s]helves, spacer-[w]idth, [b]link-interval.\n"
  "    S                    Store current configuration in EEPROM/FLASH\n"
  "    L                    Load stored cofiguration from EEPROM/FLASH\n"
  "    R                    Reboot controller (Disconnects serial!)\n"
  "    W                    Show startup loop\n"
  "\n";

// All the messages are stored in program-memory to save CPU-memory (especially for AVR type mcu's)
const uint8_t MSG_MAX_NUMBER = 35;
const uint8_t MSG_MAX_SIZE = 48;
const char messages [MSG_MAX_NUMBER] [MSG_MAX_SIZE] PROGMEM = { 
  { "-=[ USB LedStrip ]=-\n" },                         // 0  
  { "Invalid state, use 0-9\n" },                       // 1
  { "Invalid Terminal-ID, use 1-48\n" },                // 2
  { "Syntax error: Use T<TERMINAL-ID>:<STATE>\n" },     // 3 
  { "Syntax error: Use A:<STATE>\n" },                  // 4x
  { "\nDisplay configuration:\n" },                     // 5
  { "\nSave configuration: " },                         // 6
  { "Configuration saved in EEPROM.\n" },               // 7
  { "\nReset all Terminal states.\n" },                 // 8
  { "\nShowing startup loop.\n" },                      // 9
  { "\nERROR: command unknown.\n" },                    // 10
  { "\nSYNTAX ERROR: Use <A-Z>, H for help.\n" },       // 11
  { "Identifier           : " },                        // 12
  { "LEDs per shelve      : " },                        // 13
  { "Terminals per shelve : " },                        // 14
  { "Amount of shelves    : " },                        // 15
  { "Spacer width         : " },                        // 16
  { "Blinking interval    : " },                        // 17
  { "\nNo EEPROM or FLASH on this MCU\n" },             // 18
  { "\nRebooting contoller...\n" },                     // 19
  { "\nLoad configuration: " },                         // 20
  { "All terminals set to state" },                     // 21
  { "Configuration loaded\n" },                         // 22
  { "* Opening Failed" },                               // 23
  { "* Writing failed" },                               // 24
  { "ERROR, configfile is empty" },                     // 25
  { "Valid config found in EEPROM, loading it.\n" },    // 26
  { "No config stored in EEPROM, using defaults.\n\n" },// 27
  { "Configuration stored in EEPROM.\n" },              // 28
  { "Version " },                                       // 29
  { "MCU          : " },                                // 30
  { "SN           : " },                                // 31
  { " compiled for " },                                 // 32
  { "\nTesting EEPROM availability & contents\n" },     // 33
  { "Failed to initialise EEPROM, Restarting...\n" },   // 34
};


// Setup MCU
void setup() {
  char MCUid [41];

  // Setup SB -serial port
  Serial.begin(115200);
  Serial.setTimeout(0);


  // DEBUG('Ready, waiting fo serial to become active.');
    while (!Serial) {
    // wait for serial port to connect. Needed for native USB-serial port only.
    // Flash the status led green to indicate we are ready to start.
    delay(500);
  }

  // Show some details about the MCU and the codeversion
  printMSG(0);
  printMSG(29);
  Serial.print(VERSION);
  printMSG(32);
  Serial.println(MCU);
  Serial.println();
  printMSG(30);
  Serial.println(BOARD_NAME);
  printMSG(31);
  MicroID.getUniqueIDString(MCUid,16);
  Serial.println(MCUid);

  printMSG(33);
  EEPROM.begin();
  if (!eeprom_is_ready()){
    printMSG(34);
    delay(1000);
    RebootMCU();
  }
  loadConfiguration();

  // Enable current config (default or loaded from EEPRON/Flash)
  updateConfiguration();

  //startupAnimation();
  StartupLoop();

  // Show help & config:
  // ShowHelp();
  displayConfiguration() ;

  // Ready to go, show prompt.
  Serial.print("> ");
}
 

// Main loop
void loop() {
  while (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') {
      inputBuffer[bufferIndex] = '\0'; // Null-terminate the string
      checkInput(inputBuffer);
      Serial.print("> ");
      bufferIndex = 0;
    } else if ( c == 0x08 ) {   // Backspace
      inputBuffer[bufferIndex--] = '\0';
      Serial.print(c);  
      Serial.print(' ');  
      Serial.print(c);  
    } else if (bufferIndex < MAX_INPUT_LEN - 1 && c >= 0x20 && c < 0x7E && c!= 0x5C && c!=0x60 ) { // only printable chars, -1 to leave space for null terminator at end.
      inputBuffer[bufferIndex++] = c;
      Serial.print(c);
    }

  }
  SetLEDs();
}

void DEBUGLOG(String *message){
  if (DEBUG == true) {
    Serial.println(*message);
  }
}


void printMSG (int i) {
  if (i <= MSG_MAX_NUMBER) { 
    const char *ptr = reinterpret_cast<const char*>(&messages[i]);    
    char chr;
    if (!ptr) 
      return;
    while ((chr = pgm_read_byte(ptr++))) {
      Serial.print(chr);
    }
  }
}


void writeStringToEEPROM(int addrOffset, const String &strToWrite) {
  byte len = strToWrite.length();
  EEPROM.write(addrOffset, len);
  int rlen = EEPROM.read(addrOffset); 
  for (int i = 0; i < len; i++) {
    EEPROM.write(addrOffset + 1 + i, strToWrite[i]);
  }
}

String readStringFromEEPROM(int addrOffset) {
  int len = EEPROM.read(addrOffset); 
  char data[len + 1];
  for (int i = 0; i < len; i++)
    data[i] = EEPROM.read(addrOffset + 1 + i);
  data[len] = '\0'; 
  return (String) data;
}

void loadConfiguration() {
  String check = readStringFromEEPROM(0x0);
  if ( check == CONFIG_IDENTIFIER ) {
    printMSG(26);
    EEPROM.get(0x10, LedConfig);
  } else {
    printMSG(27);
  }
}

void saveConfiguration() {
  writeStringToEEPROM(0x0, CONFIG_IDENTIFIER); 
  EEPROM.put(0x10, LedConfig);
  printMSG(28);
}


void updateConfiguration() {
//  delete[] leds;
//  CRGB leds[LedConfig.numStrips][LedConfig.numLedsPerStrip];
//  leds = new CRGB[LedConfig.numStrips][LedConfig.numLedsPerStrip];

  // Code below does not work as the FastLED class expects the pin to be a constant :-/
  // FastLED.addLeds<[LEDTYPE], [PIN], [RGB-ORDER]>( [LED-array], [NUMBER OF LEDS]);
  // for (const int n = 0; n < LedConfig.numStrips; n++) {
  //   FastLED.addLeds<WS2812, n+2, GRB>(leds[n], LedConfig.numLedsPerStrip);
  // }
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT); 
  pinMode(4, OUTPUT);
  // pinMode(5, OUTPUT);
  // pinMode(6, OUTPUT);
  // pinMode(7, OUTPUT);
  // pinMode(8, OUTPUT); 
  // pinMode(9, OUTPUT); 

  FastLED.addLeds<WS2812B, 2, GRB>(leds[1], LedConfig.numLedsPerStrip);
  FastLED.addLeds<WS2812B, 3, GRB>(leds[2], LedConfig.numLedsPerStrip);
  FastLED.addLeds<WS2812B, 4, GRB>(leds[3], LedConfig.numLedsPerStrip);
  // FastLED.addLeds<WS2812B, 5, GRB>(leds[4], LedConfig.numLedsPerStrip);
  // FastLED.addLeds<WS2812B, 6, GRB>(leds[5], LedConfig.numLedsPerStrip);
  // FastLED.addLeds<WS2812B, 7, GRB>(leds[7], LedConfig.numLedsPerStrip);
}



void checkInput(char input[16]) { 

  if (input[0] == 0) {
    Serial.println("");
    return;
  }

  char output[MAX_OUTPUT_LEN];
  memset(output, '\0', sizeof(output));

  int test=0;
  int State;
  int TermID;

  // sprintf(output,"Input: [%s].",input);      
  // Serial.println(output); 

  char Command = input[0];
  char *Data = input +1; 

  if (Command >= 'A' && Command <= 'Z') {
    // sprintf(output,"Command [%c] with Data [%s]",Command, Data); 
    // Serial.println(output);

    switch(Command) {
      case 'C':
        SetConfigParameters(Data);
        break;

      case 'T':
        test = sscanf(Data, "%2d:%d", &TermID, &State);
        if ( test == 2) {
          if (TermID >= 1 && TermID <= MAX_TERMINALS) {
            if (State >= 0 && State <= 9) {
              setGroupState(TermID -1,State);
              // sprintf(output,"Terminal %d state set to %d", TermID, State);
              // Serial.println(output);
            } else {
              printMSG(1);
            }
          } else { 
            printMSG(2);
          }
        } else {
          printMSG(3);
        }
        break;
               
      case 'A':
        test = sscanf(Data, ":%d", &State);
        if (test == 1) {
          if (State >= 0 && State <= 9) {
            printMSG(21);
            Serial.print(State);
            for(int i=0; i < MAX_TERMINALS; i++){
              setGroupState(i,State);
            }
          } else {
            printMSG(1);
          }
        } else 
          printMSG(4);
        break;
      
      case 'D':
        printMSG(5);
        displayConfiguration();
        break;

      case 'S':
        printMSG(6);
        saveConfiguration();
        break;

      case 'L':
        printMSG(20);
        loadConfiguration();
        updateConfiguration();
        break;

      case 'X':
        printMSG(8);
        for(int i=0; i < MAX_TERMINALS; i++){
          setGroupState(i,0);
        }
        SetAllLEDs(CRGB::Black);
        break;

      case 'W':
        printMSG(9);
        StartupLoop();
        break;

      case 'R':
        printMSG(19);
        RebootMCU();
        break;

      case 'H':
        ShowHelp();
        break;

      default:
        printMSG(10);
        break;
    }
  } else
    printMSG(11); 
}

void RebootMCU() {
 wdt_enable(WDTO_15MS);
  while (1);
}

void ShowHelp(){
    const char *ptr = reinterpret_cast<const char*>(&HelpText);
    char chr;
    if (!ptr) 
      return;
    while ((chr = pgm_read_byte(ptr++))) {
      Serial.print(chr);
    }
}


void SetConfigParameters(char *Data){
//  char output[MAX_OUTPUT_LEN];
  char Parameter = Data[0];
  char *Value = Data +2; 

  if (Data[1] == ':') {
    switch (Parameter) {
      case 'i':
        Serial.println();
        printMSG(12);
//        if sizeof(Value) 
        strcpy(LedConfig.identifier,Value);
        Serial.println(LedConfig.identifier);
        break;
      case 'l':
        Serial.println();
        printMSG(13);
        LedConfig.numLedsPerStrip = atoi(Value);
        Serial.println(LedConfig.numLedsPerStrip);
        break;
      case 't':
        Serial.println();
        printMSG(14);
        LedConfig.numGroupsPerStrip = atoi(Value);
        Serial.println(LedConfig.numGroupsPerStrip);
        break;
      case 's':
        Serial.println();
        printMSG(15);
        LedConfig.numStrips = atoi(Value);
        Serial.println(LedConfig.numStrips);
        break;
      case 'w':
        Serial.println();
        printMSG(16);
        LedConfig.spacerWidth = atoi(Value);
        Serial.println(LedConfig.spacerWidth);
        break;
      case 'b':
        Serial.println();
        printMSG(17);
        LedConfig.blinkinterval = atoi(Value);
        Serial.println(LedConfig.blinkinterval);
        break;
      default:
        printMSG(11);
        break;
    }
    updateConfiguration();
  } else {
    printMSG(11);
  } 
}


void displayConfiguration() {
  char output[MAX_OUTPUT_LEN];
  memset(output, '\0', sizeof(output));

  printMSG(12);
  sprintf(output,"%s",LedConfig.identifier);      
  Serial.println(output); 

  printMSG(13);
  sprintf(output,"%d",LedConfig.numLedsPerStrip);      
  Serial.println(output); 

  printMSG(14);
  sprintf(output,"%d",LedConfig.numGroupsPerStrip);      
  Serial.println(output); 

  printMSG(15);
  sprintf(output,"%d",LedConfig.numStrips);      
  Serial.println(output); 

  printMSG(16);
  sprintf(output,"%d",LedConfig.spacerWidth);      
  Serial.println(output); 

  printMSG(17);
  sprintf(output,"%d",LedConfig.blinkinterval);      
  Serial.println(output); 

  Serial.println(); 
}

void setGroupState(int group, int state) {
  switch (state) {

    case 0:
      BlinkState[group] = false;
      BlinkColor[group] = CRGB::Black;
      break;

    case 1:
    case 2:
    case 3:
    case 4:
      BlinkState[group] = false;
      BlinkColor[group] = LedConfig.state_color[state];
      break;

    case 5:
    case 6:
    case 7:
    case 8:
      BlinkState[group] = true;
      BlinkColor[group] = LedConfig.state_color[state -4];
      break;

    case 9:
      BlinkColor[group] = CRGB::White;
      break;
  }
}


void SetLEDGroupColor(int group, CRGB color) {
  int stripIndex = (group / LedConfig.numGroupsPerStrip)+1;
  int groupIndex = group % LedConfig.numGroupsPerStrip;
  int startLEDIndex = groupIndex * (LedConfig.numLedsPerStrip / LedConfig.numGroupsPerStrip);
  int widthLED = (LedConfig.numLedsPerStrip / LedConfig.numGroupsPerStrip) - LedConfig.spacerWidth;
  fill_solid(&(leds[stripIndex][startLEDIndex]), widthLED, color);
}



void FadeAll(int StartLed=1, int EndLed=LedConfig.numLedsPerStrip) {
  for(int i = StartLed; i < EndLed; i++)
    for(int n = 0; n < LedConfig.numStrips; n++)
      leds[n][i].nscale8(200); 
}


void StartupLoop() {
  // Boot animmation only on 1st strip.

  // Blue ><><
  uint8_t DELAY (LedConfig.numLedsPerStrip / 8);
  CRGB color = 0x0080FF;
  for(int i = 0; i < LedConfig.numLedsPerStrip/2; i+=1) {
    for(int n = 0; n < LedConfig.numStrips; n++) {
      leds[n][i] = color; 
      leds[n][LedConfig.numLedsPerStrip -i -1] = color;
    }
    FastLED.show();
    FadeAll(0,LedConfig.numLedsPerStrip);
    delay(DELAY);
  }
 
  // White Flash
  SetAllLEDs(CRGB::White);
  FastLED.show();
  delay(75);
  for(int i = 0; i < 16; i++) {
    FadeAll(0,LedConfig.numLedsPerStrip);
    FastLED.show();
    delay(65);
  }
 
  // All Off
  SetAllLEDs(CRGB::Black);
}


void SetAllLEDs(CRGB color){
  for(int n = 0; n < LedConfig.numStrips; n++)
    fill_solid(leds[n], LedConfig.numLedsPerStrip, color);
  FastLED.show();
}


void SetLEDs() {
  // Check if it's time to toggle each group's state
  if ( millis() - lastToggleTimes >= LedConfig.blinkinterval ) { 
    lastToggleTimes = millis(); // Update last toggle time
    blink = blink == 0 ? 1 : 0;
    for(int i=0; i < LedConfig.numGroupsPerStrip*LedConfig.numStrips; i++){
      if (BlinkState[i] == true) {
        if (blink == true){ 
          SetLEDGroupColor(i, BlinkColor[i]);
        } else {
          SetLEDGroupColor(i, CRGB::Black);
        }
      } else {
        SetLEDGroupColor(i, BlinkColor[i]);
      }
    }
    FastLED.show();
  }
}