/*
 * Gray_RocketChallenge v1
 * Code by: Gray Mack
 * Published at: TBD
 * Description: Solve some code puzzles to launch a simulated rocket into space
 * Why I made this:
 * I see a lot of teaching in the form of tutorials, going though what someone else is doing and copying.
 * I wanted to try something a little different. I want to present a 
 * framework with some places to fill in the code to solve problems
 * Hopefully this will be a fun learning challenge.
 *
 * Scroll down to find the <<< CHALLENGE >>> sections where you will fill in the code
 * You should not need to change setup or loop.
 * It is a simulation, not very realistic :)
 * 
 * License: MIT License
 *          https://choosealicense.com/licenses/mit/
 * Created: 11/21/2022
 * Board: Arduino Uno or compatible
 * Select Board: Arduino Uno
 * Processor: ATMEGA328p
 * Simulation: https://wokwi.com/projects/348554683465335379
 * 
 * Connections:
 *  connect mini 16ohm speaker/piezo element to pin 9   (and the other lead to GND)
 * 
 * 
 * 11/21/2022 initial code creation
 * 11/24/2022 adding stages
 * 12/01/2022 add win sounds and cleanup up
 */


// ----[ included libraries ]--------------------------------
// none


// ----[ configuration ]------------------------------------
#define SerialBaud 9600
#define SpeedFactor 5 // 9=slow 1=superfast

// ----[ pin definitions ]-----------------------------------
const int PinLaunchButton = 2;           // connect pin 2 to button and other end of button to gnd. Internal pullup is used
const int PinFuelLed = 10;               // connect pin 10 to 220 ohm or greater resistor to RED led to ground
const int PinOxidizerLed = 11;           // connect pin 10 to 220 ohm or greater resistor to BLUE led to ground
const int PinGreenLight = 13;            // connect pin 13 to 220 ohm or greater resistor to GREEN led to ground
const int PinSpeaker = 9;                // connect a piezo element or small 16ohm speaker (as a misnomer this is often called a "buzzer" which is something else)


// ----[ constants ]-----------------------------------------

const int MinimumTone = 31; // min value for tone function according to tone documentation
const int RocketNozelMeltPoint = 2350;

#define DONE 0
#define SILENT 1
#define NOTE_CS5 554
#define NOTE_AS4 466
#define NOTE_GS4 415
#define NOTE_G4  392
#define NOTE_FS4 370
#define NOTE_C4  262
#define NOTE_B3  247
#define NOTE_A3  220
#define LENGTH_FULL 300
#define LENGTH_HALF 150
int Tune1Note[] = {    NOTE_CS5,    NOTE_AS4,    NOTE_GS4,    NOTE_GS4,     NOTE_G4,      SILENT,
                       NOTE_FS4,      SILENT,     NOTE_C4,     NOTE_C4,     NOTE_A3,        DONE };
int  Tune1Len[] = { LENGTH_FULL, LENGTH_FULL, LENGTH_HALF, LENGTH_HALF, LENGTH_HALF, LENGTH_HALF, 
                    LENGTH_HALF, LENGTH_HALF, LENGTH_HALF, LENGTH_HALF, LENGTH_FULL,        DONE };

// ----[ predeclarations ]-----------------------------------
void setup();
void loop();
enum FlightStateEnum : uint8_t;
void InitializeRocket();
// ---- user challenge code
void ManageCountdown(FlightStateEnum rState, int countdown);
void ArmSolidBooster();
void IgniteSolidBooster();
void ManageSolidBoosterEngines(int time, float altitudeKm, int speed, bool SolidBoosterEngineOn);
void JettisonSolidBoosters();
void IgniteStage1Engine();
void CoolNavigationComputer();
void NavigationFanEnable(bool turnOn);
void ManageStage1Engine(int tTime, float altitudeKm, int speed);
void ShutdownStage1Engine();
void IgniteStage2Engine();
void ManageStage2Engine(int tTime, float altitudeKm, int speed, int thrust, int engineNozzleTemperatureDegC);
void Stage2OxidizerPump(int percent);
void Stage2FuelPump(int percent);
int SetRegenerativeCooling(int percent);
void ManageSatelliteSeparation();
// ---- support code
void VerifyManageCountdown(int countdown);
void VerifyManageSolidBoosterEngines();
void VerifyCoolNavigationComputer();
void VerifyManageStage1Engine();
void VerifyManageStage2Engine();
void VerifyManageSatelliteSeparation();
void Fail(int challenge, String reason);
void Win();
void ReportStatus();
int CurveCalculation(float a, float h, float x, float cmax);
bool SimulateFlight();
// ---- sounds
void soundEffectCountdownTick();
void soundEffectRamp();
void soundEffectSpeedupRandomTones();
void SoundEffectEngineOn();
void soundEffect3Beep();
void PlayNotes(int *noteFreqs, int *noteLengths);

