/*
 * FILENAME: Gray_Hero_ComboCodeLock.ino
 *
 * Code by: Gray Mack
 * License: MIT License
 *          https://choosealicense.com/licenses/mit/
 * Created: 7/13/2022
 * One Line Description: Demonstrate intermediate Arduino techniques with parts from Hero/Lost In Space kit
 * Board: Arduino Uno R3 compatible such as Inventr.io Hero board
 * Select Board: Arduino UNO
 * Other Hardware: Parts from "30 Days Lost In Space kit" or similar parts (I have no affiliation with this vendor)
 * Simulation at: https://wokwi.com/projects/337493013886403154
 * Source control at: TBD
 * Video documentation: https://youtu.be/hwCcHJCEDvQ
 * I don't respond to IM. No specific support is offered though I am in a few Facebook arduino forums.
 * 
 * Detailed Description: 
 * This combo lock works by rotating right to first number, left to second number, right to third, left to fourth, push button
 * You may need to roll over 00 to get to the correct number. You don't have to pass the second number, but if you miss the stop
 * then keep turning through 00 until you reach it again
 * Lost In Space notes:
 *   See day 4 for 3 LED wiring
 *   See day 4 for dip switch wiring but we use INPUT_PULLUP instead of pull down resistors
 *   See day 24 for speaker wiring
 *   See day 29 for encoder wiring
 *   See day 16 for 7segment*4 wiring
 *   See day 22 for oled display wiring
 * 
 * Rev History:
 * 7/13/2022 initial code creation
 * 7/26/2022 add LDR sensor to display
 * 8/15/2022 add shuttle movement and deploy a satellite
 * 8/17/2022 convert shuttle to a bitmap for faster render
 * //2022 ...
 */

// ----[ configuration  ]------------------------------------
#define LOGGING true
#define SERIAL_BAUD 115200
#pragma GCC diagnostic ignored "-Wunused-parameter" // U8glib is filled with warnings
#define INTRO_MESSAGE "Combo Code Lock v1"
#define SERIAL_INTRO_MESSAGE F("Gray_Hero_ComboCodeLock version 1.0")
#define RENDER_SHUTTLE_AS_BITMAP true

// ----[ included libraries ]------------------------------------
#include <Arduino.h>
#include <U8glib.h> // oled display v1.19.1   Reference manual https://github.com/olikraus/u8glib/wiki/userreference
#include <TM1637Display.h> // 7segx4 V1.2.0 with TM1637 spi display driver, see TM1637_V2.4_EN.pdf
#include <RotaryEncoder.h> // RotaryEncoder v1.5.2 https://github.com/mathertel/RotaryEncoder
#include <Button2.h> // Button debouncer v2.0.1   https://github.com/LennartHennigs/Button2/blob/master/examples/MultipleButtons/MultipleButtons.ino

// ----[ pin definitions ]------------------------------------
#define PIN_RX 0
#define PIN_TX 1
#define PIN_DIP_1 2
#define PIN_DIP_2 3
#define PIN_DIP_3 4
#define PIN_7SEG_DISPLAY_CLK 6
#define PIN_7SEG_DISPLAY_DIO 5
#define PIN_BUTTON_UP 7
#define PIN_BUTTON_DOWN 8
#define PIN_SPEAKER 9
#define PIN_LED_RED   10
#define PIN_LED_BLUE  11
#define PIN_LED_GREEN 12
#define PIN_BUILT_IN_LED LED_BUILTIN // (13 built in LED)
#define PIN_LIGHT_DEPENDENT_RESISTOR A0
#define PIN_ROTARY_ENCODER_CLK A1
#define PIN_ROTARY_ENCODER_DT  A2
#define PIN_ROTARY_ENCODER_SWITCH A3
#define PIN_OLED_SDA A4
#define PIN_OLED_SCL A5

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

#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_MAX_X 127
#define OLED_MAX_Y 63
#define SHUTTLE_LOC_X 4
#define SHUTTLE_LOC_Y 11
#define SWITCH_LOC_X (OLED_WIDTH-56)
#define SWITCH_LOC_Y 9 // leave rome for text across the top
#define SWITCH_PANEL_WIDTH 56
#define SWITCH_PANEL_HEIGHT 35
#define GAGUE_LOC_X (OLED_WIDTH-67)
#define GAGUE_LOC_Y 43 // below SWITCH_LOC_Y+SWITCH_PANEL_HEIGHT
#define GAGUE_WIDTH 67
#define GAGUE_HEIGHT 42
#define SATELLITE_OFFSET_X (SHUTTLE_LOC_X+16)
#define SATELLITE_OFFSET_Y (SHUTTLE_LOC_Y+21)
#define SOLAR_PANEL_CLOSED 0
#define SOLAR_PANEL_FULLY_OPEN 11
#define OFF_SCREEN_RIGHT OLED_WIDTH
#define OFF_SCREEN_BOTTOM OLED_HEIGHT

#define DIGITS_USED_IN_CODE 4
#define SEG7_NO_LEADING_ZERO false
#define SEG7_LEADING_ZERO true
#define BAY_CLOSED 0
#define BAY_FULLY_OPEN 9
#define MAX_ADC 1023 // Uno has a 10bit ADC
#define RECENT_USER_INTERACTION_MSEC 3000UL
#define PREVENT_OLED_BURNIN_MSEC (20UL*60UL*1000UL) //20 minutes, this number would overflow the default int if not specified as UL (unsigned long)
// tip: when a constant or variable has units, include them in the name, such as meters/feet, degC/degF. Failure to know units this has cost software companies millions and loss of human life
// research: Space station temperature is usually kept around 22C and the system typically adjustable from 18C to 27C
#define THERMOSTAT_MAX_DEG_C 27.0 // 80.5
#define THERMOSTAT_MIN_DEG_C 18.0 //64.5
#define THERMOSTAT_DEFAULT_DEG_C 22.0 // 72.6deg F, typical space station temperature

const uint8_t SMILE_BITMAP_BWIDTH = 1;
const uint8_t SMILE_BITMAP_HEIGHT = 8;
const uint8_t SMILE_BITMAP[] PROGMEM = { // 1x8
  0b01111110,
  0b10000001,
  0b10100101,
  0b10000001,
  0b10100101,
  0b10011001,
  0b10000001,
  0b01111110,
};

