/***********************************
Project:     OpAmp Tester
File:        Calibration procedures
             (for DEBUG mode only)
Version:     3.0 - May 2026
Written by:  diy-electronic.ru
************************************
Version history
  1.0  Mar 2025  Initial version
  2.1  Dec 2025  Voltmeter & Ammeter
                 calibration added
  3.0  May 2026  Minor fixes and
                 improvements
************************************/

//--------------------------------------------// 
// Data storage for messages to be displayed  //
// on each of the calibration steps           //
//--------------------------------------------// 
typedef struct {
  uint16_t posY;     // Message vertical position
  String message;    // Message text
} calibrationStep;

const calibrationStep calibrationSteps[] = {
  {36,  "Step 1. Connect voltmeter to V+.\nUsing -1 and +1 buttons set\n+1.00V then press OK"}, // PPS steps
  {92,  "Step 2. Using -1 and +1 buttons\nset +12.00V then press OK"},
  {133, "Step 3. Connect voltmeter to V-.\nUsing -1 and +1 buttons set\n-1.00V then press OK"},
  {191, "Step 4. Using -1 and +1 buttons\nset -12.00V then press OK"},
  {30,  "Step 1. Set jumper S1 to position 5-6 then press OK."},                                // Voltmeter steps
  {66,  "Step 2. Set jumper S1 to position 3-4. Connect voltmeter to TP2 then press OK."},
  {120, "Step 3. Using -1 and +1 buttons set voltage then press OK."},
  {212, "Step 4. Set jumper S1 to position 1-2 then press OK."},
  {36,  "Step 1. Remove OpAmp under test from the socket then press OK."},                      // Ammeter steps 
  {88,  "Step 2. Set jumper S2 to position 3-4 then press OK."},
  {140, "Step 3. Set jumper S2 to position 1-2 then press OK."}
};

//------------ Global functions ---------------

/****************************************/
/* PPS calibration routine. Includes    */
/* manual PPS calibration using         */
/* external voltmeter as an ethalon.    */
/* Returns: true - calibration success  */ 
/*          false - calibration failure */
/****************************************/
bool calibrationPPS() {
#ifdef __DEBUG__  
  Serial.println("\n======= PPS calibration =======");
  Serial.println(calibrationSteps[0].message);
#endif
  showCalibrationStep(0);
  uint16_t Vplus1 = adjustPPSVoltage(channelPlus, 1.0);
#ifdef __DEBUG__  
  Serial.println(calibrationSteps[1].message);
#endif
  showCalibrationStep(1);
  uint16_t Vplus12 = adjustPPSVoltage(channelPlus, 12.0);
#ifdef __DEBUG__  
  Serial.println(calibrationSteps[2].message);
#endif
  showCalibrationStep(2);
  uint16_t Vminus1 = adjustPPSVoltage(channelMinus, 1.0);
#ifdef __DEBUG__  
  Serial.println(calibrationSteps[3].message);
#endif
  showCalibrationStep(3);
  uint16_t Vminus12 = adjustPPSVoltage(channelMinus, 12.0);
  
  // Calculate components of ppsVoltageToCode formula and 
  // store them in the ActiveSettings structure
  ActiveSettings.ppsPlusZero = Vplus1 * 1.0909091 - Vplus12 * 0.0909091;
  ActiveSettings.ppsPlusGain = (Vplus12 - Vplus1) * 0.0909091;
  ActiveSettings.ppsMinusZero = Vminus1 * 1.0909091 - Vminus12 * 0.0909091;
  ActiveSettings.ppsMinusGain = (Vminus12 - Vminus1) * 0.0909091;
#ifdef __DEBUG__
  serialPrintIntParam("Vplus1",   Vplus1,   false);
  serialPrintIntParam("Vplus12",  Vplus12,  false);
  serialPrintIntParam("Vminus1",  Vminus1,  false);
  serialPrintIntParam("Vminus12", Vminus12, true);
  serialPrintFloatParam("ppsPlusZero",  ActiveSettings.ppsPlusZero,  false);
  serialPrintFloatParam("ppsPlusGain",  ActiveSettings.ppsPlusGain,  false);
  serialPrintFloatParam("ppsMinusZero", ActiveSettings.ppsMinusZero, false);
  serialPrintFloatParam("ppsMinusGain", ActiveSettings.ppsMinusGain, true);
#endif

  // Calibration succeeded if both ppsPlusZero and ppsMinusZero are positive and less than 100. 
  // If at least one of them is negative or has a value more than 100 then return false.
  return ((ActiveSettings.ppsPlusZero > 0) && (ActiveSettings.ppsMinusZero > 0) && (ActiveSettings.ppsPlusZero <= 100) && (ActiveSettings.ppsMinusZero <= 100));
}

