/*
    Reflow Controller v1.31
    Modify: Steve Barth
    Date: 12-02-2022

  Brief
  The Reflow Controller is an ATMEGA328 microcontroller
  based system that automates your heater plan to reflow your DIY SMT PCB projects.
  With two profiles stored, Pb and Lead-Free solder paste.
  The firmware was modified from the original Rocketscream Electronics Tiny Reflow controller code.

   Required Libraries
  - Arduino PID Library:
    >> https://github.com/br3ttb/Arduino-PID-Library
  - Adafruit MAX6675 Library:
    >> https://github.com/adafruit/Adafruit_MAX6675
  - Adafruit SSD1306 Library:
    >> https://github.com/adafruit/Adafruit_SSD1306
  - Adafruit GFX Library:
    >> https://github.com/adafruit/Adafruit-GFX-Library

     Initial release:
        - Arduino nano or pro mini (on ATMega328 5V, 16MHz),
        - Oled display (SSD1306 128x64),
        - MAX6675 with K-type thermosensor (0-1024'C),
        - Optopoupler (MOC3041) and triac (BTA08-600),
        - Fan for cooling (depending on the power supply, 5-12V)

     Memory Usage:
            RAM:   [====      ]  41.2% (used   844 bytes from  2048 bytes)
            Flash: [=======   ]  71.1% (used 21844 bytes from 30720 bytes)

*******************************************************************************/

////**** INCLUDES
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <EEPROM.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PID_v1.h>
#include <max6675.h>


////**** STATE TYPE DEFINITIONS
typedef enum REFLOW_STATE
{
  REFLOW_STATE_IDLE,                // orange
  REFLOW_STATE_PREHEAT,             // yellow
  REFLOW_STATE_SOAK,                // green
  REFLOW_STATE_REFLOW,              // bright green
  REFLOW_STATE_COOL,                // blue
  REFLOW_STATE_COMPLETE,            // indigo
  REFLOW_STATE_TOO_HOT,             // violet
  REFLOW_STATE_ERROR                // red
} reflowState_t;

typedef enum REFLOW_STATUS
{
  REFLOW_STATUS_OFF,
  REFLOW_STATUS_ON
} reflowStatus_t;

typedef enum SWITCH
{
  SWITCH_NONE,
  SWITCH_1,
  SWITCH_2
} switch_t;

typedef enum DEBOUNCE_STATE
{
  DEBOUNCE_STATE_IDLE,
  DEBOUNCE_STATE_CHECK,
  DEBOUNCE_STATE_RELEASE
} debounceState_t;

typedef enum REFLOW_PROFILE
{
  REFLOW_PROFILE_LEADFREE,
  REFLOW_PROFILE_LEADED
} reflowProfile_t;

//// **** GENERAL PROFILE CONSTANTS
#define PROFILE_TYPE_ADDRESS 0
#define TEMPERATURE_ROOM 50
#define TEMPERATURE_SOAK_MIN 150
#define TEMPERATURE_COOL_MIN 100
#define SENSOR_SAMPLING_TIME 1000
#define SOAK_TEMPERATURE_STEP 5

//// **** LEAD FREE PROFILE CONSTANTS
#define TEMPERATURE_SOAK_MAX_LF 200
#define TEMPERATURE_REFLOW_MAX_LF 250
#define SOAK_MICRO_PERIOD_LF 9000

//// **** LEADED PROFILE CONSTANTS
#define TEMPERATURE_SOAK_MAX_PB 180
#define TEMPERATURE_REFLOW_MAX_PB 224
#define SOAK_MICRO_PERIOD_PB 10000

//// **** SWITCH SPECIFIC CONSTANTS
#define DEBOUNCE_PERIOD_MIN 100

//// **** DISPLAY SPECIFIC CONSTANTS
#define UPDATE_RATE 100
#define SCREEN_WIDTH 128            // oled display width, in pixels
#define SCREEN_HEIGHT 64            // oled display height, in pixels
#define X_AXIS_START 18             // X-axis starting position

