#include <Servo.h>
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
#define NEWIR
#include <IRremote.hpp>
//#define OLDIR
//#include <IRremote.h>
#include <MemoryUsage.h>

//CHOOSE ONE IR REMOTE FOR TESTING DEPENDING ON IR remote available
//#define AMAZON
//#define BEGINNER
#define WOKWI
#define TIMEOUTRESTORE

#define DEBUGINPUT     //enables print statements in input routines
#define DEBUGPROCESS   //enables print statements in processing routines
#define DEBUGMEM       //
#define DEBUGSERVOS    //
#define DEBUGEEPROM

/* Comments for email posting:
  Well.  Thoughts about control of servos made me realize that with a bit of
  work, one can control up to 12 servos with one Nano, complete with dual LEDs
  and button inputs, with no external boards(i.e. no MCP23017, no PCA9685).
  How?  Well, the LEDs can all be Neopixels.  The buttons are all single action
  momentary, so they can be a cascade of resistors on a single analog input, as
  shown in the attached schematic; IRL, I added a capacitor to the Analog pin,
  for simple hardware debouncing purposes.

  Note that, for wiring purposes, this means a panel connected to the Nano
  needs a 4-pin connector, carrying VCC, data, and GND for the Neopixels,
  and a single pin for the button feedback.  If multiple subpanels are desired,
  multiple connectors, each having an extra pin to return the pixel data out
  signal are required.  So, 5-pin connectors.  With up to 32 pixels on the
  VCC/GND, the paneld could draw ~2A peak, but we're only going to power one
  colour at a time(R/G/B), and not full intensity, so < 0.5A is expected normally.
  Board must also support connection of up to 12 servos, with external power
  supply for them.  See schematic for wiring details.  Board to follow, once a
  prototype is built.

  3 simultaneous methods exist for control and editing of positions:
  - Serial can be used (but needs D0/D1)
  - IR remote can be used(Wokwi for testing, Amazon or other IRL)
  - Button control can be used; resistor ladders as appropriate.
  
  All 3 methods return characters to the software; characters are 0-9, A-F,
  and +(increment) =(throw) -(decrement) P(rogram) S(ave); -1 is a no-command flag

  RS485 can be used, as D0/D1/D2 are available for that
  I2C is still an option, as we reserved A4/A5
  I chose A2 for the IR remote and A3 for the pixel output.  In Wokwi, the
  pixels are all depicted and work.  12 servos work.

  Serial and IR work in Wokwi; the resistor-button network does not, but
  since the  analog read module will return the same char values as serial
  and IR, we can presume it will work, we just need to iron out the one routine.
  All servos can be thrown using serial
  (0..9, A..B); positions can be edited using +=- with S for Save and P for
  entering/leaving Programming mode.

  The IR remote is a bonus, and allows the same functionality on a 20-button
  remote purchased from Amazon.  For most similar remotes, simply replacing
  the key values and changing button lettering will be necessary, after using
  the IRremote.h demos to figure out key values.  To relabel the buttons, I
  will use Avery dots with suitable labels and a clear plastic overlay.

  The readily available simple MAX485 circuit can be added by using headers.
  A7 is used for the button array for commanding turnouts.  A6 is available for
  a similar array for route control, or remote requests; it remains to be seen
  how many buttons could be reasonably supported by this method, but I2C button
  support could be added instead.

  I envisage this being used for local panels on my layout, allowing one Nano
  to control up to 12 servos via up to 3 subpanels; I'll make a PCB capable of
  supporting up to 4 sub-panels.  Each subpanel could be connected with a 5 or 6
  wire cable.
  Note that the Nano footprint has been augmented by newer Arduino products.  I
  haven't explored whether they're fully compatible with this development.  I
  leave that to the user.
  Enjoy!
*/
//***************************************************  CONSTANTS  *************************************
const uint8_t rcv_pin = 17;   //pin for IR reception
const uint8_t pxl_pin = 16;   //pin for pixel output

const uint8_t mode_pin = 20;  //buttons with resistors on this pin used for other functions, TBD
const uint8_t bttn_pin = 21;  //buttons with resistors on this pin used to toggle all turnouts
const uint8_t bttn_map = 23;  //value to use for Map, to generate values from 0..whatever for buttons.  Needs appropriate resistor network for each pin.

