Exemplo n.º 1
0
void Thermocycler::SetPlateControlStrategy() {
  if (InControlledRamp())
    return;

  if (absf(iTargetPlateTemp - GetPlateTemp()) >= PLATE_BANGBANG_THRESHOLD && !InControlledRamp()) {
    iPlateControlMode = EBangBang;
    iPlatePid.SetMode(MANUAL);
  }
  else {
    iPlateControlMode = EPIDPlate;
    iPlatePid.SetMode(AUTOMATIC);
  }

  if (iRamping) {
    if (iTargetPlateTemp >= GetPlateTemp()) {
      iDecreasing = false;
      if (iTargetPlateTemp < PLATE_PID_INC_LOW_THRESHOLD)
        iPlatePid.SetTunings(PLATE_PID_INC_LOW_P, PLATE_PID_INC_LOW_I, PLATE_PID_INC_LOW_D);
      else
        iPlatePid.SetTunings(PLATE_PID_INC_NORM_P, PLATE_PID_INC_NORM_I, PLATE_PID_INC_NORM_D);

    }
    else {
      iDecreasing = true;
      if (iTargetPlateTemp > PLATE_PID_DEC_HIGH_THRESHOLD)
        iPlatePid.SetTunings(PLATE_PID_DEC_HIGH_P, PLATE_PID_DEC_HIGH_I, PLATE_PID_DEC_HIGH_D);
      else if (iTargetPlateTemp < PLATE_PID_DEC_LOW_THRESHOLD)
        iPlatePid.SetTunings(PLATE_PID_DEC_LOW_P, PLATE_PID_DEC_LOW_I, PLATE_PID_DEC_LOW_D);
      else
        iPlatePid.SetTunings(PLATE_PID_DEC_NORM_P, PLATE_PID_DEC_NORM_I, PLATE_PID_DEC_NORM_D);
    }
  }
}
Exemplo n.º 2
0
void Thermocycler::SetPlateControlStrategy() {
  if (InControlledRamp())
    return;
    
  if (fabs(m_target_plate_temp - GetPlateTemp()) >= PLATE_BANGBANG_THRESHOLD && !InControlledRamp()) {
    m_plate_control_mode = EBangBang;
    m_plate_pid->SetMode(MANUAL);
  } else {
    m_plate_control_mode = EPIDPlate;
    m_plate_pid->SetMode(AUTOMATIC);
  }
  
  if (m_is_ramping) {
    if (m_target_plate_temp >= GetPlateTemp()) {
      m_is_decreasing = false;
      if (m_target_plate_temp < PLATE_PID_INC_LOW_THRESHOLD)
        m_plate_pid->SetTunings(PLATE_PID_INC_LOW_P, PLATE_PID_INC_LOW_I, PLATE_PID_INC_LOW_D);
      else
        m_plate_pid->SetTunings(PLATE_PID_INC_NORM_P, PLATE_PID_INC_NORM_I, PLATE_PID_INC_NORM_D);

    } else {
      m_is_decreasing = true;
      if (m_target_plate_temp > PLATE_PID_DEC_HIGH_THRESHOLD)
        m_plate_pid->SetTunings(PLATE_PID_DEC_HIGH_P, PLATE_PID_DEC_HIGH_I, PLATE_PID_DEC_HIGH_D);
      else if (m_target_plate_temp < PLATE_PID_DEC_LOW_THRESHOLD)
        m_plate_pid->SetTunings(PLATE_PID_DEC_LOW_P, PLATE_PID_DEC_LOW_I, PLATE_PID_DEC_LOW_D);
      else
        m_plate_pid->SetTunings(PLATE_PID_DEC_NORM_P, PLATE_PID_DEC_NORM_I, PLATE_PID_DEC_NORM_D);
    }
  }
}
Exemplo n.º 3
0
boolean Thermocycler::Pause() {
  if (iProgramState == ERunning && !iPaused) {
    iPauseTemp = GetPlateTemp();
    iPaused = true;
    return true;
  }
  return false;
}
Exemplo n.º 4
0
void Thermocycler::ControlPeltier() {
  ThermalDirection newDirection = OFF;

  if (iProgramState == ERunning || (iProgramState == EComplete && ipCurrentStep != NULL)) {
    // Check whether we are nearing target and should switch to PID control
    if (iPlateControlMode == EBangBang && absf(iTargetPlateTemp - GetPlateTemp()) < PLATE_BANGBANG_THRESHOLD) {
      iPlateControlMode = EPIDPlate;
      iPlatePid.SetMode(AUTOMATIC);
      iPlatePid.ResetI();
    }

    // Apply control mode
    if (iPlateControlMode == EBangBang)
      iPeltierPwm = iTargetPlateTemp > GetPlateTemp() ? MAX_PELTIER_PWM : MIN_PELTIER_PWM;
    iPlatePid.Compute();

    if (iDecreasing && iTargetPlateTemp > PLATE_PID_DEC_LOW_THRESHOLD) {
      if (iTargetPlateTemp < GetPlateTemp())
        iPlatePid.ResetI();
      else
        iDecreasing = false;
    } 

    if (iPeltierPwm > 0)
      newDirection = HEAT;
    else if (iPeltierPwm < 0)
      newDirection = COOL; 
    else
      newDirection = OFF;
  } 
  else {
    iPeltierPwm = 0;
  }
  iThermalDirection = newDirection;
  SetPeltier(newDirection, abs(iPeltierPwm));
}
Exemplo n.º 5
0
void Thermocycler::ControlPeltier() {
  ThermalDirection newDirection = OFF;
  
  if (m_program_state == ERunning || (m_program_state == EComplete && m_current_step != NULL)) {
    // Check whether we are nearing target and should switch to PID control
    if (m_plate_control_mode == EBangBang && fabs(m_target_plate_temp - GetPlateTemp()) < PLATE_BANGBANG_THRESHOLD) {
      m_plate_control_mode = EPIDPlate;
      m_plate_pid->SetMode(AUTOMATIC);
      m_plate_pid->ResetI();
    }
 
    // Apply control mode
    if (m_plate_control_mode == EBangBang)
      m_peltier_pwm = m_target_plate_temp > GetPlateTemp() ? MAX_PELTIER_PWM : MIN_PELTIER_PWM;
    m_plate_pid->Compute();
    
    if (m_is_decreasing && m_target_plate_temp > PLATE_PID_DEC_LOW_THRESHOLD) {
      if (m_target_plate_temp < GetPlateTemp())
        m_plate_pid->ResetI();
      else
        m_is_decreasing = false;
    } 
    
    if (m_peltier_pwm > 0)
      newDirection = HEAT;
    else if (m_peltier_pwm < 0)
      newDirection = COOL; 
    else
      newDirection = OFF;
  } else {
    m_peltier_pwm = 0;
  }
  
  m_thermal_direction = newDirection;
  SetPeltier(newDirection, abs(m_peltier_pwm));
}
Exemplo n.º 6
0
void Thermocycler::PrepareStep() {
  //update eta calc params
  if (ipPreviousStep == NULL || ipPreviousStep->GetTemp() != ipCurrentStep->GetTemp()) {
    iRamping = true;
    iRampElapsedTimeMs = 0;
    iRampStartTemp = GetPlateTemp();
  }
  else {
    iCycleElapsedTimeMs = 0; //next step starts immediately
  }
  // Switch cycle to display
  ProgramComponent *pComp = ipProgram->GetComponent(ipProgram->GetCurrentComponentIndex());
  if (pComp->GetType() == ProgramComponent::ECycle) {
      ipDisplayCycle = (Cycle*) pComp;
  }
  CalcPlateTarget();
  SetPlateControlStrategy();
}
Exemplo n.º 7
0
//private
void Thermocycler::AdvanceToNextStep() {
  m_previous_step = m_current_step;
  m_current_step = m_program->GetNextStep();
  if (m_current_step == NULL)
    return;
  
  //update eta calc params
  if (m_previous_step == NULL || m_previous_step->GetTemp() != m_current_step->GetTemp()) {
    m_is_ramping = true;
    m_ramp_start_time = millis();
    m_ramp_start_temp = GetPlateTemp();
  } else {
    m_cycle_start_time = millis(); //next step starts immediately
  }
  
  CalcPlateTarget();
  SetPlateControlStrategy();
}
Exemplo n.º 8
0
//private
void Thermocycler::AdvanceToNextStep() {
  ipPreviousStep = ipCurrentStep;
  ipCurrentStep = ipProgram->GetNextStep();
  if (ipCurrentStep == NULL)
    return;
  
  //update eta calc params
  if (ipPreviousStep == NULL || ipPreviousStep->GetTemp() != ipCurrentStep->GetTemp()) {
    iRamping = true;
    iRampStartTime = millis();
    iRampStartTemp = GetPlateTemp();
  } else {
    iCycleStartTime = millis(); //next step starts immediately
  }
  
  CalcPlateTarget();
  SetPlateControlStrategy();
}
Exemplo n.º 9
0
//PreprocessProgram initializes ETA parameters and validates/modifies ramp conditions
void Thermocycler::PreprocessProgram() {
  Step* pCurrentStep;
  Step* pPreviousStep = NULL;

  iProgramHoldDurationS = 0;
  iEstimatedTimeRemainingS = 0;
  iHasCooled = false;

  iProgramControlledRampDurationS = 0;
  iProgramFastRampDegrees = 0;
  iElapsedFastRampDegrees = 0;
  iTotalElapsedFastRampDurationMs = 0;

  ipProgram->BeginIteration();
  while ((pCurrentStep = ipProgram->GetNextStep()) && !pCurrentStep->IsFinal()) {
    //validate ramp
    if (pPreviousStep != NULL && pCurrentStep->GetRampDurationS() * 1000 < absf(pCurrentStep->GetTemp() - pPreviousStep->GetTemp()) * PLATE_FAST_RAMP_THRESHOLD_MS) {
      //cannot ramp that fast, ignored set ramp
      pCurrentStep->SetRampDurationS(0);
    }

    //update eta hold
    iProgramHoldDurationS += pCurrentStep->GetStepDurationS();

    //update eta ramp
    if (pCurrentStep->GetRampDurationS() > 0) {
      //controlled ramp
      iProgramControlledRampDurationS += pCurrentStep->GetRampDurationS();
    }
    else {
      //fast ramp
      double previousTemp = pPreviousStep ? pPreviousStep->GetTemp() : GetPlateTemp();
      iProgramFastRampDegrees += absf(previousTemp - pCurrentStep->GetTemp()) - CYCLE_START_TOLERANCE;
    }

    pPreviousStep = pCurrentStep;
  }
}
Exemplo n.º 10
0
//PreprocessProgram initializes ETA parameters and validates/modifies ramp conditions
void Thermocycler::PreprocessProgram() {
  Step* pCurrentStep;
  Step* pPreviousStep = NULL;
  
  m_program_hold_duration_sec = 0;
  m_estimated_time_remaining_sec = 0;
  m_has_cooled = false;
  
  m_program_controlled_ramp_duration_sec = 0;
  m_program_fast_ramp_degrees = 0;
  m_elapsed_fast_ramp_degrees = 0;
  m_total_elapsed_fast_ramp_duration_ms = 0;
  
  m_program->BeginIteration();
  while ((pCurrentStep = m_program->GetNextStep()) && !pCurrentStep->IsFinal()) {
    //validate ramp
    if (pPreviousStep != NULL && pCurrentStep->GetRampDurationS() * 1000 < fabs(pCurrentStep->GetTemp() - pPreviousStep->GetTemp()) * PLATE_FAST_RAMP_THRESHOLD_MS) {
      //cannot ramp that fast, ignored set ramp
      pCurrentStep->SetRampDurationS(0);
    }
    
    //update eta hold
    m_program_hold_duration_sec += pCurrentStep->GetStepDurationS();
 
    //update eta ramp
    if (pCurrentStep->GetRampDurationS() > 0) {
      //controlled ramp
      m_program_controlled_ramp_duration_sec += pCurrentStep->GetRampDurationS();
    } else {
      //fast ramp
      double previousTemp = pPreviousStep ? pPreviousStep->GetTemp() : GetPlateTemp();
      m_program_fast_ramp_degrees += fabs(previousTemp - pCurrentStep->GetTemp()) - CYCLE_START_TOLERANCE;
    }
    
    pPreviousStep = pCurrentStep;
  }
}
Exemplo n.º 11
0
void Thermocycler::SetPeltier(ThermalDirection dir, int pwm /* Signed value of peltier */) {

  // TODO Use table of internal heat & peltier efficiency
  if (dir == COOL) {
    if (GetPlateTemp() < 30) {
      pwm = pwm/8;
    }
    else if (GetPlateTemp() < 35) {
      pwm = pwm/4;
    }
    else if (GetPlateTemp() < 40) {
      pwm = pwm/2;
    }
    else if (GetPlateTemp() < 50) {
      pwm = pwm * 2 / 3;
    }
  }
  pwm = max(-MAX_PELTIER_PWM, min(MAX_PELTIER_PWM, (int)(pwm * iPowerOutputRatio)));
    Thermocycler::ThermalDirection dirActual;
    int pwmActual;
  if (dir != OFF && prevActualDirection != OFF && dir != prevActualDirection && prevActualPWMDuty!=0) {
      // Direction will be changed.
      if (prevPWMDuty==0 && pwm > PWM_SWITCHING_THRESHOLD) {
          pwmActual = pwm;
          dirActual = dir;
      } else {
          // Once set zero without switching relay
          pwmActual = 0;
          dirActual = prevActualDirection;
      }
  } else {
      // No need of switching direction.
      dirActual = dir;
      pwmActual = pwm;
  }
  PCR_DEBUG("Pout(A)=");
  PCR_DEBUG(pwmActual);
  PCR_DEBUG(", dir=");
  PCR_DEBUG_LINE(dirActual);
#ifdef USE_FAN
  digitalWrite(PIN_FAN, PIN_FAN_VALUE_ON);
#endif

  if (dirActual == COOL) {
    digitalWrite(PIN_WELL_INA, PIN_WELL_VALUE_OFF);
    digitalWrite(PIN_WELL_INB, PIN_WELL_VALUE_ON);
  }
  else if (dirActual == HEAT) {
    digitalWrite(PIN_WELL_INA, PIN_WELL_VALUE_ON);
    digitalWrite(PIN_WELL_INB, PIN_WELL_VALUE_OFF);
  }
  else {
      // Off
    digitalWrite(PIN_WELL_INA, PIN_WELL_VALUE_OFF);
    digitalWrite(PIN_WELL_INB, PIN_WELL_VALUE_OFF);
  }
  analogValuePeltier = pwmActual;
  int absOutput = (dir==COOL)?-pwmActual:pwmActual;
#ifdef PIN_WELL_PWM_ACTIVE_LOW
  analogWrite(PIN_WELL_PWM, MAX_PELTIER_PWM-absOutput);
#else
  analogWrite(PIN_WELL_PWM, absOutput);
#endif /* PIN_WELL_PWM_ACTIVE_LOW */
  statusBuff[statusIndex].wellOutput = pwm;

  prevDirection = dir;
  prevPWMDuty = pwm;
  prevActualDirection = dirActual;
  prevActualPWMDuty = pwmActual;
}
Exemplo n.º 12
0
// internal
boolean Thermocycler::Loop() {

  ipCommunicator->Process();
  unsigned long loopElapsedTimeMs = millis() - iPrevLoopStartTimeMs;
  iPrevLoopStartTimeMs = millis();

  switch (iProgramState) {
  case EStartup:
    iTempUpdated = false;
    if (millis() > STARTUP_DELAY) {
      iProgramState = EStopped;
      	iRestarted = false;
      if (!iRestarted && !ipCommunicator->CommandReceived()) {
        //check for stored program
        SCommand command;
        /*
        if (ProgramStore::RetrieveProgram(command, (char*)ipCommunicator->GetBuffer())) {
          ProcessCommand(command);
        }
        */
      }
    }
    break;

  case ELidWait:
    if (GetLidTemp() >= iTargetLidTemp - LID_START_TOLERANCE) {
      //lid has warmed, begin program
      iThermalDirection = OFF;
      iPeltierPwm = 0;
      PreprocessProgram();
      iProgramState = ERunning;

      ipProgram->BeginIteration();
      AdvanceToNextStep();

      iProgramStartTimeMs = millis();
    }
    break;

  case ERunning:
    //update program
    if (!iPaused) {
      if (iRamping) {
        // Increment ramping time
        iRampElapsedTimeMs += loopElapsedTimeMs;
      } else {
        // Increment holding time
        iCycleElapsedTimeMs += loopElapsedTimeMs;
      }
      if (iProgramState == ERunning) {
        if (!ipCurrentStep->IsFinal() && (iNextStepPending || iNextCyclePending)) {
          if (iNextStepPending) {
            iNextStepPending = false;
            AdvanceToNextStep();
          }
          if (iNextCyclePending) {
            iNextCyclePending = false;
            AdvanceToNextCycle();
          }
          //check for program completion
          if (ipCurrentStep == NULL || ipCurrentStep->IsFinal()) {
            iProgramState = EComplete;
          }
        } else if (iRamping && abs(ipCurrentStep->GetTemp() - GetTemp()) <= CYCLE_START_TOLERANCE && GetRampElapsedTimeMs() > ipCurrentStep->GetRampDurationS() * 1000) {
          //begin step hold
          //eta updates
          if (ipCurrentStep->GetRampDurationS() == 0) {
            //fast ramp
            iElapsedFastRampDegrees += absf(GetTemp() - iRampStartTemp);
            iTotalElapsedFastRampDurationMs += iRampElapsedTimeMs;
          }

          if (iRampStartTemp > GetTemp()) {
            iHasCooled = true;
          }
          iRamping = false;
          iCycleElapsedTimeMs = 0;
        }
        else if (!iRamping && !ipCurrentStep->IsFinal() && iCycleElapsedTimeMs > (unsigned long)ipCurrentStep->GetStepDurationS() * 1000) {
          //begin next step
          AdvanceToNextStep();
          //check for program completion
          if (ipCurrentStep == NULL || ipCurrentStep->IsFinal()) {
            iProgramState = EComplete;
          }
        }
      }
      break;

    case EComplete:
      PCR_DEBUG_LINE(ipCurrentStep->GetTemp());
      if (iRamping && ipCurrentStep != NULL && abs(ipCurrentStep->GetTemp() - GetTemp()) <= CYCLE_START_TOLERANCE) {
        iRamping = false;
      }
      break;
    }
  }

  statusBuff[statusIndex].timestamp = millis();
  //Read lid and well temp
  statusBuff[statusIndex].hardwareStatus = HARD_NO_ERROR;
  HardwareStatus result = iPlateThermistor.ReadTemp();
  if (result!=HARD_NO_ERROR) {
      statusBuff[statusIndex].hardwareStatus = result;
  }
  result = iLidThermistor.ReadTemp();
  if (result!=HARD_NO_ERROR) {
      statusBuff[statusIndex].hardwareStatus = result;
  }

  statusBuff[statusIndex].lidTemp = GetLidTemp();
  statusBuff[statusIndex].wellTemp = GetPlateTemp();

  float lidTemp = 0;
  float wellTemp = 0;

  CheckHardware(&lidTemp, &wellTemp);
  PCR_DEBUG("L=");
  PCR_DEBUG(lidTemp);
  PCR_DEBUG(" W=wellTemp");
  PCR_DEBUG_LINE(wellTemp);

  iLidThermistor.setTemp(lidTemp);
  iPlateThermistor.setTemp(wellTemp);

  double estimatedAirTemp = wellTemp * 0.4 + lidTemp * 0.6;
  // Estimated delta to next 1 sec
  double diff = ((wellTemp - iEstimatedSampleTemp)/THETA_WELL + (estimatedAirTemp-iEstimatedSampleTemp)/THETA_LID ) / CAPACITY_TUBE;

  if (!iTempUpdated) {
    iTempUpdated = true;
    iEstimatedSampleTemp = estimatedAirTemp;
  } else if ( 5>diff && diff > -5) {
    iEstimatedSampleTemp += diff;
  }

  CalcPlateTarget();

  // Check error
  //if (iHardwareStatus==HARD_NO_ERROR || true) { //TODO WELL_TEST (dummy line)
  if (iHardwareStatus==HARD_NO_ERROR) { //TODO WELL_TEST
      ControlLid();
      ControlPeltier();
      if (iHardwareStatus!=HARD_NO_ERROR) {
        PCR_DEBUG("ERR=");
        PCR_DEBUG_LINE(iHardwareStatus);
      }
  } else {
      PCR_DEBUG_LINE("ALL OFF");
      iProgramState = EError;
      SetPeltier(OFF, 0);
      SetLidOutput(0);
  }

  //program
  UpdateEta();
 #ifdef USE_LCD
  ipDisplay->Update();
 #endif
  statusIndex = (statusIndex+1) % CyclerStatusBuffSize;
  statusCount++;
  return true;
}
Exemplo n.º 13
0
// internal
void Thermocycler::Loop() {
	digitalWrite(6, (lamp)?HIGH:LOW);
	digitalWrite(5, (!lamp)?HIGH:LOW);
	lamp = !lamp;
  switch (iProgramState) {
  case EStartup:
    if (millis() > STARTUP_DELAY) {
      iProgramState = EStopped;
      	iRestarted = false;
      if (!iRestarted && !ipSerialControl->CommandReceived()) {
        //check for stored program
        SCommand command;
        if (ProgramStore::RetrieveProgram(command, (char*)ipSerialControl->GetBuffer()))
          ProcessCommand(command);
      }
    }
    break;

  case ELidWait:
    if (GetLidTemp() >= iTargetLidTemp - LID_START_TOLERANCE) {
      //lid has warmed, begin program
      iThermalDirection = OFF;
      iPeltierPwm = 0;
      PreprocessProgram();
      iProgramState = ERunning;

      ipProgram->BeginIteration();
      AdvanceToNextStep();

      iProgramStartTimeMs = millis();
    }
    break;

  case ERunning:
    //update program
    if (iProgramState == ERunning) {
      if (iRamping && abs(ipCurrentStep->GetTemp() - GetPlateTemp()) <= CYCLE_START_TOLERANCE && GetRampElapsedTimeMs() > ipCurrentStep->GetRampDurationS() * 1000) {
        //begin step hold
        //eta updates
        if (ipCurrentStep->GetRampDurationS() == 0) {
          //fast ramp
          iElapsedFastRampDegrees += absf(GetPlateTemp() - iRampStartTemp);
          iTotalElapsedFastRampDurationMs += millis() - iRampStartTime;
        }

        if (iRampStartTemp > GetPlateTemp()) {
          iHasCooled = true;
        }
        iRamping = false;
        iCycleStartTime = millis();

      } 
      else if (!iRamping && !ipCurrentStep->IsFinal() && millis() - iCycleStartTime > (unsigned long)ipCurrentStep->GetStepDurationS() * 1000) {
        //begin next step
        AdvanceToNextStep();

        //check for program completion
        if (ipCurrentStep == NULL || ipCurrentStep->IsFinal()) {
          iProgramState = EComplete;        
        }
      }
    }
    break;

  case EComplete:
    if (iRamping && ipCurrentStep != NULL && abs(ipCurrentStep->GetTemp() - GetPlateTemp()) <= CYCLE_START_TOLERANCE)
      iRamping = false;
    break;
  }
  //lid 
  iLidThermistor.ReadTemp();
  ControlLid();

  //plate  
  iPlateThermistor.ReadTemp();
  CalcPlateTarget();
  ControlPeltier();

  //program
  UpdateEta();
  ipDisplay->Update();
  ipSerialControl->Process();

}
Exemplo n.º 14
0
void Thermocycler::Loop()
{
  switch (m_program_state)
  {
    case EStartup:
    if (millis() > STARTUP_DELAY) {
      m_program_state = EStopped;
      
      //if (!m_is_restarted && !m_serial_control->CommandReceived())
      {
        //check for stored program
        //SCommand command;
        //if (ProgramStore::RetrieveProgram(command, (char*)m_serial_control->GetBuffer()))
        //  ProcessCommand(command);
      }
    }
    break;

  case ELidWait:    
    if (GetLidTemp() >= m_target_lid_temp - LID_START_TOLERANCE) {
      //lid has warmed, begin program
      m_thermal_direction = OFF;
      m_peltier_pwm = 0;
      PreprocessProgram();
      m_program_state = ERunning;
      
      m_program->BeginIteration();
      AdvanceToNextStep();
      
      m_program_start_time_ms = millis();
    }
    break;
  
  case ERunning:
    //update program
    if (m_program_state == ERunning) {
      if (m_is_ramping && abs(m_current_step->GetTemp() - GetPlateTemp()) <= CYCLE_START_TOLERANCE && GetRampElapsedTimeMs() > m_current_step->GetRampDurationS() * 1000) {
        //begin step hold
        
        //eta updates
        if (m_current_step->GetRampDurationS() == 0) {
          //fast ramp
          m_elapsed_fast_ramp_degrees += fabs(GetPlateTemp() - m_ramp_start_temp);
          m_total_elapsed_fast_ramp_duration_ms += millis() - m_ramp_start_time;
        }
        
        if (m_ramp_start_temp > GetPlateTemp())
          m_has_cooled = true;
        m_is_ramping = false;
        m_cycle_start_time = millis();
        
      } else if (!m_is_ramping && !m_current_step->IsFinal() && millis() - m_cycle_start_time > (unsigned long)m_current_step->GetStepDurationS() * 1000) {
        //begin next step
        AdvanceToNextStep();
          
        //check for program completion
        if (m_current_step == NULL || m_current_step->IsFinal())
          m_program_state = EComplete;
      }
    }
    break;
    
  case EComplete:
    if (m_is_ramping && m_current_step != NULL && abs(m_current_step->GetTemp() - GetPlateTemp()) <= CYCLE_START_TOLERANCE)
      m_is_ramping = false;
    break;
  case EStopped: //Nothing
  case EError: //Nothing
  case EClear: //Nothing
    break;
  }
  
  //lid 
  m_lid_thermistor.ReadTemp();
  ControlLid();
  
  //plate  
  m_plate_thermistor.ReadTemp();
  CalcPlateTarget();
  ControlPeltier();
  
  //program
  UpdateEta();
  m_display->Update();
  m_serial_control->Process();
}