/*******************************************/
/* Voltmeter calibration routine. Includes */
/* manual calibration procedures with      */
/* external voltmeter as an ethalon.       */ 
/*******************************************/
void calibrationVoltmeter() {
#ifdef __DEBUG__  
  Serial.println("\n======= Voltmeter calibration =======");
  Serial.println(calibrationSteps[4].message);
#endif
  showCalibrationStep(4);        // "Set jumper to position 5-6..." message
  waitButtonClick(rightButton);  // Wait for OK button click

//---------- Set zero for low resolution mode ----------
  //  Fullscale = +/-15V
  uint16_t FullScaleLow = 30.0 * ActiveSettings.voltageLowGain + ActiveSettings.voltageLowZero;
   // Read ADC Channel 0 with gain = 2.
  ActiveSettings.voltageLowZero = (float) (getAdcCodeDiff(GAIN_TWO, 40, FullScaleLow));
#ifdef __DEBUG__  
  serialPrintFloatParam("ActiveSettings.voltageLowZero", ActiveSettings.voltageLowZero, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
#endif

//---------- Set zero for high resolution mode ----------
  //  Fullscale = +/-2.25V
  uint16_t FullScaleHigh = 4.5 * ActiveSettings.voltageHighGain + ActiveSettings.voltageHighZero;
  // Read ADC Channel 0 with gain = 16
  ActiveSettings.voltageHighZero = (float) (getAdcCodeDiff(GAIN_SIXTEEN, 40, FullScaleHigh));
#ifdef __DEBUG__  
  serialPrintFloatParam("ActiveSettings.voltageHighZero", ActiveSettings.voltageHighZero, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
  Serial.println(calibrationSteps[5].message);
#endif

//---------- Set gain for low resolution mode ----------
  showCalibrationStep(5);                 // "Set jumper to position 3-4..." message
  waitButtonClick(rightButton);           // Wait for OK button click
  
  ppsSetVoltage(channelPlus, 12, false);  // Set test voltage to 12 V
  ppsSetVoltage(channelMinus, 12, false); // Set voltage in V- channel to turn on LED
  delay(250);
#ifdef __DEBUG__  
  Serial.println(calibrationSteps[6].message);
#endif
  showCalibrationStep(6);        // "Using -1 and +1 buttons set voltage..." message
  float volts = adjustVoltmeter(12.0);
  ActiveSettings.voltageLowGain = (getAdcCodeDiff(GAIN_TWO, 40, FullScaleLow) - ActiveSettings.voltageLowZero) / volts;
#ifdef __DEBUG__  
  serialPrintFloatParam("ActiveSettings.voltageLowGain", ActiveSettings.voltageLowGain, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
#endif

//---------- Set gain for high resolution mode ----------
  ppsSetVoltage(channelPlus, 1.85, false); // Set test voltage to 1.85 V
  delay(250);
#ifdef __DEBUG__  
  Serial.println(calibrationSteps[6].message);
#endif
  volts = adjustVoltmeter(1.85);
  ActiveSettings.voltageHighGain = (getAdcCodeDiff(GAIN_SIXTEEN, 40, FullScaleHigh) - ActiveSettings.voltageHighZero) / volts;
#ifdef __DEBUG__  
  serialPrintFloatParam("ActiveSettings.voltageHighGain", ActiveSettings.voltageHighGain, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
  Serial.println("--> Step 4. Set jumper S2 to position 1-2 then press OK.");
#endif
  showCalibrationStep(7);         // "Set jumper to position 1-2..." message
  waitButtonClick(rightButton);   // Wait for OK button click
}

/*****************************************/
/* Ammeter calibration routine. Includes */
/* manual calibration procedures using   */
/* already calibrated PPS as an ethalon. */
/* Complete PPS calibration first before */ 
/* using this procedure.                 */
/*****************************************/
void calibrationAmmeter() {
#ifdef __DEBUG__  
  Serial.println("\n======= Ammeter calibration =======");
  Serial.println(calibrationSteps[8].message);
#endif
  showCalibrationStep(8);           // "Remove OpAmp under test..." message
  waitButtonClick(rightButton);     // Wait for OK button click
  ppsSetVoltage(channelPlus, 2.5, false); // Turn on PPS, set +/-2.5 V output voltage
  ppsSetVoltage(channelMinus, 2.5, false);
  delay(500);
  // Full scale is 10mA
  uint16_t FullScaleLow = (uint16_t)(10 * ActiveSettings.currentLowGain + ActiveSettings.currentLowZero);
  // Read ADC channel 2 with 20 iterations;
  ActiveSettings.currentLowZero = (float) getAdcCodeSingleEnded(2, 20, FullScaleLow);
#ifdef __DEBUG__
  serialPrintFloatParam("ActiveSettings.currentLowZero", ActiveSettings.currentLowZero, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
#endif
  ppsSetVoltage(channelPlus, 12.0, false); // Set +/-12 V output voltage
  ppsSetVoltage(channelMinus, 12.0, false);
  delay(500);
  uint16_t FullScaleStd = (uint16_t)(10 * ActiveSettings.currentStdGain + ActiveSettings.currentStdZero);
  ActiveSettings.currentStdZero = (float) getAdcCodeSingleEnded(2, 20, FullScaleStd); // Read ADC channel 2 once more
#ifdef __DEBUG__
  serialPrintFloatParam("ActiveSettings.currentStdZero", ActiveSettings.currentStdZero, false);
  serialPrintFloatParam("var", variation, false);
    Serial.println("%");
Serial.println(calibrationSteps[9].message);
#endif
  showCalibrationStep(9);        // "Set jumper to position 3-4..." message
  waitButtonClick(rightButton);  // Wait for OK button click

  ppsSetVoltage(channelPlus, 2.5, false); // Set +/-2.5 V output voltage
  ppsSetVoltage(channelMinus, 2.5, false);
  delay(500);
  // Resistor 1.2k is connected by JP2 jumper on main board, hence Isup = 2.5V / 1.2k = 2.083mA
  ActiveSettings.currentLowGain = (getAdcCodeSingleEnded(2, 20, FullScaleLow) - ActiveSettings.currentLowZero) / 2.083;
#ifdef __DEBUG__
  serialPrintFloatParam("ActiveSettings.currentLowGain", ActiveSettings.currentLowGain, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
#endif
  ppsSetVoltage(channelPlus, 12, false); // Set +/-12 V output voltage
  ppsSetVoltage(channelMinus, 12, false);
  delay(500);
  // Resistor 1.2k is connected by JP2 jumper on main board, hence Isup = 12V / 1.2k = 10mA
  ActiveSettings.currentStdGain = (getAdcCodeSingleEnded(2, 20, FullScaleStd) - ActiveSettings.currentStdZero) / 10;
#ifdef __DEBUG__
  serialPrintFloatParam("ActiveSettings.currentStdhGain", ActiveSettings.currentStdGain, false);
  serialPrintFloatParam("var", variation, false);
  Serial.println("%");
  Serial.println(calibrationSteps[10].message);
#endif
  showCalibrationStep(10);       // "Set jumper to position 1-2..." message
  waitButtonClick(rightButton);  // Wait for OK button click
}

/********************************************/
/* Final steps if calibration is successful */
/********************************************/
void calibrationFinalize(String title) {
  ppsReset(false); // Set voltage at both V+ and V- PPS outputs to 0
  showCalibrationCompletedPage(); 
  eepromStoreSettings();   // Store all settings in EEPROM
#ifdef __DEBUG__  
  Serial.println("\n" + title + " calibration completed. Settings are stored in EEPROM.\nPress Exit to continue.");
#endif
  waitButtonClick(leftButton);
}

/*************************************/
/* Final steps if calibration failed */
/*************************************/
void calibrationFailure(String title) {
#ifdef __DEBUG__  
  Serial.println("\n" + title + " calibration failed. Settings are not stored in EEPROM.\nPress Exit to continue.");
#endif
  handleError(3);
}

//------------ Local functions ---------------

uint16_t adjustPPSVoltage(ppsChannel Channel, float voltageInit) {
  uint16_t count = ppsVoltageToCode(Channel, voltageInit);
  ppsSetVoltageCode(Channel, count, true, false);
  while (true) {
    int button = waitAnyButtonClick();
    switch (button) {
      case middleButton:
        count = (count < 4095) ? count + 1 : 0;
        ppsSetVoltageCode(Channel, count, true, false);
        break;
      case leftButton:
        count =  (count > 0) ? count - 1 : 4095;
        ppsSetVoltageCode(Channel, count, true, false);
        break;
      case rightButton:
        return count;
    }
    delay(20);
  }
}

float adjustVoltmeter(float initVoltage) {
  float voltage = initVoltage;
  showVoltmeter(voltage);
  while (true) {
    int button = waitButtonPress();
    if (button == rightButton) {   // OK button is pressed - return
      waitButtonRelease(button);
      return voltage;
    }
    else {                        // -1 or +1 button is pressed - adjust voltage
      adjustVolts(button, voltage);
    }
  }
}

void adjustVolts(int buttonPin, float &voltage) {
  const uint32_t holdInterval = 250; // change value every 250ms if user holds button pressed
  uint32_t pressStart = millis();
  uint32_t lastIncrement = 0;
  bool longPress = false;

  while (digitalRead(buttonPin) == LOW) {
    uint32_t currentTime = millis();
    // If it is a long press
    if (currentTime - pressStart >= holdInterval) {
      longPress = true;

      if (currentTime - lastIncrement >= holdInterval) {
        changeAndShow(buttonPin, voltage);  
        lastIncrement = currentTime;
      }
    }
  }
  // If it is a short press
  if (!longPress) {
    changeAndShow(buttonPin, voltage);  
  }
}

void changeAndShow(int buttonPin, float &voltage) {
  voltage = (buttonPin == leftButton) ? (voltage - 0.001) : (voltage + 0.001);
  showVoltmeter(voltage);
}

#ifdef __DEBUG__
// Print Name-Value pair to Serial monitor
void serialPrintFloatParam(String Name, float Value, bool printEol) {
  Serial.print(Name + "=" + Value + " ");
  if (printEol) {
    Serial.println("");
  }
}

void serialPrintIntParam(String Name, int Value, bool printEol) {
  Serial.print(Name + "=" + Value + " ");
  if (printEol) {
    Serial.println("");
  }
}
#endif