const uint8_t THROWN = HIGH;         //aka Reversed
const uint8_t HOME = LOW;            //aka Normal or Closed

const uint8_t MAXSERVO = 160;        //servo adjustment top and bottom; some cheap servos break if you run them 0-180
const uint8_t MINSERVO = 20;
const uint8_t ADJUSTDELTA = 5;       //degrees to add or subtract when editing position; likely too coarse, but useful for testing, as it causes a visible change
const uint8_t MOVEDELTA = 5;        //degrees to move per MOVEINTERVAL, if we're moving; we will add/subtract this many degrees each time we move the servo !!!NOT YET IMPLEMENTED

const uint32_t MOVEINTERVAL = 100;      //ms between updates to the servo when moving
const uint32_t STARTINTERVAL = 100;      //ms between servo moves in setup, to avoid power supply overloading
const uint32_t DEBOUNCEDURATION = 50;  //ms for debounce when pressed
const uint32_t MAXTHROWNTIME = 10000;  //ms maximum that a turnout can remain in THROWN state; 0 is special, means 'forever'

const uint16_t SIGNATURE = 0xAA55;     //signature to be found/written in EEPROM; We must change whenever Turnout struct definition changes


//***************************************************  GLOBALS  *************************************

//This software works with an array of turnout structs which define the characteristics of a turnout; the basic struct is as follows:

struct Turnout {
  const uint8_t servo_Pin; //self-evident
  uint8_t servo_Home;      //angle 0-180
  uint8_t servo_Thrown;    //angle 0-180
  uint8_t turnoutState;    //when read from EEPROM, at startup or during editing, this could be the default state, after that, contains the present state
  uint8_t servo_Pos;       //position servo is presently at, if different from home or thrown; interim motion values
  uint32_t timer;          //two things require time elements; motion, and return-to-home.  We can use the same timer, so we'll call it timer!
  Servo servo;             //the Servo element can't be initialized, hence compiler warns.  Could make it a separate array, but this is convenient.
};

//this is the Turnout array declaration, complete with initialization of values

struct Turnout Turnouts[] = {
  {3, 45, 135, 0, 0, 0},       //D3 Servo 1
  {4, 45, 135, 0, 0, 0},       //D4 Servo 2
  {5, 45, 135, 0, 0, 0},       //D5 Servo 3
  {6, 45, 135, 0, 0, 0},       //D6 Servo 4
  {7, 45, 135, 0, 0, 0},       //D7 Servo 5
  {8, 45, 135, 0, 0, 0},       //D8 Servo 6
  {9, 45, 135, 0, 0, 0},       //D9 Servo 7
  {10, 45, 135, 0, 0, 0},      //D10 Servo 8
  {11, 45, 135, 0, 0, 0},      //D11 Servo 9
  {12, 45, 135, 0, 0, 0},      //D12 Servo 10
  {14, 45, 135, 0, 0, 0},      //A0 Servo 11
  {15, 45, 135, 0, 0, 0},      //A1 Servo 12
};
//D0 reserved for Serial/RS485 TX
//D1 reserved for Serial/RS485 RX
//D2 reserved for RS485 direction control pin
//D13 pin reserved for onboard LED; presently, just used to indicate programming enabled
//A2 pin reserved for pixel output
//A3 pin reserved for IR input 
//A4 reserved for I2C
//A5 reserved for I2C
//A6 may be used for route requests, or alternate purposes(TBD); I expect I may want some interconnect requests with other servo controllers, or JMRI
//A7 is for direct turnout control, one button per turnout; up to 8 buttons could be added above that; 5(-=+SP) presently implemented.  

//Finally, the software needs to know how big the array is.  This is a foolproof way of determining that, rather than manually entering a constant
//Though constant, must be declared after Turnouts[] is created, as it's based on it's size

const uint8_t NUMCHAN = sizeof(Turnouts) / sizeof(Turnouts[0]); //defines how many servos we're driving
const uint8_t NUMPIXELS = NUMCHAN * 2;                          //and the number of pixels

//***************************************************  GLOBALS  *************************************
bool editMode = false;
uint8_t currChan = 0;
Adafruit_NeoPixel pixels(NUMPIXELS, pxl_pin, NEO_GRB + NEO_KHZ800);
#ifdef OLDIR
IRrecv IrReceiver(rcv_pin);
#endif