const uint8_t SWITCH_BITMAP_BWIDTH = 2;
const uint8_t SWITCH_BITMAP_HEIGHT = 30;
const uint8_t SWITCH_ON_BITMAP[] PROGMEM = { // 2x30
  0b00000011, 0b11000000,
  0b00001110, 0b01110000,
  0b00001000, 0b00010000,
  0b00001110, 0b01110000,
  0b00001001, 0b10010000,
  0b00001000, 0b00010000,
  0b00000100, 0b00100000,
  0b00111100, 0b00111100,
  0b01000100, 0b00100010,
  0b01000110, 0b01100010,
  0b10001000, 0b00010001,
  0b10001000, 0b00010001,
  0b10001100, 0b00110001,
  0b01000111, 0b11100010,
  0b01100000, 0b00000110,
  0b00011000, 0b00011000,
  0b00000111, 0b11100000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00001100, 0b01001000,
  0b00010010, 0b01101000,
  0b00010010, 0b01011000,
  0b00010010, 0b01011000,
  0b00001100, 0b01001000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000
};

const uint8_t SWITCH_OFF_BITMAP[] PROGMEM = { // 2x30
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000000, 0b00000000,
  0b00000111, 0b11100000,
  0b00011000, 0b00011000,
  0b01100000, 0b00000110,
  0b01000111, 0b11100010,
  0b10001100, 0b00110001,
  0b10001000, 0b00010001,
  0b10001000, 0b00010001,
  0b01000110, 0b01100010,
  0b01000100, 0b00100010,
  0b00111100, 0b00111100,
  0b00000100, 0b00100000,
  0b00001000, 0b00010000,
  0b00001001, 0b10010000,
  0b00001110, 0b01110000,
  0b00001000, 0b00010000,
  0b00001110, 0b01110000,
  0b00000011, 0b11000000,
  0b00000000, 0b00000000,  
  0b00000000, 0b00000000,
  0b00110001, 0b11001110,
  0b01001001, 0b00001000,
  0b01001001, 0b11001110,
  0b01001001, 0b00001000,
  0b00110001, 0b00001000
};

const uint8_t THERMOMETER_BITMAP_BWIDTH = 1;
const uint8_t THERMOMETER_BITMAP_HEIGHT = 16;
const uint8_t THERMOMETER_BITMAP[] PROGMEM = { // 1x16
  0b00111000,
  0b01000100,
  0b01000100,
  0b01001100,
  0b01000100,
  0b01000100,
  0b01001100,
  0b01000100,
  0b01000100,
  0b01001100,
  0b01000100,
  0b01000100,
  0b11111110,
  0b11111110,
  0b11111110,
  0b01111100
};

const uint8_t CROSSHAIR_BITMAP_BWIDTH = 2;
const uint8_t CROSSHAIR_BITMAP_HEIGHT = 15;
const uint8_t CROSSHAIR_BITMAP[] PROGMEM = { // 2x15
  0b00000111, 0b11000000,
  0b00011000, 0b00110000,
  0b00100001, 0b00001000,
  0b01000000, 0b00000100,
  0b01000001, 0b00000100,
  0b10000001, 0b00000010,
  0b10000000, 0b00000010,
  0b10101100, 0b01101010,
  0b10000000, 0b00000010,
  0b10000001, 0b00000010,
  0b01000001, 0b00000100,
  0b01000000, 0b00000100,
  0b00100001, 0b00001000,
  0b00011000, 0b00110000,
  0b00000111, 0b11000000,
};

const uint8_t SATELLITE_BITMAP_BWIDTH = 4;
const uint8_t SATELLITE_BITMAP_HEIGHT = 8;
const uint8_t SATELLITE_BITMAP[] PROGMEM = { // 4x8
  0b01100111, 0b11110001, 0b11111100, 0b00000000, 
  0b00110100, 0b00011011, 0b01000100, 0b00000000, 
  0b00010101, 0b11111100, 0b01000111, 0b10100000, 
  0b11111101, 0b10001100, 0b10010100, 0b11000000, 
  0b10011100, 0b00001100, 0b10010100, 0b11000000, 
  0b00010101, 0b11111110, 0b01000111, 0b10100000, 
  0b00110100, 0b00011011, 0b01000100, 0b00000000, 
  0b01100111, 0b11110001, 0b11111100, 0b00000000, 
};

const uint8_t TRIANGLE_BITMAP_BWIDTH = 1;
const uint8_t TRIANGLE_BITMAP_HEIGHT = 4;
const uint8_t TRIANGLE_UP_BITMAP[] PROGMEM = { // 1x4
  0b00010000,
  0b00110000,
  0b00111000,
  0b01111100,
};
const uint8_t TRIANGLE_DN_BITMAP[] PROGMEM = { // 1x4
  0b01111100,
  0b00111000,
  0b00110000,
  0b00010000,
};


#if RENDER_SHUTTLE_AS_BITMAP
// Made with Marlin Bitmap Converter https://marlinfw.org/tools/u8glib/converter.html
const uint8_t SHUTTLE_BITMAP_BWIDTH = 8;
const uint8_t SHUTTLE_BITMAP_HEIGHT = 50;
const uint8_t SHUTTLE_BITMAP[] PROGMEM = { //8x50
  B00000000,B11000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000011,B11111000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000011,B11111110,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B01111111,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B11111111,B10000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B01111111,B11000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B11111111,B11000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B01111111,B11100000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B11111111,B11110000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B01110111,B11111000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B11101011,B11111100,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B01101011,B11111100,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B11110111,B11111110,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B01111111,B11111111,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B11111111,B11111111,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00111111,B01111111,B11111111,B11000000,B00000000,B00000000,B00000000,B00000000,
  B00111111,B11111111,B11111111,B11111111,B10000000,B00000000,B00000000,B00000000,
  B00000000,B01111111,B11111111,B00111111,B11111111,B11000000,B00000000,B00000000,
  B00111111,B10111111,B11111111,B11111111,B11111111,B11000000,B00000000,B00000000,
  B00111011,B11011111,B11111111,B11111111,B11111111,B11111111,B11000000,B00000000,
  B00111111,B11101100,B00000000,B00000000,B00000000,B00000000,B11000000,B00000000,
  B00111011,B11001100,B00000000,B00000000,B00000000,B00000000,B11111111,B00000000,
  B00111111,B11101100,B00000000,B00000000,B00000000,B00000000,B11110011,B11110000,
  B00000000,B01101100,B00000000,B00000000,B00000000,B00000000,B10111001,B11111000,
  B11111111,B00001100,B00000000,B00000000,B00000000,B00000000,B11111000,B11101100,
  B11111111,B00001100,B00000000,B00000000,B00000000,B00000000,B11111000,B11101100,
  B00000000,B01101100,B00000000,B00000000,B00000000,B00000000,B10111001,B11111000,
  B00111111,B11101100,B00000000,B00000000,B00000000,B00000000,B11110011,B11110000,
  B00111011,B11001100,B00000000,B00000000,B00000000,B00000000,B11111111,B00000000,
  B00111111,B11101100,B00000000,B00000000,B00000000,B00000000,B11000000,B00000000,
  B00111011,B11011111,B11111111,B11111111,B11111111,B11111111,B11000000,B00000000,
  B00111111,B10111111,B11111111,B11111111,B11111111,B11000000,B00000000,B00000000,
  B00000000,B01111111,B11111111,B00111111,B11111111,B11000000,B00000000,B00000000,
  B00111111,B11111111,B11111111,B11111111,B10000000,B00000000,B00000000,B00000000,
  B00111111,B01111111,B11111111,B11000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B11111111,B11111111,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B01101011,B11111111,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00011111,B11110111,B11111110,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B01101011,B11111100,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B11110111,B11111000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B01101011,B11111000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00001111,B11111111,B11100000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B01111111,B11100000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B11111111,B11000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B01111111,B10000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000111,B11111111,B10000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000011,B01111111,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000011,B11111110,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000011,B11111000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
  B00000000,B11000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000
};
#endif // RENDER_SHUTTLE_AS_BITMAP