// ----[ global variables ]----------------------------------

// An enum gives readable names to a set of states that we want to go through
enum FlightStateEnum : uint8_t {
  WaitingForGo = 0, // internally an enum is just an int, we tell the compiler the first name will be the value of 0. The following values will automatically map to 1, 2, 3, etc
  CountingDown,
  SolidBoosterStage0FlameOn,
  Stage1FlameOn,
  Stage2FlameOn,
  EnterOrbit,
  EjectPayload,
  RocketLaunchScrubbed
};
FlightStateEnum FlightState;

// A struct is a container that holds several related things
struct RocketStatusStruct {
  float AltitudeKm;
  int SpeedKmPerHr;
  int TimeSeconds;
  bool SolidBoosterArmed;
  bool SolidBoosterIgnited;
  bool SolidBoosterJettisoned;
  bool Stage1EngineIgnited;
  int TimeOfSolidBoosterJettisoned;
  int TimeStage1Ignited;
  bool NavigationComputerFanOn;
  bool Stage1Jettisoned;
  int TimeOfStage1Jettisoned;
  bool Stage2EngineIgnited;
  int TimeStage2Ignited;
  char Stage2FuelPercent;
  char Stage2OxidixerPercent;
  char Stage2RegenCoolingPercent;
  char Stage2ThrustPercent;
  int Engine2NozzleTempDegC;
  char RegenerativeCoolingEffectiveness;
  bool Stage2Complete;
};
RocketStatusStruct RocketStatus;

bool GoForLaunch;


// ----[ code ]----------------------------------------------
void setup() {
  Serial.begin(SerialBaud);
  Serial.println("Rocket Challenge!");
  pinMode(PinLaunchButton, INPUT_PULLUP);
  pinMode(PinSpeaker, OUTPUT);
  digitalWrite(PinSpeaker, LOW);
  pinMode(PinFuelLed, OUTPUT);
  analogWrite(PinFuelLed, 0);
  pinMode(PinOxidizerLed, OUTPUT);
  analogWrite(PinOxidizerLed, 0);
  pinMode(PinGreenLight, OUTPUT);
  digitalWrite(PinGreenLight, LOW);
}

void loop() {
  InitializeRocket();
  Serial.println(F("Press button to begin launch sequence.."));
  FlightState = CountingDown;
  while(digitalRead(PinLaunchButton)==HIGH)
  {
    // do nothing until it's time to launch
  }

  Serial.println(F("Beginning countdown..."));
  for(int cd = 10; cd >= 0; cd--)
  {
    digitalWrite(PinGreenLight, HIGH);
    RocketStatus.TimeSeconds = -cd;
    ReportStatus();
    ManageCountdown(FlightState, cd);
    VerifyManageCountdown(cd);
    if(RocketStatus.TimeSeconds != -cd) Fail(1, F("Problem with the countdown"));
    soundEffectCountdownTick();
    delay(100);
    digitalWrite(PinGreenLight, LOW);
    delay(SpeedFactor*100);
  }
  
  FlightState = SolidBoosterStage0FlameOn;
  Serial.println(F("We have liftoff!"));
  bool flight = true;
  while(flight)
  {
    if(FlightState == SolidBoosterStage0FlameOn)
    {
      ManageSolidBoosterEngines(RocketStatus.TimeSeconds, 
                                RocketStatus.AltitudeKm, 
                                RocketStatus.SpeedKmPerHr, 
                                RocketStatus.SolidBoosterIgnited && !RocketStatus.SolidBoosterJettisoned);
    
      VerifyManageSolidBoosterEngines();
    }
          
    if(FlightState == Stage1FlameOn)
    {
      ManageStage1Engine(RocketStatus.TimeSeconds,
                         RocketStatus.AltitudeKm,
                         RocketStatus.SpeedKmPerHr);
      CoolNavigationComputer();
      VerifyCoolNavigationComputer();      
      VerifyManageStage1Engine();
    }
  
    if(FlightState == Stage2FlameOn)
    {
      ManageStage2Engine(RocketStatus.TimeSeconds,
                         RocketStatus.AltitudeKm,
                         RocketStatus.SpeedKmPerHr,
                         RocketStatus.Stage2ThrustPercent,
                         RocketStatus.Engine2NozzleTempDegC);
      VerifyManageStage2Engine();
    }
    
    if(FlightState == EnterOrbit)
    {
      ManageSatelliteSeparation();
      VerifyManageSatelliteSeparation();
    }
      
    ReportStatus();
    flight = SimulateFlight();
    if(RocketStatus.Stage2EngineIgnited)
    {
      analogWrite(PinFuelLed, RocketStatus.Stage2FuelPercent*2);
      analogWrite(PinOxidizerLed, RocketStatus.Stage2OxidixerPercent*2);
    }

    digitalWrite(PinGreenLight, HIGH);
    delay(100);
    digitalWrite(PinGreenLight, LOW);
    delay(SpeedFactor*100);
  }

  // notes
  // https://mars.nasa.gov/odyssey/mission/timeline/mtlaunch/launch1/
  // Speed of GEO satellites should be about 3 km per second at an altitude of 35 786 km
  // low altitude polar orbiter is about 850km
  // Solid booster separation 66sec @ altitude 18.5km speed 3550km/h and stage 1 fire
  // Solid booster  stage 1 separation T 136sec @ altitude 57km, speed 8773km/h
  // stage 1 cutoff T 263 sec @ altitude 126km, speed 21475km/h
  // stage 2 ignition 276 sec @ altitude 134km, speed 21479km/h
  // stage 2 cutoff 603 sec @ altitude 195km, speed 28055km/h
  // satellite separation can then take place
  // 1km == 0.539956803nmi (nautical mile)
}