void setup() {             //***************  SETUP  *******************************
  Serial.begin(115200);                                      //start Serial for Monitor
  Serial.flush();
#ifdef NEWIR
  IrReceiver.begin(rcv_pin);                               // Start the IR receiver
#endif
#ifdef OLDIR
  IrReceiver.enableIRIn();
#endif
  pixels.begin();                                          // setup the NeoPixel strip object
  pinMode(LED_BUILTIN, OUTPUT);                             //use it for scan blinker, and maybe diagnostic blinks

  if (readEEPROM() == -1) { //reads the EEPROM into Turnout array if signature is valid and array is correct size; returns -1 if failed
  }
  pixels.clear();
  for (int chan = 0; chan < NUMCHAN; chan++) {
    pixels.setPixelColor(chan * 2, pixels.Color(0, 150, 0)); //GREEN
    pixels.setPixelColor(chan * 2 + 1, pixels.Color(150, 0, 0)); //RED
    Turnouts[chan].servo.attach(Turnouts[chan].servo_Pin);//attach servo
    Turnouts[chan].timer = 0; //clear
    if (Turnouts[chan].turnoutState == HOME) {
      Turnouts[chan].servo.write(Turnouts[chan].servo_Home);//and set initial position
      Turnouts[chan].servo_Pos = Turnouts[chan].servo_Home; //set position variable
    }
    else {
      Turnouts[chan].servo.write(Turnouts[chan].servo_Thrown);//and set initial position
      Turnouts[chan].servo_Pos = Turnouts[chan].servo_Thrown; //set position variable
    }
    pixels.show();
    delay(STARTINTERVAL);  //pause, so that we don't enable and churn all servos at the same time
    //now darken the LED for the position the turnout is NOT in
    if (Turnouts[chan].turnoutState == HOME)  pixels.setPixelColor(chan * 2 + 1, pixels.Color(0, 0, 0)); //DARK
    else                                      pixels.setPixelColor(chan * 2, pixels.Color(0, 0, 0)); //DARK
  }
  pixels.show();  //last pixel show, to catch the post-delay turn off of the unselected LED
#ifdef DEBUGMEM
  MEMORY_PRINT_START
  MEMORY_PRINT_HEAPSTART
  MEMORY_PRINT_HEAPEND
  MEMORY_PRINT_STACKSTART
  MEMORY_PRINT_END
  MEMORY_PRINT_HEAPSIZE
  Serial.println();
  FREERAM_PRINT;
  Serial.println(F("Setup complete"));
#endif
}  //end setup

void loop() {                          //***********  LOOP  *************************************
  //  int bttn = checkIR(checkSerial(scanButtons())); //returns a character, or -1  Only 0123456789ABCDEF+-SP have significance
  int bttn = checkIR(checkSerial(-1));                //does not check buttons, just IR and Serial
  //  int bttn = checkSerial(-1);                         //does not check buttons or IR, just Serial
  if (bttn >= 0) {                                    //deal with it; remember to debounce
    bttn = toupper(bttn);                             //allows serial to include lower case letters
#ifdef DEBUGINPUT
    Serial.print(F("Received command from input checks: "));
    Serial.println(bttn, HEX);
#endif
    processLogic(bttn);
  }
  updateOutputs();                     //handle LEDs and Servos.
  digitalWrite(LED_BUILTIN, editMode);
  delay(50);
}//end loop