const uint8_t SEG7_OPEN[] = {
  SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,   // O
  SEG_A | SEG_B | SEG_E | SEG_F | SEG_G,           // p
  SEG_A | SEG_D | SEG_E | SEG_F | SEG_G,           // E
  SEG_C | SEG_E | SEG_G                            // n
};

const uint8_t SEG7_ERR[] = {
  SEG_A | SEG_D | SEG_E | SEG_F | SEG_G,           // E
  SEG_E | SEG_G,                                   // r
  SEG_E | SEG_G,                                   // r
  0
};

// ----[ helper classes ]-------------------------------------------

// ----[ global variables ]------------------------------------
Button2 DipSwitch1, DipSwitch2, DipSwitch3;
Button2 RotaryEncoderButton;
Button2 TemperatureUp, TemperatureDown;
  
// Oled display object
U8GLIB_SH1106_128X64 OledPanel(U8G_I2C_OPT_NONE); // I2C / TWI

// 7 seg x 4 display object
TM1637Display CodeEntry7Seg4 = TM1637Display(PIN_7SEG_DISPLAY_CLK, PIN_7SEG_DISPLAY_DIO);

// Setup a RotaryEncoder with 2 steps per latch for the 2 signal input pins:
RotaryEncoder Encoder(PIN_ROTARY_ENCODER_DT, PIN_ROTARY_ENCODER_CLK, RotaryEncoder::LatchMode::FOUR3);
RotaryEncoder::Direction UserSpinDirection = RotaryEncoder::Direction::CLOCKWISE; // can also be COUNTERCLOCKWISE, initially NOROTATION
  
bool UnlockError = false; // true after user entered incorrect combo and we are animating the error message
bool Locked = true;       // the state of the combination lock system
bool SatelliteInBay = true; // is there a satellite in the shuttle bay
uint8_t UserDecodingDigit = 0;
uint8_t UserEnteredCode[DIGITS_USED_IN_CODE];
uint8_t TheCode[DIGITS_USED_IN_CODE] = { 55, 30, 7, 66 };

int LightLevel = 0;
int LevelB = 0;
float ThermostatDegCLevel = THERMOSTAT_DEFAULT_DEG_C;

unsigned long LastUserInteraction = millis(); // reset each time user pushes button/switch/encoder
bool OledOff = false; // whether the oled is off

// ----[ code ]------------------------------------
void setup() // put your setup code here, to run once:
{
  Serial.begin(SERIAL_BAUD);
  Serial.println(SERIAL_INTRO_MESSAGE);
  
  pinMode(PIN_DIP_1, INPUT_PULLUP);
  pinMode(PIN_DIP_2, INPUT_PULLUP);
  pinMode(PIN_DIP_3, INPUT_PULLUP);
  pinMode(PIN_BUTTON_UP, INPUT_PULLUP);
  pinMode(PIN_BUTTON_DOWN, INPUT_PULLUP);
  pinMode(PIN_LED_RED, OUTPUT);
  pinMode(PIN_LED_BLUE, OUTPUT);
  pinMode(PIN_LED_GREEN, OUTPUT);
  digitalWrite(PIN_LED_RED, Locked);
  digitalWrite(PIN_LED_BLUE, LOW);
  digitalWrite(PIN_LED_GREEN, !Locked);
  pinMode(PIN_SPEAKER, OUTPUT);
  digitalWrite(PIN_SPEAKER, LOW); // caution low ohm load, do not wright HIGH, tone command seems ok to use
  pinMode(PIN_BUILT_IN_LED, OUTPUT);
  digitalWrite(PIN_BUILT_IN_LED, LOW);

  pinMode(PIN_ROTARY_ENCODER_CLK, INPUT);
  pinMode(PIN_ROTARY_ENCODER_DT, INPUT);
  pinMode(PIN_ROTARY_ENCODER_SWITCH, INPUT);

  DipSwitch1.begin(PIN_DIP_1);
  DipSwitch1.setChangedHandler(DipChanged);
  DipSwitch2.begin(PIN_DIP_2);
  DipSwitch2.setChangedHandler(DipChanged);
  DipSwitch3.begin(PIN_DIP_3);
  DipSwitch3.setChangedHandler(DipChanged);
  TemperatureUp.begin(PIN_BUTTON_UP);
  TemperatureUp.setTapHandler(ThermostatChanged); // catch any click
  TemperatureDown.begin(PIN_BUTTON_DOWN);
  TemperatureDown.setTapHandler(ThermostatChanged);

  pinMode(PIN_LIGHT_DEPENDENT_RESISTOR, INPUT);
  DipSwitchesToLevelB();
  
  RotaryEncoderButton.begin(PIN_ROTARY_ENCODER_SWITCH);
  RotaryEncoderButton.setTapHandler(RotaryEncoderButtonClick); // catch any click, long-click, etc

  // Initialize 7 segment x 4 display
  CodeEntry7Seg4.clear();
  CodeEntry7Seg4.setBrightness(7); // 0 lowest, 7 highest

  // Initialize oled display
  OledPanel.begin();
  //OledPanel.setRot180(); // flip screen if required
  RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, BAY_CLOSED);

  Lock();
}