void InitializeRocket()
{
  FlightState = WaitingForGo;
  RocketStatus.AltitudeKm = 0.0;    // notice how RocketStatus is of type RocketStatusStruct and it groups together things like Altitude and Speed within it.
  RocketStatus.SpeedKmPerHr = 0;
  RocketStatus.SolidBoosterArmed = false;
  RocketStatus.SolidBoosterIgnited = false;
  RocketStatus.SolidBoosterJettisoned = false;
  RocketStatus.Stage1EngineIgnited = false;
  RocketStatus.TimeOfSolidBoosterJettisoned = 0;
  RocketStatus.TimeStage1Ignited = 0;
  RocketStatus.NavigationComputerFanOn = false;
  RocketStatus.Stage1Jettisoned = false;
  RocketStatus.TimeOfStage1Jettisoned = 0;
  RocketStatus.Stage2EngineIgnited = false;
  RocketStatus.TimeStage2Ignited = 0;
  RocketStatus.Stage2FuelPercent = 50;
  RocketStatus.Stage2OxidixerPercent = 50;
  RocketStatus.Stage2RegenCoolingPercent = 0;
  RocketStatus.Stage2ThrustPercent = 0;
  RocketStatus.Engine2NozzleTempDegC = 60;
  RocketStatus.RegenerativeCoolingEffectiveness = 0;
  RocketStatus.Stage2Complete = false;

  GoForLaunch = true;
}


void ManageCountdown(FlightStateEnum rState, int countdown)
{
  if(rState == CountingDown)
  {
    // <<<<<<<<<<< CHALLENGE 1 >>>>>>>>>>
    // Utilize the following local and global variables as needed: rState countdown GoForLaunch
    // Write the following code: When the countdown is 1 then call ArmSolidBooster()
    //  and when the countdown is 0 call IgniteSolidBooster()
    // However don't do these things unless rState is CountingDown and GoForLaunch is true.
    // Do not call the Booster functions more than once.
    //---------- insert code

    //----------
  }
}

void ArmSolidBooster()
{
  if(RocketStatus.SolidBoosterArmed) Fail(1, F("ArmSolidBooster called more than once"));
  RocketStatus.SolidBoosterArmed = true;
  Serial.println(F("  Solid Booster Armed"));
}

void IgniteSolidBooster()
{
  if(RocketStatus.SolidBoosterIgnited) Fail(1, F("IgniteSolidBooster called more than once"));
  if(!RocketStatus.SolidBoosterArmed) Fail(1, F("IgniteSolidBooster called but booster was not armed first"));
  RocketStatus.SolidBoosterIgnited = true;
  Serial.println(F("\n\n  Solid Booster Ignited"));
  SoundEffectEngineOn();
}

//---------- Space between this block for user to add global variables to help with challenge 2
bool jettisoned = false;
int timeJettisoned = 0;
bool ignited = false;
//---------- end global section
void ManageSolidBoosterEngines(int tTime, float altitudeKm, int speed, bool SolidBoosterEngineOn)
{
  // <<<<<<<<<<< CHALLENGE 2 >>>>>>>>>>
  // The solid booster is designed to thrust until propellant is exhausted it cannot be shut down.
  // Ensure the rocket has reached an altitude of >= 57km then call JettisonSolidBoosters(). Only call this once.
  // Then keep checking until 4 seconds from the tTime you jettisoned the booster and IgniteStage1Engine(); Only call this once.
  // hint: this function may be called more or less than once per second so use tTime values and 
  //       static or global variables to keep track of state
  //---------- insert code

  //---------- 
}