//!      *****  PID PARAMETERS  *****

//// **** PRE-HEAT STAGE
#define PID_KP_PREHEAT 100
#define PID_KI_PREHEAT 0.025
#define PID_KD_PREHEAT 20

//// **** SOAKING STAGE
#define PID_KP_SOAK 300
#define PID_KI_SOAK 0.05
#define PID_KD_SOAK 250

//// **** REFLOW STAGE
#define PID_KP_REFLOW 300
#define PID_KI_REFLOW 0.05
#define PID_KD_REFLOW 350
#define PID_SAMPLE_TIME 1000

//// **** LCD MESSAGES
const char *lcdMessagesReflowStatus[] = {"Ready", "PreHeat", "Soak", "Reflow", "Cool", "Done!", "Hot!", "Error"};

//// **** DEGREE SYMBOL FOR LCD
unsigned char degree[8] = {140, 146, 146, 140, 128, 128, 128, 128};

//// **** PIN ASSIGNMENT

unsigned char Start_Stop = A0;
unsigned char Lf_Pb = A2;
#define MAXCS 2
#define MAXCLK 3
#define MAXDO 4
unsigned char Buzzer = 6;
unsigned char Fan = 7;                // Fan sw on/off
#define Led 8 // A6
unsigned char Triac = 9;
bool coolingOn = false;            // Fan status enabled or not
double Fan_timer = 0;              // time lenght for the on period for Fan

//#define oled_MOSI 11
//#define oled_CLK 13
//#define oled_DC 10
//#define oled_CS 8
#define oled_RESET 12


// ***** PID CONTROL VARIABLES *****
double setpoint;
double input;
double output;
double kp = PID_KP_PREHEAT;
double ki = PID_KI_PREHEAT;
double kd = PID_KD_PREHEAT;
unsigned long windowSize;
unsigned long windowStartTime;
unsigned long nextCheck;
unsigned long nextRead;
unsigned long updateLcd;
unsigned long timerSoak;
unsigned long buzzerPeriod;
unsigned char soakTemperatureMax;
unsigned char reflowTemperatureMax;
unsigned long soakMicroPeriod;

//* Reflow oven controller state machine state variable
reflowState_t reflowState;

//* Reflow oven controller status
reflowStatus_t reflowStatus;

//* Reflow profile type
reflowProfile_t reflowProfile;

//* Switch debounce state machine state variable
debounceState_t debounceState;

//* Switch debounce timer
long lastDebounceTime;

//* Switch press status
switch_t switchStatus;
switch_t switchValue;
switch_t switchMask;

//* Seconds timer
unsigned int timerSeconds;

//* thermosensor fault status
unsigned char fault;
unsigned int timerUpdate;
unsigned char temperature[SCREEN_WIDTH - X_AXIS_START];
unsigned char x;

//* PID control interface
PID reflowOvenPID(&input, &output, &setpoint, kp, ki, kd, DIRECT);

//* OLED display SETUP
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, oled_RESET);

//* MAX6675 thermosensor interface           Adafruit_MAX31855 thermosensor(MAXCLK, MAXCS, MAXDO);
MAX6675 thermosensor(MAXCLK, MAXCS, MAXDO);

switch_t readSwitch(void)
{
  //* Checking if either the START/STOP or CHANGE PROFILE Pins were selected
  if (digitalRead(Start_Stop) == LOW)
    return SWITCH_1;
  if (digitalRead(Lf_Pb) == LOW)
    return SWITCH_2;
  if (!(digitalRead(Lf_Pb) == LOW) || !(digitalRead(Lf_Pb) == LOW))
    return SWITCH_NONE;
}