// Put your main code here in loop, to run repeatedly.
void loop()
{
  BlinkBlue();
  ReadButtons();
  ReadSensors();
  PreventOledBurnIn();
  AnimateSpinDirection(UserSpinDirection);
  if(Locked) CheckEncoder();
  
  if(CheckIfUnlocking())
  {
    Unlock();
    UnlockAnimation();
    bool shuttleMoves = DipSwitch1.isPressed() && DipSwitch2.isPressed();
    bool ejectSatellite = SatelliteInBay && DipSwitch1.isPressed() && DipSwitch2.isPressed() && DipSwitch3.isPressed();
    if(shuttleMoves)
    {
      ShuttleMovesAwayWithAnimation(ejectSatellite);
      if(ejectSatellite)
      {
        SatelliteDeploysSolarCells();
        SatelliteInBay = false;
      }
    }
    RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, BAY_FULLY_OPEN);
  }
  else
  {
    IfCodeErrorBeepUser(); // there may or may not be an error but if so beep at user
  }
}



// ----[ function ]------------------------------------
// blink the blue led
// We do not use delay, instead we utilize the millis technique.
// An excellent tutorial of this technique may be found at https://learn.adafruit.com/multi-tasking-the-arduino-part-1
void BlinkBlue()
{
  static unsigned long BlinkerMillis = millis();
  const unsigned long BlinkerMsec = 2000; // every 2 sec
  
  if(millis() - BlinkerMillis < BlinkerMsec) return;
  // function will do nothing and just return until its time to change the LED. 
  // This pattern works even upon millis rollover in approx 50 days because of the unsigned subtraction and comparison.
  // Stick with this technique of (current-previous) compared to desiredWait with all components unsigned long
  // for example the alternate formula "current > (previous+desiredWait)" will fail at rollover
  BlinkerMillis = millis();
  //digitalWrite(PIN_LED_BLUE, ! digitalRead(PIN_LED_BLUE)); // read low/high then invert to high/low. This trick works on uno but not for all micros replace with better code below
  static uint8_t BlueValue = LOW;
  BlueValue = (BlueValue == LOW) ? HIGH : LOW;
  digitalWrite(PIN_LED_BLUE, BlueValue);
  
}

// ----[ function ]------------------------------------
void ReadButtons()
{
  DipSwitch1.loop();
  DipSwitch2.loop();
  DipSwitch3.loop();
  RotaryEncoderButton.loop();
  TemperatureUp.loop();
  TemperatureDown.loop();
}

// ----[ function ]------------------------------------
void ReadSensors()
{
  static unsigned long LightSensorMillis = millis();
  const unsigned long ReadSensorsMsec = 1000;
  
  if(millis() - LightSensorMillis < ReadSensorsMsec) return;
  // function will do nothing and just return until its time to read the light sensor/other sensors
  // This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
  LightSensorMillis = millis();
  
  bool someSensorsChanged = false;
  int lastLightSensor = LightLevel;
  LightLevel = map(analogRead(PIN_LIGHT_DEPENDENT_RESISTOR), 0,MAX_ADC, 0,100);
  if(LightLevel != lastLightSensor) someSensorsChanged = true;

  // read other sensors here
  
  // since we don't have another sensor, use the dip switches to form a binary number
  // but this is called in the dip switch change handler

  // display new values to oled screen
  if(millis() - LastUserInteraction >= RECENT_USER_INTERACTION_MSEC)
  { // it seems that frequent refreshing of oled causes the encoder reading to skip steps a small amount
    // so lets only do that if the user isn't spinning the encoder
    if(someSensorsChanged)
    {
      RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, Locked ? BAY_CLOSED : BAY_FULLY_OPEN);
      #if LOGGING
      Serial.print("LDR=");
      Serial.print(LightLevel);
      Serial.print(" B=");
      Serial.println(LevelB);
      #endif // LOGGING
    }
  }
}

// ----[ function ]------------------------------------
void DipSwitchesToLevelB()
{
  int binaryVal = 0;
  if(DipSwitch1.isPressed()) binaryVal += 1;
  if(DipSwitch2.isPressed()) binaryVal += 2;
  if(DipSwitch3.isPressed()) binaryVal += 4;
  LevelB = map(binaryVal, 0,7, 0, 100);  
}

// ----[ function ]------------------------------------
void CheckEncoder()
{
  static int OldPos = -1;
  Encoder.tick();

  int newPos = Encoder.getPosition();
  RotaryEncoder::Direction newDir = Encoder.getDirection();
  
  if(newPos < 0) { newPos = 99; Encoder.setPosition(newPos); }
  if(newPos > 99) { newPos = 0; Encoder.setPosition(newPos); } 

  if ((UserSpinDirection != newDir) && (newDir != RotaryEncoder::Direction::NOROTATION))
  {
    UserSpinDirection = newDir;
    if(UserDecodingDigit < DIGITS_USED_IN_CODE) // protect from array overrun
      UserEnteredCode[UserDecodingDigit] = OldPos;
    UserDecodingDigit++;
    #if LOGGING
      Serial.print(" new dir:");
      Serial.print((int)newDir);
      Serial.print(" digit:");
      Serial.println(UserDecodingDigit);
    #endif // LOGGING
  }
  if (OldPos != newPos)
  {
    #if LOGGING
      Serial.print(" pos:");
      Serial.print(newPos);
      Serial.print(" dir:");
      Serial.println((int)newDir);
    #endif // LOGGING
        
    CodeEntry7Seg4.showNumberDec(newPos, SEG7_LEADING_ZERO, 2, 2);
    SpeakerClick();
    ResetUserInteractionTimer();
    
    OldPos = newPos;
  } // if
  
}


// ----[ function ]------------------------------------
void RotaryEncoderButtonClick(Button2& btn)
{
  ResetUserInteractionTimer();
  if(Locked) 
    ResetLock();
  else
    Lock();
}

// ----[ function ]------------------------------------
void DipChanged(Button2& btn)
{
  ResetUserInteractionTimer();
  if(btn == DipSwitch1)
  {
    Serial.print("DipSwitch1=");
    Serial.println(btn.isPressed());
  }
  else if(btn == DipSwitch2)
  {
    Serial.print("DipSwitch2=");
    Serial.println(btn.isPressed());
  }
  else if(btn == DipSwitch3)
  {
    Serial.print("DipSwitch3=");
    Serial.println(btn.isPressed());
  }
  DipSwitchesToLevelB();
  RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, Locked ? BAY_CLOSED : BAY_FULLY_OPEN);
}

// ----[ function ]------------------------------------
void ThermostatChanged(Button2& btn)
{
  ResetUserInteractionTimer();
  if(btn == TemperatureUp)
  {
    ThermostatDegCLevel += 0.5;
  }
  else if(btn == TemperatureDown)
  {
    ThermostatDegCLevel -= 0.5;
  }
  ThermostatDegCLevel = constrain(ThermostatDegCLevel, THERMOSTAT_MIN_DEG_C, THERMOSTAT_MAX_DEG_C);
  Serial.print(" Thermostat: ");
  Serial.print(ThermostatDegCLevel, 1); // print float with 1 digit of precision
  Serial.println(" degC");
  SpeakerClick();
  RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, Locked ? BAY_CLOSED : BAY_FULLY_OPEN);
}