void processLogic(int bttn) {          //***********  process Logic  *********************
  switch (bttn) {
    case '0'...'9'://convert to number 0...9
      bttn = bttn - '0';//fall through is deliberate
      [[fallthrough]];
    case 'A'...'B'://convert to numbers 10,11
      if (bttn >= 'A') bttn = (bttn - 'A') + 10;
      if (bttn < NUMCHAN) {
        if (editMode == false) {
          Turnouts[bttn].turnoutState = Turnouts[bttn].turnoutState == HOME ? THROWN : HOME;  //if not editing, simply toggle Turnout position, as button already debounced.
          //Turnouts[bttn].timer = millis(); // if timedreset is enabled, set timer for timed reset
#ifdef DEBUGPROCESS
          Serial.print(F("set turnout "));
          Serial.print(bttn + 1);
          Serial.print(F(" to "));
          Serial.println(Turnouts[bttn].turnoutState);
#endif
          currChan = bttn;  //track most recent channel for editing
        }
        else {  //if we just pressed a number, and it's already our currChan, presume we should flip the turnout.  If it's a different number, just change .
          if (bttn == currChan) {
            Turnouts[bttn].turnoutState = Turnouts[bttn].turnoutState == HOME ? THROWN : HOME;
#ifdef DEBUGPROCESS
            Serial.println(F("flipped turnout being edited"));
#endif
          }
          else {
            currChan = bttn;  //track most recent channel for editing
#ifdef DEBUGPROCESS
            Serial.println(F("moved to new turnout"));
#endif
          }
        }
      }
      break;
    case '+':  //if edit enabled, increment&limit present Turnout position; e.g. if home, and pos=home, then home = home + ADJUSTDELTA
      if (editMode == true) {  //consider whether we want this to become a "nudge harder to rail"
        if (Turnouts[currChan].turnoutState == HOME && Turnouts[currChan].servo_Pos == Turnouts[currChan].servo_Home) {
          Turnouts[currChan].servo_Home += ADJUSTDELTA;
          if (Turnouts[currChan].servo_Home > MAXSERVO)Turnouts[currChan].servo_Home = MAXSERVO;
#ifdef DEBUGPROCESS
          Serial.print(F("Increased Home Position to "));  Serial.println(Turnouts[currChan].servo_Home);
#endif
          Turnouts[currChan].servo_Pos = Turnouts[currChan].servo_Home;
        }
        else {
          Turnouts[currChan].servo_Thrown += ADJUSTDELTA;
          if (Turnouts[currChan].servo_Thrown > MAXSERVO) Turnouts[currChan].servo_Thrown = MAXSERVO;
#ifdef DEBUGPROCESS
          Serial.print(F("Increased Thrown Position to "));  Serial.println(Turnouts[currChan].servo_Thrown);
#endif
          Turnouts[currChan].servo_Pos = Turnouts[currChan].servo_Thrown;
        }
      }
      break;
    case '-':  //if edit enabled, decrement&limit present Turnout position; e.g. if home, and pos=home, then home = home + ADJUSTDELTA
      if (editMode == true) {  //consider whether we want this to become a "pull back from rail"
        if (Turnouts[currChan].turnoutState == HOME && Turnouts[currChan].servo_Pos == Turnouts[currChan].servo_Home) {
          Turnouts[currChan].servo_Home -= ADJUSTDELTA;
          if (Turnouts[currChan].servo_Home < MINSERVO)Turnouts[currChan].servo_Home = MINSERVO;
#ifdef DEBUGPROCESS
          Serial.println(F("Decreased Home Position to "));  Serial.println(Turnouts[currChan].servo_Home);
#endif
          Turnouts[currChan].servo_Pos = Turnouts[currChan].servo_Home;
        }
        else {
          Turnouts[currChan].servo_Thrown -= ADJUSTDELTA;
          if (Turnouts[currChan].servo_Thrown < MINSERVO) Turnouts[currChan].servo_Thrown = MINSERVO;
#ifdef DEBUGPROCESS
          Serial.print(F("Decreased Thrown Position to "));  Serial.println(Turnouts[currChan].servo_Thrown);
#endif
          Turnouts[currChan].servo_Pos = Turnouts[currChan].servo_Thrown;
        }
      }
      break;
    case '=':  //toggle turnout
      Turnouts[currChan].turnoutState = Turnouts[currChan].turnoutState == HOME ? THROWN : HOME;
#ifdef DEBUGPROCESS
      Serial.println(F("flipped current turnout"));
#endif
      break;
    case 'L':
#ifdef DEBUGPROCESS
      Serial.println(F("ch pin pin pin state  angle cmd"));
      for (int ch = 0; ch < NUMCHAN; ch++) {
        listTO(ch);
      }
#endif
      break;
    case 'M':  //emits a menu to Serial Monitor; not ifdef-ed because you only can use this with Serial anyway
#ifdef DEBUGPROCESS
      Serial.println(F("----------------------------------  MENU  -------------------------"));
      Serial.println(F("             NEEDS UPDATING WITH ACTUAL COMMAND LETTERS            "));
      Serial.println(F("= - Alternate (Home->Thrown, or Thrown - > Home"));
      Serial.println(F("+ - Increment                  - - Decrement"));
      Serial.println(F("S - Store                      P - Enable/Disable limits programming"));
      Serial.println(F("H - Help menu                      "));
      Serial.println(F("C - Home                         T - Throw"));
      Serial.println(F("L - Print complete turnout table for visual Inspection(for verifying changes)"));
      Serial.println(F("any valid turnout number, e.g. 3 - go directly to that turnout"));
#endif
      break;
    case 'S':  //saves all turnout info to EEPROM
      if (editMode == true) {}
      writeEEPROM();
#ifdef DEBUGPROCESS
      Serial.println(F("Wrote EEPROM"));
#endif
      break;
    case 'P':  //toggles between edit mode and not.
      if (editMode == true) {
#ifdef DEBUGPROCESS
        Serial.println(F("Turned edit mode OFF"));
#endif
        editMode = false;

      }
      else {
#ifdef DEBUGPROCESS
        Serial.print(F("Turned edit mode ON; "));
        Serial.print(F("Editing turnout "));
        Serial.println(currChan + 1);
#endif
        editMode = true;
      }
      break;
    default:
#ifdef DEBUGPROCESS
      Serial.print(F("Character in processLogic: -"));
      Serial.print(bttn, HEX);
      Serial.println(F("- was unexpected"));
#endif
      break;
  }
#ifdef TIMEOUTRESTORE   //can be added if desired.  Simply checks each turnout for timeout.
  //Feature is wanted by some, not all.  Returns any non-home turnouts to home after a timeout, if editing is not happening.
  for (int n = 0; n < NUMCHAN; n++) {
    if ((editMode != true) && (millis() - Turnouts[n].timer >= MAXTHROWNTIME)) Turnouts[n].turnoutState = HOME;
  }
#endif
} //end processLogic