void setup()
{
  //* Check current selected reflow profile
  unsigned char value = EEPROM.read(PROFILE_TYPE_ADDRESS);
  if ((value == 0) || (value == 1))
  {
    //* Valid reflow profile value
    reflowProfile = value;
    //___ (sqiggle?!) invalid conversion from 'unsigned char' to 'reflowProfile_t {aka REFLOW_PROFILE}' [-fpermissive]
  }
  else
  {
    //* Default to Lead-Free profile
    /*EEPROM.write(PROFILE_TYPE_ADDRESS, 0);
      reflowProfile = REFLOW_PROFILE_LEADFREE;*/

    //* Default to Pb profile
    reflowProfile = REFLOW_PROFILE_LEADED;
    EEPROM.write(PROFILE_TYPE_ADDRESS, 1);
  }

  pinMode(Start_Stop, INPUT_PULLUP);
  pinMode(Lf_Pb, INPUT_PULLUP);
  pinMode(Fan, OUTPUT);
  digitalWrite(Fan, LOW);

  //* triac pin initialization to ensure reflow oven is off
  pinMode(Triac, OUTPUT);
  digitalWrite(Triac, LOW);

  //* Buzzer pin initialization to ensure annoying buzzer is off
  pinMode(Buzzer, OUTPUT);
  digitalWrite(Buzzer, LOW);

  //* LED pins initialization and turn on upon start-up (active high)
  pinMode(Led, OUTPUT);               //
  digitalWrite(Led, HIGH);            //

  //* Start-up splash
  digitalWrite(Buzzer, HIGH);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3D);
  oled.display();
  digitalWrite(Buzzer, LOW);
  delay(200);
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setTextColor(WHITE);
  oled.setCursor(0, 4);
  oled.println(F("        Reflow"));
  oled.println(F("      Controller"));
  oled.println(F("        v 1.31"));
  delay(1500);
  oled.println();
  oled.println(F("    by Steve Barth"));
  oled.println();
  oled.println(F("      2022.dec.2."));
  oled.display();
  delay(2500);
  oled.clearDisplay();

  //* Serial communication at 115200 bps
  Serial.begin(115200);

  //* Turn off LED (active high)
  digitalWrite(Led, LOW);

  //* Set window size
  windowSize = 2000;

  //* Initialize time keeping variable
  nextCheck = millis();

  //* Initialize thermosensor reading variable
  nextRead = millis();

  //* Initialize LCD update timer
  updateLcd = millis();
}