// ----[ function ]------------------------------------
inline void SpeakerClick()
{
  tone(PIN_SPEAKER,1000,2);
}

// ----[ function ]------------------------------------
// make the top o of the 8 digit bars appear to rotate in a given direction
void AnimateSpinDirection(RotaryEncoder::Direction dir)
{
  static unsigned long SpinMillis = millis();
  const unsigned long SpinMsec = 100;
  static int8_t SpinState = 0;
  const uint8_t SpinAnimLookup[5] = { SEG_A, SEG_B, SEG_G, SEG_F, 0 };
  const uint8_t LENGTH_1 = 1;
  const uint8_t LEFT_POSITION = 0;

  if(!Locked) return;
  
  if(millis() - SpinMillis < SpinMsec) return;
  // function will do nothing and just return until its time to change the LED. 
  // This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
  SpinMillis = millis();

  if(dir == RotaryEncoder::Direction::NOROTATION)
  {
    SpinState = 4; // off
  }
  else if(dir == RotaryEncoder::Direction::CLOCKWISE)
  {
    if(++SpinState > 3) SpinState = 0;
  }
  else // RotaryEncoder::Direction::COUNTERCLOCKWISE
  {
    if(--SpinState < 0) SpinState = 3;
  }
  CodeEntry7Seg4.setSegments(&SpinAnimLookup[SpinState], LENGTH_1, LEFT_POSITION);
}


// ----[ function ]------------------------------------
// if we have all the digits then verify they are correct
bool CheckIfUnlocking()
{
  if(!Locked) return false; // already unlocked, nothing to do
  if(UserDecodingDigit != DIGITS_USED_IN_CODE) return false;
  bool correctCode = true;
  bool digitGood = false;
  for(uint8_t i = 0; i < DIGITS_USED_IN_CODE; i++)
  {
    if(UserEnteredCode[i] != TheCode[i]) correctCode = false;
    // Try to keep timing same for either path whether all digits are wrong or just a few.
    // Hackers use timing differences to break codes.
    // I don't claim that this implementation is by any means secure, just thought it would be fun to try a little.
    // Thus this code is more complicated than it needs to be to just return false as soon as an incorrect digit was encountered
    if(UserEnteredCode[i] == TheCode[i]) digitGood = true;
  }
  UnlockError = !correctCode;
  return digitGood && correctCode;
}

// ----[ function ]------------------------------------
// lock the system and if it was unlocked then show the animation
void Lock()
{
  Serial.println("Lock()");
  if(! Locked) LockAnimation();
  Locked = true;
  UserSpinDirection = RotaryEncoder::Direction::CLOCKWISE;
  UserDecodingDigit = 0;
  for(uint8_t i = 0; i < DIGITS_USED_IN_CODE; i++)
  {
    UserEnteredCode[i] = 0;
  }
  digitalWrite(PIN_LED_RED, Locked);
  digitalWrite(PIN_LED_GREEN, !Locked);
  CodeEntry7Seg4.clear();
  Encoder.setPosition(0);
  RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, BAY_CLOSED);
}

// ----[ function ]------------------------------------
// start combo over
void ResetLock()
{
  Serial.println("ResetLock()");
  tone(PIN_SPEAKER, 1000, 500);
  Lock();
}

// ----[ function ]------------------------------------
// close the shuttle bay doors with some tones
void LockAnimation()
{
  Serial.println("LockAnimation");
  const int TONE_LENGTH = 100;
  const unsigned long LOCK_ANIM_SPEED = 250;
  static unsigned long LockMsgMillis;
  uint8_t lockState = BAY_FULLY_OPEN;
  do {
    if(millis() - LockMsgMillis >= LOCK_ANIM_SPEED)
    {
      LockMsgMillis = millis();
      tone(PIN_SPEAKER, 200*lockState, TONE_LENGTH);
      lockState--;
      RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, lockState);
    }
    BlinkBlue();
  } while(lockState > BAY_CLOSED);
  noTone(PIN_SPEAKER);
}

// ----[ function ]------------------------------------
void Unlock()
{
  Serial.println("Unlock()");
  UserSpinDirection = RotaryEncoder::Direction::NOROTATION;
  CodeEntry7Seg4.setSegments(SEG7_OPEN);  
  Locked = false;
  digitalWrite(PIN_LED_RED, Locked);
  digitalWrite(PIN_LED_GREEN, !Locked);
}

// ----[ function ]------------------------------------
// Unlock the system
// Take control for a few seconds to beep at the user for wrong code, 
// Ignore all buttons but continue to blink blue led in background
void UnlockAnimation()
{
  const unsigned long UNLOCK_ANIM_SPEED = 250;
  unsigned long unlockMsgMillis = millis();
  const int TONE_LENGTH = 200;

  Serial.println("UnlockAnimation()");
  int unlockTones[BAY_FULLY_OPEN] = { 0, 200, 0, 400, 0, 800, 0, 1400, 1400 };

  uint8_t unlockState = BAY_CLOSED;
  do {
    if(millis() - unlockMsgMillis >= UNLOCK_ANIM_SPEED)
    {
      unlockMsgMillis = millis();
      if(unlockTones[unlockState] > 0)
        tone(PIN_SPEAKER, unlockTones[unlockState], TONE_LENGTH);
      unlockState++;
      RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, unlockState);
    }
    BlinkBlue();
  } while(unlockState < BAY_FULLY_OPEN);
  noTone(PIN_SPEAKER);
  
  RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, BAY_FULLY_OPEN);
}

// ----[ function ]------------------------------------
// Move the shuttle off the screen, optionally carrying the satellite or leaving it behind
// Ignore all buttons but continue to blink blue led in background
void ShuttleMovesAwayWithAnimation(bool leaveSatellite)
{
  Serial.println("ShuttleMovesAwayWithAnimation()");
  #define SHUTTLE_MOVE_SPEED 5 // the animation refresh rate is pretty low so don't need much time
  unsigned long shuttleMoveMillis = millis();
  uint8_t shuttleMove = 0;
  do {
    if(millis() - shuttleMoveMillis >= SHUTTLE_MOVE_SPEED)
    {
      shuttleMoveMillis = millis();
      shuttleMove += 2;
      uint8_t satelliteX = leaveSatellite ? SATELLITE_OFFSET_X : (SATELLITE_OFFSET_X + shuttleMove);
      RefreshOledShuttleAndSatelliteOnly(SHUTTLE_LOC_X+shuttleMove, SHUTTLE_LOC_Y, BAY_FULLY_OPEN, satelliteX, SATELLITE_OFFSET_Y, SOLAR_PANEL_CLOSED);
    }
    BlinkBlue();
  } while(shuttleMove <= OFF_SCREEN_RIGHT);
}

