commit de400a11fb40c995d4feff60c9ac14a51074e919 Author: Andrzej Surowiec Date: Sun Mar 18 18:10:32 2018 +0100 Initial commit. diff --git a/ReflowOven2.ino b/ReflowOven2.ino new file mode 100644 index 0000000..394816d --- /dev/null +++ b/ReflowOven2.ino @@ -0,0 +1,725 @@ +/******************************************************************************* +* ControLeo Reflow Oven Controller (v2) +* Author: Keith Rome +* Website: www.wintellect.com/blogs/krome +* +* Based on the original ControLeo Reflow Oven Controller sample by Peter Easton @ whizoo.com +* +* This version is a near-complete rewrite that uses a phase-based profile system with +* minimum/maximum times per phase. Also supports additional runtime data on the display +* (time elapsed in current phase), triggering the buzzer alarm at various phase transitions, +* and sends data to the Serial console to help with fine-tuning the reflow profile for +* your individual oven hardware. Also uses EEPROM nonvolatile memory on ControLeo to +* remember the last profile that was selected when the oven is powered back up again. +* +* Thanks to Rocketscream for the original code using the PID library. The code has +* been heavily modified (removing PID) to give finer control over individual heating +* elements. +* +* This is an example of a reflow oven controller. The reflow curve below is for a +* lead-free profile, but this code supports both leaded and lead-free profiles. +* +* Temperature (Degree Celcius) Magic Happens Here! +* 245-| x x +* | x x +* | x x +* | x x +* 200-| x x +* | x | | x +* | x | | x +* | x | | +* 150-| x | | +* | x | | | +* | x | | | +* | x | | | +* | x | | | +* | x | | | +* | x | | | +* 30 -| x | | | +* |< 60 - 90 s >|< 90 - 120 s >|< 90 - 120 s >| +* | Preheat Stage | Soaking Stage | Reflow Stage | Cool +* 0 |_ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +* Time (Seconds) +* +* Here is an example serial log generated by running a complete cycle using +* the default lead-bearing solder profile: +* +* ControLeo Reflow Oven v2 firmware startup +* Last selected profile loading from eeprom: 1 +* Advancing to next profile +* Advancing to next profile +* Button Pressed: CONTROLEO_BUTTON_BOTTOM +* Starting Profile: Leaded solder +* Leaving Phase: Idle (0), Elapsed: 8s +* Entering Phase: Pre-heat (1), Exit Temp:145C, Min Time: 0s, Max: 0s, Ideal: 90s +* **Exit Temperature Reached, Time in phase: 173s, Temperature: 145.00C +* Leaving Phase: Pre-heat (1), Elapsed: 173s +* Entering Phase: Soak (2), Exit Temp:180C, Min Time: 30s, Max: 120s, Ideal: 30s +* **Duration/Temperature Reached, Time in phase: 55s, Temperature: 180.00C +* Leaving Phase: Soak (2), Elapsed: 55s +* Entering Phase: Liquidus (3), Exit Temp:210C, Min Time: 30s, Max: 90s, Ideal: 60s, alarm sounds on exit +* **Duration/Temperature Reached, Time in phase: 51s, Temperature: 210.00C +* Leaving Phase: Liquidus (3), Elapsed: 51s +* Entering Phase: Reflow (4), Exit Temp:180C (falling), Min Time: 30s, Max: 90s, Ideal: 60s +* *** Alarm *** +* **Duration/Temperature Reached, Time in phase: 69s, Temperature: 180.00C +* Leaving Phase: Reflow (4), Elapsed: 69s +* Entering Phase: Cooling (5), Exit Temp:50C (falling), Min Time: 0s, Max: 0s, Ideal: 0s +* **Exit Temperature Reached, Time in phase: 679s, Temperature: 50.00C +* Profile Finished, Total Elapsed Time: 1028s, Peak Temperature Observed: 212.25C +* Leaving Phase: Cooling (5), Elapsed: 679s +* Entering Phase: Idle (0), Exit Temp:0C, Min Time: 0s, Max: 0s, Ideal: 0s +* +* +* +* This firmware builds on the work of other talented individuals: +* ========================================== +* Rocketscream (www.rocketscream.com) +* Produced the Arduino reflow oven shield ands code that inspired this project. +* +* ========================================== +* Limor Fried of Adafruit (www.adafruit.com) +* Author of Arduino MAX6675 library. Adafruit has been the source of tonnes of +* tutorials, examples, and libraries for everyone to learn. +* +* Disclaimer +* ========== +* Dealing with high voltage is a very dangerous act! Please make sure you know +* what you are dealing with and have proper knowledge before hand. Your use of +* any information or materials on this reflow oven controller is entirely at +* your own risk, for which we shall not be liable. +* +* Released under WTFPL license. +* +* Revision Description +* ======== =========== +* 1.00 Initial public release. +* 2.00 Public release. +*******************************************************************************/ + +// ***** INCLUDES ***** +#include +//#include +#include "ControLeo2_MAX31855.h" +#include +#include "max6675.h" +#include + +// Defines for the 2 buttons +#define CONTROLEO_BUTTON_TOP_PIN 3 // Top button is on D11 +#define CONTROLEO_BUTTON_BOTTOM_PIN 2 // Bottom button is on D2 +#define CONTROLEO_BUTTON_NONE 0 +#define CONTROLEO_BUTTON_TOP 1 // S1 +#define CONTROLEO_BUTTON_BOTTOM 2 // S2 + +// The Buzzer is on D13 +#define CONTROLEO_BUZZER_PIN 4 + +// ***** CONSTANTS ***** +#define CLOCK_INTERVAL 100 // how frequently the state machine checks for advancements (ms) +#define SAMPLE_INTERVAL 500 // how frequently temperature measurements are updated (ms) +#define CYCLE_INTERVAL 1000 // how frequently the oven cycles through the current phase's heating pattern (ms per bit) +#define MAX_START_TEMP 50 // maximum temperature where a new reflow session will be allowed to start +#define NUM_PHASES 4 // number of phases in a profile (always assume a final "cooling" phase) +#define NUM_HEATERS 3 // number of heaters installed +#define DEFAULT_PROFILE 0 // default temperature profile at startup (overridden by EEPROM) +#define BUZZER_DURATION 250 // how long to play buzzer sounds +#define ADDR_CURR_PROFILE 0 // address in EEPROM for storing the current profile ID + +#define OFF 0 +#define ON 1 + +// ***** PRODUCT IDENTIFICATION ***** +const char* BRAND_ID = "ControLeo"; +const char* PRODUCT_ID = "Reflow Oven v2"; + +int thermoDO = 8; +int thermoCS = 9; +int thermoCLK = 10; + +const int rs = A3, en = A2, d4 = A1, d5 = A0, d6 = 15, d7 = 14; + +// ***** HARDWARE INTERFACES ***** +struct Hardware { + unsigned long DisableBuzzerAt; + int HeaterPins[NUM_HEATERS]; // Pin assignments + //ControLeo2_LiquidCrystal LCD; // Specify LCD interface + LiquidCrystal LCD; + //ControLeo2_MAX31855 Thermocouple; // Specify MAX31855 thermocouple interface + MAX6675 Thermocouple; +}; +Hardware hardware = { 0, { + 7, // upper heater pin + 5, // lower heater pin + 6 // booster heater pin + }, + //ControLeo2_LiquidCrystal(), + LiquidCrystal(rs, en, d4, d5, d6, d7), + MAX6675(thermoCLK, thermoCS, thermoDO)}; + +// ***** PROFILES ***** +// Each element of this array is a 8-second window for an element. For example, if the value is 0b11001111 then +// the element will be on for 2 seconds, off for 2 seconds then on for 4 seconds. This pattern will keep +// repeating itself until the temperature rises through the temperate transition point given in tempPoints. This +// gives fine control over each element and has the following benefits: +// 1. Prevents individual elements from getting too hot, perhaps burning insulation. +// 2. Ensures heat comes from the right part of the oven at the right time +// 3. Helps overall current draw by being able to turn off some elements while turning others on +// ==================== YOU SHOULD TUNE THESE VALUES TO YOUR REFLOW OVEN!!! ==================== +enum CrossingDirection { + RISE, + FALL +}; +struct ReflowPhase { + char* Name; + int ExitTemperatureC; + CrossingDirection RisingOrFalling; + int MinDurationS; + int MaxDurationS; + int TargetDurationS; + int HeaterPattern[NUM_HEATERS]; + boolean AlarmOnExit; +}; +ReflowPhase idlePhase = { "Idle", 0, RISE, 0, 0, 0, { 0b00000000, 0b00000000, 0b00000000 }, false }; +ReflowPhase coolingPhase = { "Cooling", MAX_START_TEMP, FALL, 0, 0, 0, { 0b00000000, 0b00000000, 0b00000000 }, false }; + +struct ReflowProfile { + char* Name; + ReflowPhase Phases[NUM_PHASES]; +}; +ReflowProfile profiles[] = { + { + "Lead-free solder", + { // Zone Exit(C) Direction Min(S) Max(S) Tgt(S) Upper Lower Boost Alarm + { "Pre-heat", 150, RISE, 0, 0, 90, { 0b11001101, 0b10111110, 0b01010011 }, false }, + { "Soak", 205, RISE, 30, 120, 30, { 0b01000100, 0b10101011, 0b00010000 }, false }, + { "Liquidus", 235, RISE, 30, 90, 60, { 0b11011110, 0b10111111, 0b01101101 }, false }, + { "Reflow", 225, FALL, 30, 90, 60, { 0b00010001, 0b01000100, 0b00000000 }, true }, + } + }, + { + "Leaded solder", + { // Zone Exit(C) Direction Min(S) Max(S) Tgt(S) Upper Lower Boost Alarm + { "Pre-heat", 145, RISE, 0, 0, 90, { 0b11001101, 0b01110110, 0b01010011 }, false }, + { "Soak", 180, RISE, 30, 120, 30, { 0b01000100, 0b10101011, 0b00010001 }, false }, + { "Liquidus", 210, RISE, 30, 90, 60, { 0b10111110, 0b11110111, 0b00101000 }, true }, + { "Reflow", 180, FALL, 30, 90, 60, { 0b01000000, 0b00011000, 0b00000100 }, false }, + } + }, +}; +#define NUM_PROFILES (sizeof(profiles)/sizeof(ReflowProfile)) //array size is computed from initialized data + +// ***** STATE TRACKING ***** +struct OvenState { + int SelectedProfile; + boolean IsFaulted; + boolean IsActive; + double TemperatureC; + double PeakTemperatureC; + unsigned long LastClocked; + unsigned long NextClock; + unsigned long NextSample; + unsigned long NextCycle; + int ActiveHeatCycle; + int ActivePhase; + unsigned long EnteredCurrentPhase; + int SecInPhase; + unsigned long ActiveSince; + ReflowPhase PhaseSchedule[NUM_PHASES + 2]; +}; +OvenState currentState = { + -1, false, false, 0, 0, + 0, CLOCK_INTERVAL, SAMPLE_INTERVAL, CYCLE_INTERVAL, + 0, 0, 0, 0, 0, + { idlePhase, idlePhase, idlePhase, idlePhase, idlePhase, coolingPhase} }; + + +void setup() +{ + // *********** Start of ControLeo2 initialization *********** + // Set up the buzzer and buttons + pinMode(CONTROLEO_BUZZER_PIN, OUTPUT); + pinMode(CONTROLEO_BUTTON_TOP_PIN, INPUT_PULLUP); + pinMode(CONTROLEO_BUTTON_BOTTOM_PIN, INPUT_PULLUP); + // Set the relays as outputs and turn them off + // The relay outputs are on D4 to D7 (4 outputs) + for (int i=4; i<8; i++) { + pinMode(i, OUTPUT); + digitalWrite(i, LOW); + } + // Set up the LCD's number of rows and columns +// lcd.begin(16, 2); + // Create the degree symbol for the LCD +// unsigned char degree[8] = {12,18,18,12,0,0,0,0}; +// lcd.createChar(0, degree); + // *********** End of ControLeo2 initialization *********** + + InitializeDisplay(); + + Serial.begin(9600); + Serial.print(BRAND_ID); Serial.print(" "); Serial.print(PRODUCT_ID); Serial.println(" firmware startup"); + + // configure heater GPIO + for (int i = 0; i < NUM_HEATERS; i++) { + pinMode(hardware.HeaterPins[i], OUTPUT); + digitalWrite(hardware.HeaterPins[i], LOW); + } + + int lastProfile = EEPROM.read(ADDR_CURR_PROFILE); + if (lastProfile == 255) { + lastProfile = DEFAULT_PROFILE; + Serial.print("Default profile loading: "); Serial.println(DEFAULT_PROFILE); + } else { + Serial.print("Last selected profile loading from eeprom: "); Serial.println(lastProfile); + } + + while (currentState.SelectedProfile != lastProfile) { + AdvanceProfile(true); + } + + DisplaySplashScreen(); + ResetState(); +} + +void loop() +{ + unsigned long now = millis(); + + // check for clock overflow (should only happen if you leave the oven on for 50+ days) + if (currentState.LastClocked > now) { + currentState.LastClocked = 0; + currentState.NextClock = CLOCK_INTERVAL; + currentState.NextCycle = CYCLE_INTERVAL; + currentState.NextSample = SAMPLE_INTERVAL; + currentState.EnteredCurrentPhase = 0; + } + + // check to see if we should capture a new temperature sample + if (currentState.NextSample <= now) { + CaptureTemperatureSample(); + currentState.NextSample = now + SAMPLE_INTERVAL; + } + + // should we check for phase transition this cycle? + if (currentState.NextClock <= now) { + CheckForPhaseTransition(); + currentState.LastClocked = now; + currentState.NextClock = now + CLOCK_INTERVAL; + } + + // advance the heater pattern + if (currentState.NextCycle <= now) { + AdvanceHeatingCycle(); + currentState.NextCycle = now + CYCLE_INTERVAL; + } + + // always check for button press so there is no unnecessary lag + // also, in the unlikely event that a phase transition and button press happen in the same + // cycle, the button press will always get the last s + CheckForButtonPress(); + + // turn off the buzzer if we need to + if (hardware.DisableBuzzerAt != 0 && hardware.DisableBuzzerAt <= now) { + EnableBuzzer(OFF); + } + + delay(10); +} + +void InitializeDisplay() +{ + // Degree symbol for LCD + unsigned char degree[8] = {140,146,146,140,128,128,128,128}; + + hardware.LCD.begin(16, 2); + hardware.LCD.createChar(0, degree); + hardware.LCD.clear(); + delay(3000); +} + +void DisplaySplashScreen() +{ + hardware.LCD.print(BRAND_ID); + hardware.LCD.setCursor(0, 1); + hardware.LCD.print(PRODUCT_ID); + delay(3000); + hardware.LCD.clear(); + + DisplayProfile(); + UpdateDisplayedTemperature(); + EnableBuzzer(ON); +} + +void PrintAt(int line, int column, int maxWidth, boolean rightAlign, char*msg) +{ + int msgLen = strlen(msg); + if (msgLen > maxWidth) + msgLen = maxWidth; + int startCol = column; + if (rightAlign && (msgLen < maxWidth)) + startCol += (maxWidth - msgLen); + + hardware.LCD.setCursor(column, line); + for (int i = 0; i < (startCol - column); i++) + hardware.LCD.print(" "); + hardware.LCD.print(msg); + for (int i = startCol + msgLen; i < maxWidth; i++) + hardware.LCD.print(" "); +} + +void WriteAt(int line, int column, int glyph) +{ + hardware.LCD.setCursor(column, line); + hardware.LCD.write(glyph); +} + +void DisplayProfile() +{ + PrintAt(0, 0, 16, false, profiles[currentState.SelectedProfile].Name); +} + +void DisplayPhase() +{ + PrintAt(0, 0, 11, false, currentState.PhaseSchedule[currentState.ActivePhase].Name); +} + +void DisplayElapsedInPhase(boolean forceUpdate) +{ + int secInPhase = (millis() - currentState.EnteredCurrentPhase) / 1000; + if (forceUpdate || (secInPhase != currentState.SecInPhase)) { + currentState.SecInPhase = secInPhase; + char msg[6]; + sprintf(msg, " %ds", secInPhase); + PrintAt(0, 11, 5, true, msg); + } +} + +void UpdateDisplayedTemperature() +{ + // if the thermocouple is in a faulted state, then display error + if (currentState.IsFaulted) { + PrintAt(1, 0, 16, false, "fault detected"); + return; + } + + // Print current temperature + char buffer[12]; + dtostrf(currentState.TemperatureC, 3, 1, buffer); + int sz = strlen(buffer); + char msg[12]; + sprintf(msg, "%s %s", buffer, "C"); + PrintAt(1, 0, 12, false, msg); + Serial.println(currentState.TemperatureC); + // draw the special glyph for degree symbol + WriteAt(1, sz, 0); +} + +void EnableBuzzer(uint8_t onOrOff) +{ + //analogWrite(CONTROLEO_BUZZER_PIN, onOrOff? 15:0); + digitalWrite(CONTROLEO_BUZZER_PIN, onOrOff? 1:0); + if (onOrOff == ON) { + Serial.println("*** Alarm ***"); + hardware.DisableBuzzerAt = millis() + BUZZER_DURATION; + } else { + hardware.DisableBuzzerAt = 0; + } +} + +void CaptureTemperatureSample() +{ + // Read current temperature + //double currentTemperature = hardware.Thermocouple.readThermocouple(CELSIUS); + double currentTemperature = hardware.Thermocouple.readCelsius(); + + + // don't bother doing anything unless it actually changes + if (currentState.TemperatureC == currentTemperature) return; + + // Check for thermocouple problem + if (currentTemperature == FAULT_OPEN || currentTemperature == FAULT_SHORT_GND || currentTemperature == FAULT_SHORT_VCC) { + // There is a problem with the thermocouple + Serial.print("Thermocouple Fault: "); + if (currentTemperature == FAULT_OPEN) Serial.println("FAULT_OPEN"); + if (currentTemperature == FAULT_SHORT_GND) Serial.println("FAULT_SHORT_GND"); + if (currentTemperature == FAULT_SHORT_VCC) Serial.println("FAULT_SHORT_VCC"); + currentState.IsFaulted = true; + End(false); + } else { + currentState.IsFaulted = false; + } + + // track peaks + if (currentTemperature > currentState.PeakTemperatureC) { + currentState.PeakTemperatureC = currentTemperature; + } + + // update display + currentState.TemperatureC = currentTemperature; + UpdateDisplayedTemperature(); +} + +void CheckForButtonPress() +{ + int button = GetRisingEdgeOfButtonPress(); + if (button == CONTROLEO_BUTTON_NONE) return; + + if (button == CONTROLEO_BUTTON_TOP) { + Serial.println("Button Pressed: CONTROLEO_BUTTON_TOP"); + } else { + Serial.println("Button Pressed: CONTROLEO_BUTTON_BOTTOM"); + } + + if (button == CONTROLEO_BUTTON_TOP) { + // top button was pressed + + // if the oven is idle, the top button will cycle through the known profiles + // but is ignored while the oven is operating + if (!currentState.IsActive) { + AdvanceProfile(false); + } + } + + if (button == CONTROLEO_BUTTON_BOTTOM) { + // bottom button was pressed + + // bottom button starts/stops the oven + if (currentState.IsActive) { + End(true); + } else { + Start(); + } + } +} + +#define DEBOUNCE_DETECT 50 // minimum time button needs be pressed +#define DEBOUNCE_RESET 500 // minimum time between button signals +int GetRisingEdgeOfButtonPress() +{ + static int heldButton = CONTROLEO_BUTTON_NONE; + static int lastEvent = CONTROLEO_BUTTON_NONE; + static unsigned long lastReset = 0; + static unsigned long heldSince = 0; + + unsigned long now = millis(); + int signal = CONTROLEO_BUTTON_NONE; + int buttonEvent = CONTROLEO_BUTTON_NONE; + + // Read the current button status + signal = CONTROLEO_BUTTON_NONE; + if (digitalRead(CONTROLEO_BUTTON_TOP_PIN) == LOW) + signal = CONTROLEO_BUTTON_TOP; + else if (digitalRead(CONTROLEO_BUTTON_BOTTOM_PIN) == LOW) + signal = CONTROLEO_BUTTON_BOTTOM; + + if (heldButton == signal && heldButton != CONTROLEO_BUTTON_NONE) { + // a button is currently pressed, but is it pressed for long enough? + if ((lastEvent == CONTROLEO_BUTTON_NONE) && (now - heldSince >= DEBOUNCE_DETECT)) { + // trigger it only once + buttonEvent = signal; + lastEvent = signal; + } + } + + if (heldButton != CONTROLEO_BUTTON_NONE && signal == CONTROLEO_BUTTON_NONE) { + // button was released + heldButton = CONTROLEO_BUTTON_NONE; + lastReset = now; + heldSince = 0; + lastEvent = CONTROLEO_BUTTON_NONE; + } + + if (heldButton == CONTROLEO_BUTTON_NONE && signal != CONTROLEO_BUTTON_NONE && (now - lastReset >= DEBOUNCE_RESET)) { + // new button is being pressed + heldButton = signal; + heldSince = now; + } + + return buttonEvent; +} + +void AdvanceHeatingCycle() +{ + int mask = 0b10000000 >> currentState.ActiveHeatCycle; + + if (currentState.IsActive) { + currentState.ActiveHeatCycle++; + if (currentState.ActiveHeatCycle > 7) { + currentState.ActiveHeatCycle = 0; + } + } else { + mask = 0b00000000; // disable all heaters + } + + // toggle heater GPIO pins + ReflowPhase currentPhase = currentState.PhaseSchedule[currentState.ActivePhase]; + for (int heaterIx = 0; heaterIx < NUM_HEATERS; heaterIx++) { + if (currentPhase.HeaterPattern[heaterIx] & mask) { + digitalWrite(hardware.HeaterPins[heaterIx], HIGH); + } else { + digitalWrite(hardware.HeaterPins[heaterIx], LOW); + } + } +} + +void CheckForPhaseTransition() +{ + if (!currentState.IsActive) return; + + DisplayElapsedInPhase(false); + + // can't leave idle without explicit start + if (currentState.ActivePhase == 0) return; + + ReflowPhase currentPhase = currentState.PhaseSchedule[currentState.ActivePhase]; + int timeInPhase = (millis() - currentState.EnteredCurrentPhase) / 1000; + CrossingDirection dir = currentPhase.RisingOrFalling; + + // check for phase change due to temperature rise + if (dir == RISE) { + if (currentPhase.ExitTemperatureC <= currentState.TemperatureC) { + if (currentPhase.MinDurationS == 0) { + MoveToNextPhase("Exit Temperature Reached", timeInPhase); + } else { + // but only allow phase change if any minimum time spent in phase has been met + if (timeInPhase > currentPhase.MinDurationS) { + MoveToNextPhase("Duration/Temperature Reached", timeInPhase); + } + } + } + } + + // check for phase change due to temperature fall + if (dir == FALL) { + if (currentPhase.ExitTemperatureC >= currentState.TemperatureC) { + if (currentPhase.MinDurationS == 0) { + MoveToNextPhase("Exit Temperature Reached", timeInPhase); + } else { + // but only allow phase change if any minimum time spent in phase has been met + if (timeInPhase > currentPhase.MinDurationS) { + MoveToNextPhase("Duration/Temperature Reached", timeInPhase); + } + } + } + } + + // check for phase change due to timer overrun + if (currentPhase.MaxDurationS > 0) { + if (timeInPhase >= currentPhase.MaxDurationS) { + MoveToNextPhase("Max Duration Exceeded", timeInPhase); + } + } +} + +void MoveToNextPhase(char* reason, int timeInPhase) +{ + Serial.print("**"); + Serial.print(reason); + Serial.print(", Time in phase: "); + Serial.print(timeInPhase); + Serial.print("s, Temperature: "); + Serial.print(currentState.TemperatureC); + Serial.println("C"); + TransitionToPhase(currentState.ActivePhase + 1); +} + +void Start() +{ + Serial.print("Starting Profile: "); Serial.println(profiles[currentState.SelectedProfile].Name); + currentState.PeakTemperatureC = 0; // reset + currentState.ActiveSince = millis(); // reset + currentState.IsActive = true; + TransitionToPhase(1); +} + +void End(boolean isUserInitiated) +{ + if (isUserInitiated) { + Serial.print("Profile Aborted by User"); + } else { + Serial.print("Profile Finished"); + } + Serial.print(", Total Elapsed Time: "); Serial.print((millis() - currentState.ActiveSince) / 1000); Serial.print("s"); + Serial.print(", Peak Temperature Observed: "); Serial.print(currentState.PeakTemperatureC); Serial.println("C"); + currentState.PeakTemperatureC = 0; // reset + currentState.ActiveSince = millis(); // reset + ResetState(); +} + +void ResetState() +{ + CaptureTemperatureSample(); + if (currentState.TemperatureC > MAX_START_TEMP) { + currentState.IsActive = true; + TransitionToPhase(NUM_PHASES + 1); + } else { + TransitionToPhase(0); + currentState.IsActive = false; + // Return to profile display + DisplayProfile(); + } +} + +void TransitionToPhase(int phase) +{ + if (phase == currentState.ActivePhase) return; + + if (phase > NUM_PHASES + 1) { + // transition beyond cooling phase + End(false); + return; + } + + int timeInLastPhase = (millis() - currentState.EnteredCurrentPhase) / 1000; + ReflowPhase oldPhase = currentState.PhaseSchedule[currentState.ActivePhase]; + ReflowPhase newPhase = currentState.PhaseSchedule[phase]; + + Serial.print("Leaving Phase: "); Serial.print(oldPhase.Name); + Serial.print(" ("); Serial.print(currentState.ActivePhase); + Serial.print("), Elapsed: "); Serial.print(timeInLastPhase); Serial.println("s"); + + Serial.print("Entering Phase: "); Serial.print(newPhase.Name); + Serial.print(" ("); Serial.print(phase); + Serial.print("), Exit Temp:"); Serial.print(newPhase.ExitTemperatureC); Serial.print("C"); + if (newPhase.RisingOrFalling == FALL) Serial.print(" (falling)"); + Serial.print(", Min Time: "); Serial.print(newPhase.MinDurationS); + Serial.print("s, Max: "); Serial.print(newPhase.MaxDurationS); + Serial.print("s, Ideal: "); Serial.print(newPhase.TargetDurationS); Serial.print("s"); + if (newPhase.AlarmOnExit) Serial.print(", alarm sounds on exit"); + Serial.println(); + + currentState.ActivePhase = phase; + currentState.EnteredCurrentPhase = millis(); + DisplayPhase(); + DisplayElapsedInPhase(true); + + if (oldPhase.AlarmOnExit) { + EnableBuzzer(ON); + } +} + +void AdvanceProfile(boolean silently) +{ + Serial.println("Advancing to next profile"); + + // select the next profile + currentState.SelectedProfile++; + if (currentState.SelectedProfile >= NUM_PROFILES) { + currentState.SelectedProfile = 0; + } + + // copy the phases to phase schedule + ReflowProfile newProfile = profiles[currentState.SelectedProfile]; + for (int phaseIx = 0; phaseIx < NUM_PHASES; phaseIx++) { + // first phase in schedule is always the "idle" phase, and last is always "cooling" + currentState.PhaseSchedule[phaseIx +1] = newProfile.Phases[phaseIx]; + } + + if (!silently) { + // update the UI + DisplayProfile(); + // remember which profile was last selected + EEPROM.write(ADDR_CURR_PROFILE, currentState.SelectedProfile); + } +} +