void JettisonSolidBoosters()
{
  Serial.print(F("  Jettison Solid Boosters at ")); Serial.println(RocketStatus.TimeSeconds);
  if(RocketStatus.SolidBoosterJettisoned) Fail(2, F("SolidBoosterJettisoned was already jettisoned. Can not do it again"));
  RocketStatus.SolidBoosterJettisoned = true;
  RocketStatus.TimeOfSolidBoosterJettisoned = RocketStatus.TimeSeconds;
}


void IgniteStage1Engine()
{
  Serial.print(F("\n\n  Ignite Stage 1 Engine at ")); Serial.println(RocketStatus.TimeSeconds);
  if(RocketStatus.Stage1EngineIgnited) Fail(2, F("IgniteStage1Engine was already called. Can not ignite it again"));
  if(! RocketStatus.SolidBoosterJettisoned) Fail(2, F("IgniteStage1Engine was called but SolidBooster has not been Jettisoned"));
  RocketStatus.TimeStage1Ignited = RocketStatus.TimeSeconds;  
  RocketStatus.Stage1EngineIgnited = true;
  SoundEffectEngineOn();
}

void CoolNavigationComputer()
{
  // <<<<<<<<<<< CHALLENGE 3 >>>>>>>>>>
  // We need to run a cooling fan on the navigation computer so it does not overheat
  // running at full on wastes too much power. For electrical reasons, 
  // it is impractical to run it at half voltage, instead lets run it at 50% duty cycle
  // toggle it on/off toggeling each call using NavigationFanEnable() with the parameter true or false
  // you may use a static variable to keep track of it's previous state
  // https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/static/
  //---------- insert code

  //----------
}

void NavigationFanEnable(bool turnOn)
{
  RocketStatus.NavigationComputerFanOn = turnOn;
}

void ManageStage1Engine(int tTime, float altitudeKm, int speed)
{
  // <<<<<<<<<<< CHALLENGE 4 >>>>>>>>>>
  // Whan an altitude of 126km is reached or a speed of 21475km/h is reached then call ShutdownStage1Engine()
  // When an altitude of 134km is reached or speed 21479km/h is reached then call IgniteStage2Engine()
  // do not call the functions more than once. The spent stage will automatically eject and fall away
  //---------- insert code

  //----------
}

void ShutdownStage1Engine()
{
  Serial.print(F("  Shutdown Stage1 Engine at ")); Serial.println(RocketStatus.TimeSeconds);
  if(RocketStatus.Stage1Jettisoned) Fail(4, F("ShutdownStage1Engine was already shutdown. Can not do it again"));
  RocketStatus.Stage1Jettisoned = true;
  RocketStatus.TimeOfStage1Jettisoned = RocketStatus.TimeSeconds;
}

void IgniteStage2Engine()
{
  Serial.print(F("\n\n  Ignite Stage 2 Engine at ")); Serial.println(RocketStatus.TimeSeconds);
  if(RocketStatus.Stage2EngineIgnited) Fail(4, F("IgniteStage2Engine was already called. Can not ignite it again"));
  if(! RocketStatus.Stage1Jettisoned) Fail(4, F("IgniteStage2Engine was called but Stage1 was not Shutdown"));
  RocketStatus.TimeStage2Ignited = RocketStatus.TimeSeconds;  
  RocketStatus.Stage2EngineIgnited = true;
  SoundEffectEngineOn();
}


void ManageStage2Engine(int tTime, float altitudeKm, int speed, int thrust, int engineNozzleTemperatureDegC)
{
  // <<<<<<<<<<< CHALLENGE 5 >>>>>>>>>>
  // This stage requires careful management of the engine
  // you may add global or static variables to help.
  // Change the Stage2OxidixerPump(percent) and Stage2FuelPump(percent)
  // The two percents should always total to 100 percent or the engine will be damaged.
  // A stoichiometric ratio of 30fuel/70oxidizer will give the best burn however
  // your engineNozzleTemperatureDegC will then hit 3727C
  // Though your engine nozzle is made of C-103 Nb (niobium+hafnium+titanium alloy), it will 
  // still melt at 2350C which would not be good.
  // You can cycle a portion of your fuel through the hollow engine nozzle using SetRegenerativeCooling(percent)
  // The liquified fuel will expand to a gas state in there which produces cooling
  // however there is a limit which may fluctuate a bit over time.
  // The function returns an int which is the effectiveness percent.
  // You can call SetRegenerativeCooling() up to 5 times, then you must wait for the next call to set it again
  // Setting it to 100 would be bad so find a good setting to get the highest percent return
  // Probably somewhere between 5 and 20%
  // Even with this cooling, you may need to run fuel rich (higher percent fuel) to keep the engine from melting
  //---------- insert/modify code
  static int lastFuelPercent = 80;
  static int lastOxidizerPercent = 20;
  static int lastCoolingPercent = 0;
  Stage2FuelPump(lastFuelPercent);
  Stage2OxidizerPump(lastOxidizerPercent);
  int coolingEfficiencyPercent = SetRegenerativeCooling(lastCoolingPercent);

  //----------
}