// ----[ function ]------------------------------------
// If the satellite has been left behind, deploy it's solar cells
// Ignore all buttons but continue to blink blue led in background
void SatelliteDeploysSolarCells()
{
  Serial.println("SatelliteDeploysSolarCells()");
  #define DEPLOY_SOLAR_CELL_SPEED 500
  unsigned long solarDeployMillis = millis();
  uint8_t solarPanelState = SOLAR_PANEL_CLOSED;
  do {
    if(millis() - solarDeployMillis >= DEPLOY_SOLAR_CELL_SPEED)
    {
      solarDeployMillis = millis();
      solarPanelState++;
      tone(PIN_SPEAKER, 800, 15);
      RefreshOledShuttleAndSatelliteOnly(SHUTTLE_LOC_X+OFF_SCREEN_RIGHT, SHUTTLE_LOC_Y, BAY_FULLY_OPEN, SATELLITE_OFFSET_X, SATELLITE_OFFSET_Y, solarPanelState);
    }
    BlinkBlue();
  } while(solarPanelState < SOLAR_PANEL_FULLY_OPEN);

  Serial.println("successful deployment sound effects");
  uint8_t soundEffectsState = 1;
  do {
    if(millis() - solarDeployMillis >= DEPLOY_SOLAR_CELL_SPEED)
    {
      solarDeployMillis = millis();
      switch(soundEffectsState)
      {
        case 1:
          tone(PIN_SPEAKER, 300, 80);
          break;
        case 2:
          tone(PIN_SPEAKER, 400, 80);
          break;
        case 3:
          tone(PIN_SPEAKER, 900, 320);
          break;
        default:
          noTone(PIN_SPEAKER);
      }
      soundEffectsState++;
    }
    BlinkBlue();
  } while(soundEffectsState < 6);
}

// ----[ function ]------------------------------------
// If wrong code then take control for a few seconds to beep at the user for wrong code, 
// Ignore all buttons but continue to blink blue led in background
void IfCodeErrorBeepUser()
{
  static uint8_t ErrorState;
  static unsigned long ErrorMsgMillis;
  if(!UnlockError) return; // nothing to do

  Serial.println("IfCodeErrorBeepUser()");
  ErrorState = 1;
  while(ErrorState)
  {
    switch (ErrorState)
    {
      case 1: case 3: case 5:
        if(millis() - ErrorMsgMillis >= 700)
        {
          ErrorMsgMillis = millis();
          CodeEntry7Seg4.setSegments(SEG7_ERR);
          tone(PIN_SPEAKER,100);
          ErrorState++;
        }
        break;
      case 2: case 4: case 6:
        if(millis() - ErrorMsgMillis >= 300)
        {
          ErrorMsgMillis = millis();
          CodeEntry7Seg4.clear();
          noTone(PIN_SPEAKER);
          ErrorState++;
        }
        break;
      case 7: default:
        UnlockError = false;
        noTone(PIN_SPEAKER);
        Lock();
        ErrorState = 0;
        break;
    }
    BlinkBlue();
  }
}


// ----[ function ]------------------------------------
void ResetUserInteractionTimer()
{
  LastUserInteraction = millis();
  if(OledOff)
  {
    RefreshOled(SHUTTLE_LOC_X, SHUTTLE_LOC_Y, Locked ? BAY_CLOSED : BAY_FULLY_OPEN);
  }
}

// ----[ function ]------------------------------------
// oled pixels that are left on for 1000 of hours will get burn-in and be dimmer than the other pixels
// we disable the screen if the user has not interacted in some time
void PreventOledBurnIn()
{
  if(OledOff) return; // it's already off
  if(millis() - LastUserInteraction >= PREVENT_OLED_BURNIN_MSEC)
  { // the oled has been on a long time without user interaction switch it off until the user interacts again
    Serial.println("Disabling the OLED to prevent permanent burn-in");
    OledPanel.setColorIndex(0);
    OledPanel.drawBox(0,0, OLED_WIDTH,OLED_HEIGHT);
    OledPanel.sleepOn();
    OledOff = true;
  }
}

// ----[ function ]------------------------------------
void ReEnableOled()
{
  Serial.print("Enabling the OLED");
  OledPanel.sleepOff();
  OledOff = false;  
}