void listTO(int ch) {
  Serial.print(ch + 1);
  Serial.print(F("  "));
  Serial.print(F(" HOME: "));
  if (Turnouts[ch].servo_Home < 100)Serial.print(" ");
  Serial.print(Turnouts[ch].servo_Home);
  Serial.print(F("   THROWN: "));
  if (Turnouts[ch].servo_Thrown < 100)Serial.print(" ");
  Serial.print(Turnouts[ch].servo_Thrown);
  Serial.print(F("  Pos: "));
  Serial.println(Turnouts[ch].servo_Pos);
}

void updateOutputs() {                 //***********  update Outputs  ********************
  //runs every loop, due to LED and motion changes between endpoints
  pixels.clear();                      //Set all pixel colors to 'off'
  for (int chan = 0; chan < NUMCHAN; chan++) {
    setLEDs(chan);
    setServos(chan);
#ifdef DEBUGPROCESS
    listTO(chan);
#endif
  }
  pixels.show();   // Send the updated pixel colors to the hardware.
}//end updateOutputs

void setServos(int chan) {             //***********  set Servos  ********************
  if (Turnouts[chan].turnoutState == HOME && Turnouts[chan].servo_Pos == Turnouts[chan].servo_Home) {
    Turnouts[chan].servo_Pos = Turnouts[chan].servo_Home;
    Turnouts[chan].servo.write(Turnouts[chan].servo_Home); //write pos to servo
  }
  else if (Turnouts[chan].turnoutState == THROWN && Turnouts[chan].servo_Pos == Turnouts[chan].servo_Thrown) {
    Turnouts[chan].servo_Pos = Turnouts[chan].servo_Thrown;
    Turnouts[chan].servo.write(Turnouts[chan].servo_Thrown);
  }
  else {
    /*
       if pos is not at declared present state, must be moving there.  If it's time to, add/subtract DELTA,
       check constraints, and write to pos, then write pos to servo.
    */
    if (millis() - Turnouts[chan].timer > MOVEINTERVAL) {
      Turnouts[chan].timer = millis();
#ifdef DEBUGSERVOS
      listTO(chan);
      Serial.print(F("Turnout ")); Serial.print(chan + 1); Serial.print(F(" Not at HOME or THROWN positions; position is: ")); Serial.print(Turnouts[chan].servo_Pos); Serial.print(F(" Moving to: "));
      if (Turnouts[chan].turnoutState == HOME) Serial.println(Turnouts[chan].servo_Home);
      else  Serial.println(Turnouts[chan].servo_Thrown);
#endif
      int pos = Turnouts[chan].servo_Pos;
      if (Turnouts[chan].turnoutState == HOME) {
        if (abs(pos - Turnouts[chan].servo_Home) < MOVEDELTA) pos = Turnouts[chan].servo_Home;
        else {
          if (pos - Turnouts[chan].servo_Home < 0) pos += MOVEDELTA;
          else pos -= MOVEDELTA;
        }
      }
      if (Turnouts[chan].turnoutState == THROWN) {
        if (abs(pos - Turnouts[chan].servo_Thrown) < MOVEDELTA) pos = Turnouts[chan].servo_Thrown;
        else {
          if (pos - Turnouts[chan].servo_Thrown < 0) pos += MOVEDELTA;
          else pos -= MOVEDELTA;
        }
      }
      //  constrain pos and write to servo
      Turnouts[chan].servo_Pos = pos < MINSERVO ? MINSERVO : pos > MAXSERVO ? MAXSERVO : pos;
      Turnouts[chan].servo.write(Turnouts[chan].servo_Pos);
    }//end of time conditional
  }
}  //end setServos