void Stage2OxidizerPump(int percent)
{
  if(percent < 0 || percent > 100) Fail(5, F("Stage2OxidizerPump() range is 0-100 only"));
  RocketStatus.Stage2OxidixerPercent = percent;
}

void Stage2FuelPump(int percent)
{
  if(percent < 0 || percent > 100) Fail(5, F("Stage2FuelPump() range is 0-100 only"));
  RocketStatus.Stage2FuelPercent = percent;
}

// https://www.youtube.com/watch?v=he_BL6Q5u1Y everyday astronaut explans rocket cone cooling 
// rocket engine fuel/oxidizer 50/50mix 3250K/2977C. 
//                             70/30mix 3000K/2727C. (fuel rich)
//                             30/70mix 4000K/3727C - stoichiometric ratio for best burn
// film cooling by injecting fuel along the outer edge of the engine
// regenerative cooling by expanding some of the gasses in a thin layer between the cone walls
// Graphite tungston aluminum alloy melting point 1300C
// Niobium Melting point 4476F / 2468C
// C-103 Nb is a unique niobium alloy which comprises niobium with 10% hafnium and 1% titanium
// You can call SetRegenerativeCooling up to 5 times per time cycle to hone on a better value 
int SetRegenerativeCooling(int percent)
{
  if(percent < 0 || percent > 100) Fail(5, F("SetRegenerativeCooling() range is 0-100 only"));
  static int calls = 1;
  static int lastCallTime = 0;
  if(RocketStatus.TimeSeconds == lastCallTime)
  {
    if(++calls > 5) Fail(5, F("SetRegenerativeCooling() called more than 5 times per cycle"));
  }
  else
  {
    lastCallTime = RocketStatus.TimeSeconds;
    calls = 1;
  }
  RocketStatus.Stage2RegenCoolingPercent = percent;
  const float a = -0.08; // tightness of the curve
  float h = 5+(RocketStatus.Stage2OxidixerPercent/10);  // curve peak
  float c = 100.0; // highest value
  int yInt = CurveCalculation(a, h, percent, c);
  RocketStatus.RegenerativeCoolingEffectiveness = yInt;  
  return yInt;
}



void ManageSatelliteSeparation()
{
  // <<<<<<<<<<< CHALLENGE 6 >>>>>>>>>>
  // Not implemented yet so if you get here, you win!
}




//======================================= Support functions, you don't have to change anything below this point



void VerifyManageCountdown(int countdown)
{
  if(countdown > 1 && RocketStatus.SolidBoosterArmed) Fail(1, "SolidBoosterArmed before countdown was 1");
  if(countdown > 0 && RocketStatus.SolidBoosterIgnited) Fail(1, "SolidBoosterIgnited before countdown was 0");
  if(countdown == 2) // test to ensure GoForLaunch is respected
  {
    GoForLaunch = false;
    ManageCountdown(FlightState, 1);
    if(RocketStatus.SolidBoosterArmed) Fail(1, F("At the last second GoForLaunch was false, but the Booster got armed anyway"));
    GoForLaunch = false;
    ManageCountdown(FlightState, 0);
    if(RocketStatus.SolidBoosterIgnited) Fail(1, F("At the last second GoForLaunch was false, but the Booster got ignited anyway"));
    ManageCountdown(RocketLaunchScrubbed, 0);
    if(RocketStatus.SolidBoosterIgnited) Fail(1, F("At the last second FlightState was changed to RocketLaunchScrubbed, but the Booster got ignited anyway"));
    GoForLaunch = true; // restore for normal operation
  }
  if(countdown == 1 && !RocketStatus.SolidBoosterArmed) Fail(1, "Countdown hit 1 but Solid Booster was not Armed");
  if(countdown == 0 && !RocketStatus.SolidBoosterIgnited) Fail(1, "Countdown hit 0 but Solid Booster was not Ignited");
}

void VerifyManageSolidBoosterEngines()
{
  if(FlightState != SolidBoosterStage0FlameOn) return;
  if(!RocketStatus.SolidBoosterIgnited) Fail(2, F("The Solid Booster should be ignited"));
  if(RocketStatus.AltitudeKm >= 57.0)
  {
    if(!RocketStatus.SolidBoosterJettisoned) Fail(2, F("Solid Booster was not Jettisoned by Altitude 57Km"));
    if(RocketStatus.Stage1EngineIgnited)
    {
      if(RocketStatus.TimeOfSolidBoosterJettisoned + 4 != RocketStatus.TimeStage1Ignited ) Fail(2, F("Stage1 Engine ignited too soon"));
      FlightState = Stage1FlameOn;
    }
    else
    {
      if(RocketStatus.TimeSeconds > RocketStatus.TimeOfSolidBoosterJettisoned + 4) Fail(2, F("Stage1 Engine not ignited 4 seconds after Solid Booster jettisoned"));
    }
  }
  else
  {
    if(RocketStatus.SolidBoosterJettisoned) Fail(2, F("Solid Booster Jettisoned before reaching proper altitude"));
  }
  // Ensure the rocket has reached an altitude of >= 57km then call JettisonSolidBoosters()
  // Then keep checking until 2 seconds from tTime you jettisoned the booster and IgniteStage1Engine();
}