void loop()
{
  //* Current time
  unsigned long currentTime;

  //? Time to read thermosensor?
  if (millis() > nextRead)
  {
    //* Read thermosensor next sampling period
    nextRead += SENSOR_SAMPLING_TIME;
    //* Read current temperature
    input = thermosensor.readCelsius();

    //* If any thermosensor fault is detected
    if (isnan(input))
    {
      reflowState = REFLOW_STATE_ERROR;
      reflowStatus = REFLOW_STATUS_OFF;
      Serial.println(F("Error"));
    }
  }

  if (millis() > nextCheck)
  {
    //* Check input in the next seconds
    nextCheck += SENSOR_SAMPLING_TIME;
    //* If reflow process is on going
    if (reflowStatus == REFLOW_STATUS_ON)
    {
      //* Toggle red LED as system heart beat
      digitalWrite(Led, !(digitalRead(Led)));
      //* Increase seconds timer for reflow curve plot
      timerSeconds++;
      //* Send temperature and time stamp to serial
      Serial.print(timerSeconds);
      Serial.print(F("s, "));
      Serial.print(setpoint);
      Serial.print(F(", "));
      Serial.print(input);
      Serial.print(F("C, "));
      Serial.println(output);
    }
    else
    {
      //* Turn off red LED
      digitalWrite(Led, LOW);
    }
  }

  if (millis() > updateLcd)
  {
    //* Update LCD in the next 100 ms
    updateLcd += UPDATE_RATE;

    oled.clearDisplay();
    oled.setTextSize(2);
    oled.setCursor(0, 0);
    oled.print(lcdMessagesReflowStatus[reflowState]);
    oled.setTextSize(1);
    oled.setCursor(115, 0);

    if (reflowProfile == REFLOW_PROFILE_LEADFREE)
    {
      oled.print(F("LF"));
    }
    else
    {
      oled.print(F("PB"));
    }

    //* Temperature markers
    oled.setCursor(0, 18);
    oled.print(F("250"));
    oled.setCursor(0, 36);
    oled.print(F("150"));
    oled.setCursor(0, 54);
    oled.print(F("50"));
    //*  Draw temperature and time axis on monochrome oled
    oled.drawLine(18, 18, 18, 63, WHITE);
    oled.drawLine(18, 63, 127, 63, WHITE);
    oled.setCursor(115, 0);

    //* If currently in error state
    if (reflowState == REFLOW_STATE_ERROR)
    {
      oled.setCursor(80, 9);
      oled.print(F("TS Error"));
    }
    else
    {
      //* Right align temperature reading
      if (input < 10)
        oled.setCursor(91, 9);
      else if (input < 100)
        oled.setCursor(85, 9);
      else
        oled.setCursor(80, 9);
      // oled current temperature
      oled.print(input);
      oled.print((char)247);
      oled.print(F("'C"));
    }

    if (reflowStatus == REFLOW_STATUS_ON)
    {
      //* We are updating the display faster than sensor reading
      if (timerSeconds > timerUpdate)
      {
        //* Store temperature reading every 3 s
        if ((timerSeconds % 3) == 0)
        {
          timerUpdate = timerSeconds;
          unsigned char averageReading = map(input, 0, 250, 63, 19);
          if (x < (SCREEN_WIDTH - X_AXIS_START))
          {
            temperature[x++] = averageReading;
          }
        }
      }
    }

    unsigned char timeAxis;
    for (timeAxis = 0; timeAxis < x; timeAxis++)
    {
      oled.drawPixel(timeAxis + X_AXIS_START, temperature[timeAxis], WHITE);
    }

    //* Update screen
    oled.display();
  }

  //* Reflow oven controller state machine
  switch (reflowState)
  {
    case REFLOW_STATE_IDLE:
      //* If oven temperature is still above room temperature
      if (input >= TEMPERATURE_ROOM)
      {
        reflowState = REFLOW_STATE_TOO_HOT;
      }
      else
      {
        //* If switch is pressed to start reflow process
        if (switchStatus == SWITCH_1)
        {
          //* Send header for CSV file
          Serial.println(F("Time, Setpoint, Input, Output"));
          //* Intialize seconds timer for serial debug information
          timerSeconds = 0;

          //* Initialize reflow plot update timer
          timerUpdate = 0;
          for (x = 0; x < (SCREEN_WIDTH - X_AXIS_START); x++)
          {
            temperature[x] = 0;
          }
          //* Initialize index for average temperature array used for reflow plot
          x = 0;

          //* Initialize PID control window starting time
          windowStartTime = millis();
          //* Ramp up to minimum soaking temperature
          setpoint = TEMPERATURE_SOAK_MIN;
          //* Load profile specific constant
          if (reflowProfile == REFLOW_PROFILE_LEADFREE)
          {
            soakTemperatureMax = TEMPERATURE_SOAK_MAX_LF;
            reflowTemperatureMax = TEMPERATURE_REFLOW_MAX_LF;
            soakMicroPeriod = SOAK_MICRO_PERIOD_LF;
          }
          else
          {
            soakTemperatureMax = TEMPERATURE_SOAK_MAX_PB;
            reflowTemperatureMax = TEMPERATURE_REFLOW_MAX_PB;
            soakMicroPeriod = SOAK_MICRO_PERIOD_PB;
          }
          //* Tell the PID to range between 0 and the full window size
          reflowOvenPID.SetOutputLimits(0, windowSize);
          reflowOvenPID.SetSampleTime(PID_SAMPLE_TIME);
          //* Turn the PID on
          reflowOvenPID.SetMode(AUTOMATIC);
          //* Proceed to preheat stage
          reflowState = REFLOW_STATE_PREHEAT;
        }
      }
      break;

    case REFLOW_STATE_PREHEAT:
      reflowStatus = REFLOW_STATUS_ON;

      //* If minimum soak temperature is achieve
      if (input >= TEMPERATURE_SOAK_MIN)
      {
        //* Chop soaking period into smaller sub-period
        timerSoak = millis() + soakMicroPeriod;
        //* Set less agressive PID parameters for soaking ramp
        reflowOvenPID.SetTunings(PID_KP_SOAK, PID_KI_SOAK, PID_KD_SOAK);
        //* Ramp up to first section of soaking temperature
        setpoint = TEMPERATURE_SOAK_MIN + SOAK_TEMPERATURE_STEP;
        //* Proceed to soaking state
        reflowState = REFLOW_STATE_SOAK;
      }
      break;

    case REFLOW_STATE_SOAK:

      //* If micro soak temperature is achieved
      if (millis() > timerSoak)
      {
        timerSoak = millis() + soakMicroPeriod;
        //* Increment micro setpoint
        setpoint += SOAK_TEMPERATURE_STEP;
        if (setpoint > soakTemperatureMax)
        {
          //* Set agressive PID parameters for reflow ramp
          reflowOvenPID.SetTunings(PID_KP_REFLOW, PID_KI_REFLOW, PID_KD_REFLOW);
          //* Ramp up to first section of soaking temperature
          setpoint = reflowTemperatureMax;
          //* Proceed to reflowing state
          reflowState = REFLOW_STATE_REFLOW;
        }
      }
      break;

    case REFLOW_STATE_REFLOW:

      //* We need to avoid hovering at peak temperature for too long
      //* Crude method that works like a charm and safe for the components
      if (input >= (reflowTemperatureMax - 5))
      {
        //* Set PID parameters for cooling ramp
        reflowOvenPID.SetTunings(PID_KP_REFLOW, PID_KI_REFLOW, PID_KD_REFLOW);
        //* Ramp down to minimum cooling temperature
        setpoint = TEMPERATURE_COOL_MIN;
        //* Proceed to cooling state
        reflowState = REFLOW_STATE_COOL;
      }
      break;

    case REFLOW_STATE_COOL:

      //* If minimum cool temperature is achieved
      if (input <= TEMPERATURE_COOL_MIN)
      {
        //* Retrieve current time for buzzer usage
        buzzerPeriod = millis() + 1000;
        //* Turn on buzzer to indicate completion
        digitalWrite(Buzzer, HIGH);
        //* Turn off reflow process
        reflowStatus = REFLOW_STATUS_OFF;

coolingOn = true;
digitalWrite(Fan, HIGH);
Fan_timer = millis();
  if (coolingOn == true && millis() - Fan_timer > 90000)
  {
  digitalWrite(Fan, LOW);            // turn off the Fan after 90s
  coolingOn = false;
  }

        //* Proceed to reflow Completion state
        reflowState = REFLOW_STATE_COMPLETE;
      }
      break;

    case REFLOW_STATE_COMPLETE:

      //* play buzzer for buzzerperiod
      if (millis() > buzzerPeriod)
      {
        //* Turn off buzzer
        digitalWrite(Buzzer, LOW);
        //* Reflow process ended
        reflowState = REFLOW_STATE_IDLE;
      }
      break;

    case REFLOW_STATE_TOO_HOT:

      //* If oven temperature drops below room temperature
      if (input < TEMPERATURE_ROOM)
      {
        //* Ready to reflow
        reflowState = REFLOW_STATE_IDLE;
      }
      break;

    case REFLOW_STATE_ERROR:

      //* Check for thermosensor fault
      double c = thermosensor.readCelsius();

      //* If thermosensor problem is still present - kinked, or broken, or not sitting in the terminal block correctly.
      if (isnan(c))
      {
        //* Wait until thermosensor wire is re-connected
        reflowState = REFLOW_STATE_ERROR;
      }
      else
      {
        //* Clear to perform reflow process
        reflowState = REFLOW_STATE_IDLE;
      }
      break;
  }

  //* If switch 1 is pressed
  if (switchStatus == SWITCH_1)
  {
    //* If currently reflow process is ongoing
    if (reflowStatus == REFLOW_STATUS_ON)
    {
      //* Button press is for cancelling
      //* Turn off reflow process NOW
      reflowStatus = REFLOW_STATUS_OFF;
      //* Reinitialize state machine
      reflowState = REFLOW_STATE_IDLE;
    }
  }

  //* Switch 2 is pressed
  else if (switchStatus == SWITCH_2)
  {
    //* Only can switch reflow profile during idle
    if (reflowState == REFLOW_STATE_IDLE)
    {
      //* Currently using lead-free reflow profile
      if (reflowProfile == REFLOW_PROFILE_LEADFREE)
      {
        //* Switch to leaded reflow profile
        reflowProfile = REFLOW_PROFILE_LEADED;
        EEPROM.write(PROFILE_TYPE_ADDRESS, 1);
      }
      //* Currently using leaded reflow profile
      else
      {
        //* Switch to lead-free profile
        reflowProfile = REFLOW_PROFILE_LEADFREE;
        EEPROM.write(PROFILE_TYPE_ADDRESS, 0);
      }
    }
  }

  //* Switch status has been read
  switchStatus = SWITCH_NONE;

  //* Switch debounce state machine (analog switch)
  switch (debounceState)
  {
    case DEBOUNCE_STATE_IDLE:
      //* No valid switch press
      switchStatus = SWITCH_NONE;
      switchValue = readSwitch();

      //* If either switch is pressed
      if (switchValue != SWITCH_NONE)
      {
        //* Note the pressed switch
        switchMask = switchValue;
        //* Intialize debounce counter
        lastDebounceTime = millis();
        //* Proceed to check validity of button press
        debounceState = DEBOUNCE_STATE_CHECK;
      }
      break;

    case DEBOUNCE_STATE_CHECK:
      switchValue = readSwitch();
      if (switchValue == switchMask)
      {
        //* If minimum debounce period is complete
        if ((millis() - lastDebounceTime) > DEBOUNCE_PERIOD_MIN)
        {
          //* Valid switch press
          switchStatus = switchMask;
          //* Wait for button release
          debounceState = DEBOUNCE_STATE_RELEASE;
        }
      }

      //* A False trigger Dettected
      else
      {
        //* Reinitialize button debounce state machine
        debounceState = DEBOUNCE_STATE_IDLE;
      }
      break;

    case DEBOUNCE_STATE_RELEASE:
      switchValue = readSwitch();
      if (switchValue == SWITCH_NONE)
      {
        //* Reinitialize button debounce state machine when no switches are pressed
        debounceState = DEBOUNCE_STATE_IDLE;
      }
      break;
  }

  //* PID computation and appropriate triac control
  if (reflowStatus == REFLOW_STATUS_ON)
  {
    currentTime = millis();
    reflowOvenPID.Compute();
    if ((currentTime - windowStartTime) > windowSize)
    {
      //* Time to shift the Relay Window
      windowStartTime += windowSize;
    }
    if (output > (currentTime - windowStartTime))

      digitalWrite(Triac, HIGH);

    else
      digitalWrite(Triac, LOW);
  }

  //* Reflow oven process is off, ensure oven is off
  else
  {
    digitalWrite(Triac, LOW);
  }
}

/*
    coolingOn = true;
    digitalWrite(Fan, HIGH);
    Fan_timer = millis();
     if (coolingOn == true && (millis() - Fan_timer) > 4000)
    {
      digitalWrite(Fan, LOW);            // turn off the Fan after 90s
      coolingOn = false;
    }
*/