void setLEDs(int chan) {               //***********  set LEDs  ********************
  //for homeLED to be lit green, must be at home position, and it's not being edited
  //for homeLED to be lit blue, must be at home position, and we're in edit mode
  //for homeLED to be dark, must not be at home position
  //for thrownLED to be lit red, must be at thrown position, and it's not being edited
  //for thrownLED to be lit blue, must be at thrown position, and we're in edit mode
  //for thrownLED to be dark, must not be at thrown position
  //pixels were cleared earlier, so only set them if needed
  if (Turnouts[chan].servo_Pos == Turnouts[chan].servo_Home && Turnouts[chan].turnoutState == HOME ) {
    if (editMode == true && currChan == chan) pixels.setPixelColor(chan * 2, pixels.Color(0, 0, 150)); //BLUE
    else pixels.setPixelColor(chan * 2, pixels.Color(0, 150, 0)); //GREEN
  }
  if (Turnouts[chan].servo_Pos == Turnouts[chan].servo_Thrown && Turnouts[chan].turnoutState == THROWN ) {
    if (editMode == true && chan == currChan ) pixels.setPixelColor((chan * 2) + 1, pixels.Color(0, 0, 150)); //BLUE
    else pixels.setPixelColor((chan * 2) + 1, pixels.Color(150, 0, 0)); //RED
  }
}  //end setLEDs

int readEEPROM() { //***************************************  readEEPROM  **************************************************
  //read EEPROM signature.  If current, read array size; otherwise, return error flag.  If same as presently allocated, read the array; otherwise, return error flag
  int retval = -1;
  uint16_t sig;
  EEPROM.get(0, sig);     //read the signature  (should be 0XAA55)
  if (sig == SIGNATURE) {//if signature is correct,
    uint8_t num;
    EEPROM.get(sizeof(sig), num);     //read array size;
    if (num == NUMCHAN) {
      EEPROM.get(sizeof(sig) + sizeof(num), Turnouts);   //and read the array  WHOOPS.  Need to at least check that the array is the same size.  Sigh.
#ifdef DEBUGEEPROM
      Serial.println(F("EEPROM has been Read"));
#endif
      retval = 0;
    }
    else {
#ifdef DEBUGEEPROM
      Serial.println(F("Error - array size mismatch, EEPROM content not read; write new content to EEPROM first"));
#endif
    }
  }
  else {
#ifdef DEBUGEEPROM
    Serial.println(F("Error - signature mismatch, EEPROM content not read; write new content to EEPROM first"));
#endif
  }
  return (retval);
} //end readEEPROM

void writeEEPROM() { //***************************************  writeEEPROM  **************************************************
  EEPROM.put(0, SIGNATURE); //write the signature
  //write the array size
  EEPROM.put(2, NUMCHAN);
  //write the array
  EEPROM.put(3, Turnouts);
#ifdef DEBUGEEPROM
  Serial.println(F("EEPROM Written"));
#endif
}  //end writeEEPROM