void VerifyCoolNavigationComputer()
{
  static bool lastFanState = false;
  static byte numErrors = 0;
  if(RocketStatus.NavigationComputerFanOn == lastFanState)
  {
    if(++numErrors > 5) Fail(3, F("Navigation computer fan is not toggling on and off for 50% duty cycle"));
  }
  lastFanState = RocketStatus.NavigationComputerFanOn;
}

void VerifyManageStage1Engine()
{
  if(!RocketStatus.Stage1EngineIgnited) Fail(4, F("Managing Stage1 engines but they are not ignited"));
  if(FlightState != Stage1FlameOn) return;
  // Whan an altitude of 126km is reached or a speed of 21475km/h is reached then call ShutdownStage1Engine()  
  if(RocketStatus.AltitudeKm >= 126)
  {
    if(!RocketStatus.Stage1Jettisoned) Fail(4, F("126km Altitude reached but failed to jettison stage 1"));
  }
  else if(RocketStatus.SpeedKmPerHr >= 21475)
  {
    if(!RocketStatus.Stage1Jettisoned) Fail(4, F("21475km/hr speed reached but failed to jettison stage 1"));
  }
  else
  {
    if(RocketStatus.Stage1Jettisoned) Fail(4, F("Stage 1 Jettisoned too soon"));
  }
  // When an altitude of 134km is reached or speed 21479km/h is reached then call IgniteStage1Engine()
  if(RocketStatus.AltitudeKm >= 134)
  {
    if(!RocketStatus.Stage2EngineIgnited) Fail(4, F("134km Altitude reached but failed to ignite stage 2"));
    FlightState = Stage2FlameOn;
  }
  else if(RocketStatus.SpeedKmPerHr >= 21479)
  {
    if(!RocketStatus.Stage2EngineIgnited) Fail(4, F("21479km/hr speed reached but failed to ignite stage 2"));
    FlightState = Stage2FlameOn;
  }
  else
  {
    if(RocketStatus.Stage2EngineIgnited) Fail(4, F("Stage 2 Ignited too soon"));
  }
}

const int expectedStage2BurnTime = 327;
const int stageBurnTime85PercentOffset = 327/(100-85);
const int allowedStage2BurnTime = expectedStage2BurnTime+stageBurnTime85PercentOffset;
const int altitudeDelta90PercentOffset = 69/(100-90);
const int expectedStage2AltitudeDelta = 69+altitudeDelta90PercentOffset;
const int speedDelta90PercentOffset = 9555/(100-90);
const int expectedStage2SpeedDelta = 9555+speedDelta90PercentOffset;
const int allowedOverheats = 20;
void VerifyManageStage2Engine()
{
  static int overheats = 0;
  if(FlightState != Stage2FlameOn) return;
  if((RocketStatus.Stage2OxidixerPercent + RocketStatus.Stage2FuelPercent) != 100) Fail(5, F("The Fuel/Oxidizer percents must total 100%"));
  if(RocketStatus.Engine2NozzleTempDegC > RocketNozelMeltPoint) overheats++;
  if(overheats > allowedOverheats) Fail(5, F("The Stage2 nozel is too hot, it melted!\nEnsure cooling is near 100% effective and run fuel rich (higher than optimum) to keep the temperature below melting point."));
  if(RocketStatus.TimeSeconds > RocketStatus.TimeStage2Ignited+allowedStage2BurnTime) Fail(5, F("The Stage2 was not efficient enough to reach trajectory before fuel/oxidizer ran out"));
  // stage 2 cutoff 603 sec (327 burn) @ altitude 195km (69 delta), speed 28055km/h (6580 delta)
  if(RocketStatus.AltitudeKm >= 195.0 && RocketStatus.SpeedKmPerHr >= 28055)
  {
    Serial.println(F("Orbital speed and altitude reached!"));
    FlightState = EnterOrbit;
    RocketStatus.Stage2EngineIgnited = false;
    RocketStatus.Stage2FuelPercent = 0;
    RocketStatus.Stage2OxidixerPercent = 0;
    RocketStatus.Stage2Complete = true;
    analogWrite(PinFuelLed, 0);
    analogWrite(PinOxidizerLed, 0);
  }
  
}