// ----[ function ]------------------------------------
// like map command but maps a float from one range to another
float mapfloat(float x, float in_min, float in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

// ----[ function ]------------------------------------
// redisplay the data to the oled screen
void RefreshOled(uint8_t shuttleX, uint8_t shuttleY, uint8_t shuttleBay)
{
  if(millis() - LastUserInteraction >= PREVENT_OLED_BURNIN_MSEC) return;
  
  if(OledOff) ReEnableOled();
  
  OledPanel.firstPage();
  do  // more info on why we need this loop here: https://github.com/olikraus/u8glib/wiki/tpictureloop
  {
    DrawNewScreen();
    DrawSwitches();
    DrawShuttle(shuttleX, shuttleY);
    if(SatelliteInBay) 
      DrawSatellite(SATELLITE_OFFSET_X, SATELLITE_OFFSET_Y, SOLAR_PANEL_CLOSED);
    DrawShuttleBayDoors(shuttleX, shuttleY, shuttleBay);
    int thermostatToGuage = (int)mapfloat(ThermostatDegCLevel, THERMOSTAT_MIN_DEG_C,THERMOSTAT_MAX_DEG_C, 0,100); // convert our min..max range to the 0-100 range
    thermostatToGuage = constrain(thermostatToGuage, 0,100);
    DrawGagues(LightLevel, LevelB, thermostatToGuage);
  } while( OledPanel.nextPage());
}

// ----[ function ]------------------------------------
void RefreshOledShuttleAndSatelliteOnly(uint8_t shuttleX, uint8_t shuttleY, uint8_t shuttleBay, uint8_t satelliteX, uint8_t satelliteY, uint8_t solarPanelDeploy)
{
  OledPanel.firstPage();
  do
  {
    DrawShuttle(shuttleX, shuttleY);
    if(SatelliteInBay)
      DrawSatellite(satelliteX, satelliteY, solarPanelDeploy);
    DrawShuttleBayDoors(shuttleX, shuttleY, shuttleBay);
  } while( OledPanel.nextPage());
}

// ----[ function ]------------------------------------
// Setup the screen area and heading text
void DrawNewScreen()
{
  OledPanel.setColorIndex(1); // pixel on
  OledPanel.setFont(u8g_font_6x10);
  OledPanel.setFontRefHeightExtendedText();
  OledPanel.setDefaultForegroundColor();
  OledPanel.setFontPosTop();
  OledPanel.drawStr( 0, 0, INTRO_MESSAGE);
  OledPanel.drawBitmapP(OLED_MAX_X-8, 0, SMILE_BITMAP_BWIDTH, SMILE_BITMAP_HEIGHT, SMILE_BITMAP); // upper right corner
  OledPanel.drawFrame(0,9,OLED_WIDTH,OLED_HEIGHT-9);
}

void DrawSwitches()
{
  OledPanel.setColorIndex(1);
  OledPanel.drawFrame(SWITCH_LOC_X,SWITCH_LOC_Y, SWITCH_PANEL_WIDTH, SWITCH_PANEL_HEIGHT);
  if(DipSwitch1.isPressed())
    OledPanel.drawBitmapP(SWITCH_LOC_X+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_ON_BITMAP);
  else
    OledPanel.drawBitmapP(SWITCH_LOC_X+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_OFF_BITMAP);
  if(DipSwitch2.isPressed())
    OledPanel.drawBitmapP(SWITCH_LOC_X+18+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_ON_BITMAP);
  else
    OledPanel.drawBitmapP(SWITCH_LOC_X+18+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_OFF_BITMAP);
  if(DipSwitch3.isPressed())
    OledPanel.drawBitmapP(SWITCH_LOC_X+18+18+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_ON_BITMAP);
  else
    OledPanel.drawBitmapP(SWITCH_LOC_X+18+18+2, SWITCH_LOC_Y+2, SWITCH_BITMAP_BWIDTH, SWITCH_BITMAP_HEIGHT, SWITCH_OFF_BITMAP);
}

// ----[ function ]------------------------------------
// this generates a 61x51 pixel shuttle. doorState can be 0-8
void DrawShuttle(uint8_t x, uint8_t y)
{
  if(x > OFF_SCREEN_RIGHT || y > OFF_SCREEN_BOTTOM) return;
// note- I origionally drew up the shuttle in paintbrush and zoomed in and converted it to the 
// algorithmic version in about an hour or so by hand from counting pixels. It seemed like it was 
// slowing things down after I added all the other screen elements so I decided 
// to convert it into a bitmap. I was worried about that taking up a lot of program memory
// It turns out that the bitmap takes up 400 bytes of program mem but it is 1062 less bytes than
// all these function calls and significantly faster to render
#if RENDER_SHUTTLE_AS_BITMAP
  OledPanel.drawBitmapP(x, y, SHUTTLE_BITMAP_BWIDTH, SHUTTLE_BITMAP_HEIGHT, SHUTTLE_BITMAP);
#else  // algorithmic render of shuttle using primitives
  OledPanel.setColorIndex(1);
  // body of ship with some boxes
  OledPanel.drawBox(x+2,y+23, 59,4);
  OledPanel.drawBox(x+2,y+22, 58,6);
  OledPanel.drawBox(x+2,y+21, 54,8);
  OledPanel.drawBox(x+2,y+19, 48,12);
  OledPanel.drawBox(x+2,y+17, 40,16);
  OledPanel.drawBox(x+2,y+16, 31,18);
  OledPanel.drawBox(x+2,y+14, 22,21);
  OledPanel.drawBox(x+0,y+24, 7,2);
  // wings of ship with some triangles
  OledPanel.drawTriangle(x+2,y+14, x+7,y+0, x+15,y+2);   // upper wing of ship
  OledPanel.drawTriangle(x+2,y+14, x+15,y+2, x+27,y+17);
  OledPanel.drawTriangle(x+2,y+35, x+15,y+47, x+7,y+49);   // lower wing of ship
  OledPanel.drawTriangle(x+2,y+35, x+27,y+32, x+15,y+47);
  
  // detailing black lines over the white
  OledPanel.setColorIndex(0);
   // engines
  OledPanel.drawLine(x+0,y+17, x+8,y+17);
  OledPanel.drawLine(x+0,y+32, x+8,y+32);
  OledPanel.drawLine(x+8,y+17, x+11,y+20);
  OledPanel.drawLine(x+8,y+32, x+11,y+29);
  OledPanel.drawLine(x+11,y+20, x+11,y+29);
   // tail fin
  OledPanel.drawLine(x+0,y+23, x+8,y+23);
  OledPanel.drawLine(x+0,y+26, x+8,y+26);
  OledPanel.drawLine(x+9,y+24, x+11,y+24);
  OledPanel.drawLine(x+9,y+25, x+11,y+25);
   // cabin
  OledPanel.drawLine(x+49,y+23, x+50,y+23);
  OledPanel.drawLine(x+49,y+26, x+50,y+26);
  OledPanel.drawPixel(x+52,y+22);
  OledPanel.drawPixel(x+52,y+27);
  OledPanel.drawLine(x+53,y+22, x+53,y+27);
  OledPanel.drawLine(x+54,y+23, x+54,y+26);
  OledPanel.drawLine(x+55,y+24, x+55,y+25);
  OledPanel.drawLine(x+59,y+24, x+59,y+25);

  // bay
  OledPanel.drawBox(x+14,y+20, 34,10);
  OledPanel.setColorIndex(1);
#endif // RENDER_SHUTTLE_AS_BITMAP

}

// ----[ function ]------------------------------------
void DrawShuttleBayDoors(uint8_t x, uint8_t y, uint8_t doorState)
{
  if(x > OFF_SCREEN_RIGHT || y > OFF_SCREEN_BOTTOM) return;
  switch(doorState)
  {
    case BAY_CLOSED:
      //OledPanel.drawHLine(x+15,y+24, 32);
      OledPanel.drawBox(x+15,y+21, 32,8);
      break;
    case 1:
      OledPanel.drawHLine(x+15,y+25, 32);
      [[fallthrough]]; // don't warn for not having a break statement, we want the next case code to be executed
    case 2:
      OledPanel.drawHLine(x+15,y+23, 32);
      [[fallthrough]];
    case 3:
      OledPanel.drawHLine(x+15,y+26, 32);
      [[fallthrough]];
    case 4:
      OledPanel.drawHLine(x+15,y+22, 32);
      [[fallthrough]];
    case 5:
      OledPanel.drawHLine(x+15,y+27, 32);
      [[fallthrough]];
    case 6:
      OledPanel.drawHLine(x+15,y+21, 32);
      [[fallthrough]];
    case 7:
      OledPanel.drawHLine(x+15,y+28, 32); // repeated similar code is often an indication that it should be refactored, 
                                          // in this case probably a while loop and lookup array could reduce complexity and code space
      [[fallthrough]];
    case 8: case BAY_FULLY_OPEN:
      [[fallthrough]];
    default:
      break;
  }
}

// ----[ function ]------------------------------------
void DrawSatellite(uint8_t x, uint8_t y, uint8_t solarPanelState)
{
  if(x > OFF_SCREEN_RIGHT || y > OFF_SCREEN_BOTTOM) return;
  if(Locked) return; // bay is closed so satellite would be obscured by doors, save the processing
  
  OledPanel.drawBitmapP(x, y, SATELLITE_BITMAP_BWIDTH, SATELLITE_BITMAP_HEIGHT, SATELLITE_BITMAP);
  OledPanel.setColorIndex(1);
  switch(solarPanelState)
  {
    default:
    case SOLAR_PANEL_FULLY_OPEN: //11
      OledPanel.drawStr( 0, 0, "Satellite deployed");
      [[fallthrough]];
    case 8: case 9: case 10:
      OledPanel.drawHLine(x+9, y-8, 7);
      OledPanel.drawHLine(x+9, y+15, 7);
      [[fallthrough]];
    case 5: case 6: case 7:
      OledPanel.drawHLine(x+9, y-5, 7);
      OledPanel.drawHLine(x+9, y+12, 7);
      [[fallthrough]];
    case 2: case 3: case 4:
      // solar panel lines innermost
      OledPanel.drawHLine(x+9, y-2, 7);
      OledPanel.drawHLine(x+9, y+9, 7);
      [[fallthrough]];
    case 1:
      // solar panel connectors
      OledPanel.drawPixel(x+10, y-1);  OledPanel.drawPixel(x+15, y-1);
      OledPanel.drawPixel(x+10, y+8);  OledPanel.drawPixel(x+15, y+8);
      break;
    case SOLAR_PANEL_CLOSED: // 0
      break;
  }
  if(solarPanelState > 2 && solarPanelState <= SOLAR_PANEL_FULLY_OPEN)
  {
      // solar panel lines outermost
      OledPanel.drawHLine(x+9, y-solarPanelState, 7);
      OledPanel.drawHLine(x+9, y+7+solarPanelState, 7);
      // solar panel lines verticle
      OledPanel.drawLine(x+ 9,y-2, x+ 9,y-solarPanelState);
      OledPanel.drawLine(x+11,y-2, x+11,y-solarPanelState);
      OledPanel.drawLine(x+13,y-2, x+13,y-solarPanelState);
      OledPanel.drawLine(x+15,y-2, x+15,y-solarPanelState);
      OledPanel.drawLine(x+ 9,y+9, x+ 9,y+7+solarPanelState);
      OledPanel.drawLine(x+11,y+9, x+11,y+7+solarPanelState);
      OledPanel.drawLine(x+13,y+9, x+13,y+7+solarPanelState);
      OledPanel.drawLine(x+15,y+9, x+15,y+7+solarPanelState);    
  }
}

// ----[ function ]------------------------------------
// show some gauges on the oled
void DrawGagues(int levelA, int levelB, int levelC) // levelA 0-100, levelB 0-100, levelC 0-100
{
   const uint8_t LINEAR_INDICATOR_WIDTH = 30;
   const uint8_t GAGUE_TICK = 5;
   const uint8_t THERMOM_X = 54;
   const uint8_t THERMOM_BASE_Y = 13;
   
   // set up our linear indicator gague, a rectangle with tick bars inside and a upper and lower triangle pointers
   OledPanel.setColorIndex(1);
   OledPanel.drawFrame(GAGUE_LOC_X,GAGUE_LOC_Y, GAGUE_WIDTH,GAGUE_HEIGHT); // Gague panel
   OledPanel.drawFrame(GAGUE_LOC_X,GAGUE_LOC_Y, LINEAR_INDICATOR_WIDTH,GAGUE_HEIGHT); // ticked linear gauge meter
   for(uint8_t i=GAGUE_TICK; i<LINEAR_INDICATOR_WIDTH; i+=GAGUE_TICK)
   {
      OledPanel.drawVLine(GAGUE_LOC_X+i, GAGUE_LOC_Y+8, 4);
   }
   // upper triangle
   uint8_t scaledLevelA = map(levelA, 0,100, 4,LINEAR_INDICATOR_WIDTH-4);
   //bitmap should be more efficient// OledPanel.drawTriangle(GAGUE_LOC_X+scaledLevelA-3,GAGUE_LOC_Y+2,  GAGUE_LOC_X+scaledLevelA+3,GAGUE_LOC_Y+2,  GAGUE_LOC_X+scaledLevelA, GAGUE_LOC_Y+6);
   OledPanel.drawBitmapP(GAGUE_LOC_X+scaledLevelA-3, GAGUE_LOC_Y+2, TRIANGLE_BITMAP_BWIDTH, TRIANGLE_BITMAP_HEIGHT, TRIANGLE_DN_BITMAP);
   // lower triangle
   uint8_t scaledLevelB = map(levelB, 0,100, 4,LINEAR_INDICATOR_WIDTH-4);
   //bitmap should be more efficient// OledPanel.drawTriangle(GAGUE_LOC_X+scaledLevelB-3,GAGUE_LOC_Y+19,  GAGUE_LOC_X+scaledLevelB+3,GAGUE_LOC_Y+19,  GAGUE_LOC_X+scaledLevelB,GAGUE_LOC_Y+13);
   OledPanel.drawBitmapP(GAGUE_LOC_X+scaledLevelB-3, GAGUE_LOC_Y+14, TRIANGLE_BITMAP_BWIDTH, TRIANGLE_BITMAP_HEIGHT, TRIANGLE_UP_BITMAP);
   

   //Draw a crosshair. Realistically there might instead be an Attitude Directional Indicator (ADI), and Horizontal Situation Indicator (HSI), http://gt500mustangs.com/spaceshuttleguide/F8panel.htm 
   OledPanel.drawBitmapP(GAGUE_LOC_X+32, GAGUE_LOC_Y+3, CROSSHAIR_BITMAP_BWIDTH, CROSSHAIR_BITMAP_HEIGHT, CROSSHAIR_BITMAP);

   //Draw Thermometer
   uint8_t scaledLevelC = map(levelC, 0,100, 0,9);
   OledPanel.drawBitmapP(GAGUE_LOC_X+THERMOM_X, GAGUE_LOC_Y+2, THERMOMETER_BITMAP_BWIDTH, THERMOMETER_BITMAP_HEIGHT, THERMOMETER_BITMAP);
   for(uint8_t t=0; t<=scaledLevelC; t++)
   {
      OledPanel.drawHLine(GAGUE_LOC_X+THERMOM_X+2, GAGUE_LOC_Y+THERMOM_BASE_Y-t, 2);
   }
}
4-Digit Display