//********************************* Local data for ScanButtons  **********************************************
int prevButton = -1;       //default previous button to "not pressed"
int prevReport = -1;       //default previous reported button to "not pressed"
uint32_t buttonTimer = 0;  //used as debounce timer for buttons
char results[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', '-', '=', '+', 'S', 'P'}; //defines the possible values to return

int scanButtons() {                     //***********  scan Buttons  ***********************
  //returns 0..9, A..B and +=-SP only after debounce, and -1 signifies no button pressed
  //read the analog and use map to determine a value(0->?); convert that value to a character. See spreadsheet for resistor values.
  int val = analogRead(A7);
  val = map(val, 0, 1023, 0, bttn_map);
  if (val >= (int) sizeof(results) / sizeof(results[0])) val = -1; //guard against higher values.
  else val = results[val];
  if (val != prevButton) {
    prevButton = val;
    buttonTimer = millis();
  }
  else  {                                                     //okay, is same as previous button, check for debounce, and then check for previous report
    if (millis() - buttonTimer < DEBOUNCEDURATION) val = -1;  //not ready to report yet
    else if (val == prevReport)  val = -1;                    //if we reported this one already, don't do it again; if we wanted auto-repeat, this is where we watch for a longer period
  }
  return (val);
} //end scanButtons

int checkSerial(int retval) {                    //***********  check Serial  **********************
  //returns 0..9, A..B and -=+SPL  (any other serial character as well, so beware).
  //caller must ignore all other return values.  -1 returned if no character
  if (Serial.available()) {
    retval = Serial.read();
  }
  return (retval);
} //end checkSerial

//Keypad codes and ASCII keys returned for:
//     FUNCTION     |     KEY    |    WOKWI HEX   |   WOKWI DECIMAL
// Prog (none) Save |  P       S | 0xA2      0xE2 | 162         226
//(none)  inc (none)|            | 0x22 0x02 0xC2 | 034   002   194
//  dec   dec   inc |  -   =   + | 0xE0 0xA8 0x90 | 224   168   144
//   1     2     3  |  0   1   2 | 0x68 0x98 0xB0 | 104   152   176
//   4     5     6  |  3   4   5 | 0x30 0x18 0x7A | 048   024   122
//   7     8     9  |  6   7   8 | 0x10 0x38 0x5A | 016   056   090
//  10    11    12  |  9   A   B | 0x42 0x4A 0x52 | 066   074   082
#ifdef WOKWI
const uint8_t IRCodes[][20] =
{ {0xA2, 0xE2, 0x22, 0x02, 0xC2, 0xE0, 0xA8, 0x90, 0x68, 0x98, 0xB0, 0x30, 0x18, 0x7A, 0x10, 0x38, 0x5A, 0x42, 0x4A, 0x52},
  { 'P',  'S',  ' ',  ' ',  ' ',  '-',  '=',  '+',  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',  '9',  'A',  'B'}
};
#endif

//Keypad codes and ASCII keys returned for:
//     FUNCTION     |     KEY    |   AMAZON HEX
// Prog (none) Save |  P       S | 0x45 0x46 0x47
//(none)(none)(none)|            | 0x44 0x40 0x43
//  dec  oper   inc |  -   =   + | 0x07 0x15 0x09
//   1     2     3  |  0   1   2 | 0x16 0x19 0x0d
//   4     5     6  |  3   4   5 | 0x0c 0x18 0x5e
//   7     8     9  |  6   7   8 | 0x08 0x1c 0x5a
//  10    11    12  |  9   A   B | 0x42 0x52 0x4a

#ifdef AMAZON
const uint8_t IRCodes[][21] =
{ {0x45, 0X46, 0x47, 0x44, 0x40, 0x43, 0x07, 0x15, 0x09, 0x16, 0x19, 0x0D, 0x0C, 0x18, 0x5E, 0x08, 0x1C, 0x5A, 0x42, 0x52, 0x4A},
  { 'P',  ' ',  'S',  ' ',  ' ',  ' ',  '-',  '=',  '+',  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',  '9',  'A',  'B'}
};
#endif

int sizeCodes = sizeof(IRCodes) / sizeof(IRCodes[0][0]) / 2;

int checkIR(int retval) {                       //***********  check IR Remote  *******************
  //returns 0..9, A..B and -=+SP
  if (IrReceiver.decode()) {
    retval = parseCodes(IrReceiver.decodedIRData.command);
    IrReceiver.resume();  // Receive the next value
  }
  return (retval);
} //end checkIR

int parseCodes(int ch) {  //looks up the IR code received, returns the character command
  int retval = -1;
  for (int n = 0; n < sizeCodes; n++)
    if (ch == IRCodes[0][n]) {
      retval = IRCodes[1][n];
      break;
    }
  return retval;
}//  end parseCodes