void VerifyManageSatelliteSeparation()
{
  if(FlightState != EnterOrbit) return;
  // satellite separation
  Serial.println(F("Satellite payload successfully launched!"));
  FlightState = EjectPayload;
  Win();   // not implemented yet so Win! 
}



void Fail(int challenge, String reason)
{
  Serial.println(F("Oh no, rocket challenge FAIL!"));
  if(challenge > 0) {
    Serial.print(F("Challenge ")); Serial.println(challenge);
  }
  Serial.println(reason);
  soundEffectRamp();
  soundEffect3Beep();
  Serial.println(F("Please check your code"));
  Serial.println(F("Hit reset button to start the launch again, but you need to fix your code first"));
  while(true) { } // wait forever
}

void Win()
{
  Serial.println(F("+++ Mission complete. Congradulations! +++"));
  soundEffectSpeedupRandomTones();
  delay(200);
  PlayNotes(Tune1Note, Tune1Len);
  while(true) { } // wait forever
}


void ReportStatus()
{
  static int counter = 0;
  counter = 0;
  Serial.print("T"); 
  if(RocketStatus.TimeSeconds >= 0) Serial.print("=");
  Serial.print(RocketStatus.TimeSeconds);
  if(RocketStatus.AltitudeKm > 0.0 || RocketStatus.SpeedKmPerHr > 0)
  {
    Serial.print(F("   Alt=")); Serial.print(RocketStatus.AltitudeKm, 1);
    Serial.print(F("km   Speed=")); Serial.print(RocketStatus.SpeedKmPerHr);
    Serial.print(F("km/hr"));
  }
  Serial.println();
  if(FlightState == Stage2FlameOn)
  {
    Serial.print(F("        fuel/oxi="));
    Serial.print((int)RocketStatus.Stage2FuelPercent);
    Serial.print("/");
    Serial.print((int)RocketStatus.Stage2OxidixerPercent);
    Serial.print(F(" cooling%/effective="));
    Serial.print((int)RocketStatus.Stage2RegenCoolingPercent);
    Serial.print("/");
    Serial.print((int)RocketStatus.RegenerativeCoolingEffectiveness);
    Serial.print(F(" Thrust%="));
    Serial.print((int)RocketStatus.Stage2ThrustPercent);
    if(RocketStatus.Stage2ThrustPercent < 50)
    {
      Serial.print(F("<Warning>"));
    }
    Serial.print(F(" NozzleTemp="));
    Serial.print(RocketStatus.Engine2NozzleTempDegC);
    if(RocketStatus.Engine2NozzleTempDegC > RocketNozelMeltPoint)
    {
      Serial.print(F("<Warning>"));
    }
    Serial.println();
  }
}

// calculate a value on a peak curve
// a is the curve thickness -2 is very tight peak, 0.01 is very wide, 0 is no curve, all x->cmax
// h is the center point of the peak (where cmax value occurs)
// x the input to the equation
// cmax the max value. For percent we want the peak to be at 100.0
// use https://www.desmos.com/calculator with the formula a * (x-h)^{2} + c
int CurveCalculation(float a, float h, float x, float cmax)
{
  float y = a*pow(x - h,2)+cmax;
  if(y < 0.0) y = 0.0;
  int yInt = constrain((int)(y+0.5), 0, 100);
  return yInt;
}

bool SimulateFlight()
{  
  const byte RealTime = 1;
  const byte DoubleTime = 2;
  const byte QuadTime = 4;
  byte timeMultiplier;
  
  switch(FlightState)
  {
    case WaitingForGo: 
      break;
    case CountingDown:
      break;
    case SolidBoosterStage0FlameOn:
      // Solid booster separation T 136sec @ altitude 57km, speed 8773km/h
      timeMultiplier = DoubleTime;
      RocketStatus.TimeSeconds += timeMultiplier;
      RocketStatus.SpeedKmPerHr += 8773/(136/timeMultiplier);
      RocketStatus.AltitudeKm += 57.0/(136/timeMultiplier);
      break;
    case Stage1FlameOn:
      // stage 1 cutoff T 263 sec (127 burn) @ altitude 126km (69 delta), speed 21475km/h
      timeMultiplier = QuadTime;
      RocketStatus.TimeSeconds += timeMultiplier;
      RocketStatus.SpeedKmPerHr += 8773/(127/timeMultiplier);
      RocketStatus.AltitudeKm += 69.0/(127/timeMultiplier);
      break;
    case Stage2FlameOn:
      // stage 2 ignition 276 sec @ altitude 134km, or speed 21479km/h (actual speed may be about 18500km/h)
      // stage 2 cutoff 603 sec (327 burn) @ altitude 195km (69 delta), speed 28055km/h (6580 delta) (actual delta may be about 9555)
      timeMultiplier = QuadTime;
      RocketStatus.TimeSeconds += timeMultiplier;

      // rocket engine fuel/oxidizer 50/50mix 3250K/2977C. 
      //                             70/30mix 3000K/2727C. (fuel rich)
      //                             30/70mix 4000K/3727C - stoichiometric ratio for best burn
      
      //RocketStatus.Stage2RegenCoolingPercent not needed 
      //RocketStatus.c is calculated by SetRegenerativeCooling()
      //Stage2FuelPercent is not needed since it is 100-Stage2OxidixerPercent
      // calculate Stage2Thrust
      const float a = -0.03; // tightness of the curve
      const float IdeaOxidizerValue = 70.0;
      char yInt = CurveCalculation(a, IdeaOxidizerValue, RocketStatus.Stage2OxidixerPercent, 100.0);
      RocketStatus.Stage2ThrustPercent = yInt;
      // calculate Engine2NozzleTempDegC
      if(RocketStatus.Stage2OxidixerPercent <= 70)
        // approx y=25x+1970 to get this mapping
        RocketStatus.Engine2NozzleTempDegC = map(RocketStatus.Stage2OxidixerPercent, 30,70, 2727,3727);
      else
        RocketStatus.Engine2NozzleTempDegC = map(RocketStatus.Stage2OxidixerPercent, 70,100, 3727,2960);
      // apply cooling
      RocketStatus.Engine2NozzleTempDegC -= (1000L * RocketStatus.RegenerativeCoolingEffectiveness) / 100;

      float degradedAccelPerSec constrain(map(RocketStatus.Stage2ThrustPercent, 0,100, 0,expectedStage2SpeedDelta), 1316, expectedStage2SpeedDelta);
      float degradedAltitudeGainPerSec = constrain(map(RocketStatus.Stage2ThrustPercent, 0,100, 0,expectedStage2AltitudeDelta), 13,expectedStage2AltitudeDelta);
      //Serial.print("AltGain "); Serial.print(degradedAltitudeGainPerSec,1); Serial.print(" ->"); Serial.println(degradedAltitudeGainPerSec/((float)expectedStage2BurnTime/timeMultiplier),1);
      RocketStatus.SpeedKmPerHr += degradedAccelPerSec/((float)expectedStage2BurnTime/timeMultiplier);
      RocketStatus.AltitudeKm += degradedAltitudeGainPerSec/((float)expectedStage2BurnTime/timeMultiplier);
      break;
    case EnterOrbit:
      RocketStatus.TimeSeconds += DoubleTime;
      break;
    case EjectPayload:
      RocketStatus.TimeSeconds += DoubleTime;
      break;
    case RocketLaunchScrubbed:
      break;
  }
}


void soundEffectCountdownTick()
{
  tone(PinSpeaker, 400, 10);
}


void soundEffectRamp()
{ // rising frequency (frequency ramp)
  for(int repeats = 0; repeats<5; repeats++)
  {
    for(int i=50; i<600; i+=30)
    {
      tone(PinSpeaker, i);
      delay(20);
    }
    noTone(PinSpeaker);
  }
}


void soundEffectSpeedupRandomTones()
{  // speeding up random tones
  const int MinFreq = 440;
  const int MaxFreq = 1881;
  const float speedChange = 0.8;
  float faster = 100.0;
  while(faster > 10)
  {
    faster -= speedChange;
    tone(PinSpeaker, random(MinFreq, MaxFreq), 20);
    delay(faster);
  }
  noTone(PinSpeaker);
}


void SoundEffectEngineOn()
{
  // random frequency static
  for(int repeats = 0; repeats < 200; repeats++)
  {
      tone(PinSpeaker, random(60)+MinimumTone);
      delay(random(10)+5);
  }
  noTone(PinSpeaker);
}


void soundEffect3Beep()
{
  for(int repeat = 0; repeat < 3; repeat++)
  {
    tone(PinSpeaker, 180, 400);
    delay(600);
  }
}


void PlayNotes(int *noteFreqs, int *noteLengths)
{
  int idx = 0;
  while(noteFreqs[idx] != DONE)
  {
    int fullDuration = noteLengths[idx];
    int noteDuration = noteLengths[idx]-20;
    if(noteFreqs[idx] == SILENT)
      noTone(PinSpeaker);
    else
      tone(PinSpeaker,noteFreqs[idx],noteDuration);
    delay(fullDuration);
    idx++;
  }
  noTone(PinSpeaker);
}



// misc research
//https://mars.nasa.gov/odyssey/mission/timeline/mtlaunch/launch1
//https://en.wikipedia.org/wiki/Titan_IVv
//https://spaceflightnow.com/2021/09/27/launch-timeline-for-atlas-5s-mission-with-landsat-9/
//https://www.youtube.com/watch?v=he_BL6Q5u1Y