// --------- BLYNK STUFF ----------------
#define BLYNK_TEMPLATE_ID "TMPL3L7LEP3E4"
#define BLYNK_FIRMWARE_VERSION "1.0"
#define BLYNK_TEMPLATE_NAME "PTC Air Heater"
#define BLYNK_AUTH_TOKEN "AUTH_TOKEN"
char ssid[] = "Wokwi-GUEST";
char pass[] = "";
char auth[] = BLYNK_AUTH_TOKEN;
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <AutoPID.h>
#include <WiFi.h>
#include <BlynkSimpleEsp32_SSL.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define MAX_TEMP 75
#define MIN_TEMP 0
#define MAX_FIRING_TIME 7400 //(us) Time taken for a single side to complete is 10ms. 7400us was found to be best by someone?
// --------- PIN DEFINITIONS ------------
#define VatTH 34
#define HeaterTH 35
#define DetectorPin 16
#define ControllerPin 4
#define BuzzPin 5
#define EncClock 27 // EncClock pin connected to D27
#define EncData 26 // EncData pin connected to D26
#define EncSW 25 // EncSW button pin connected to D25
#define FanPin 33
unsigned long elapsedTime, Time = 0.0, timePrev;
int power_consumption = 0;
int old_power_consumption = power_consumption;
double set_temperature = 50; // Use this variable to store "steps"
double old_set_temperature = set_temperature;
double vat_temperature = 20;
double vat_old_temperature = vat_temperature;
double heater_temperature = 20;
double heater_old_temperature = heater_temperature;
int thermal_runaway = 0;
bool zero = 0;
bool longPress = false;
bool warned = false;
volatile int clkPinLast = LOW;
volatile int clkPinCurrent = LOW;
const float BETA = 3950; // should match the Beta Coefficient of the thermistor
static int8_t previous_temperature = 0; // Use this variable to store previous "steps" value
static int8_t rot_enc_table[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};
static uint16_t store = 0;
static bool edit_mode = true;
static bool old_edit_mode = edit_mode;
bool used_encoder = false;
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 500; // the debounce time; increase if the output flickers
unsigned long lastPressTime = 0;
unsigned long lastActionTime = 0;
// -------- PID STUFF -------------
const double kp = 100.0; // Heater Proportional constant
const double ki = 7.4; // Heater Integral constant
const double kd = 5.2; // Heater Derivative constant
double PID_value = 0;
// -------- BITMAP STUFF -----------
#pragma once
const unsigned char bitmap_vattray[] PROGMEM = {
B00000000, B00000000,
B00010001, B00010000,
B00100010, B00100000,
B00010001, B00010000,
B00001000, B10001000,
B00010001, B00010000,
B00100010, B00100000,
B00010001, B00010000,
B00001000, B10001000,
B00010001, B00010000,
B10100010, B00100001,
B10010001, B00010001,
B11001000, B10001011,
B01111111, B11111110,
B01111111, B11111110,
B00000000, B00000000};
const unsigned char bitmap_heater[] PROGMEM = {
B00000000, B00000000,
B01111111, B11111110,
B01000000, B00000010,
B01000000, B00000010,
B01111111, B11111110,
B01010101, B10101010,
B01111111, B11111110,
B01010101, B10101010,
B01111111, B11111110,
B01010101, B10101010,
B01111111, B11111110,
B01000000, B00000010,
B01010111, B11101010,
B01000000, B00000010,
B01111111, B11111110,
B00000000, B00000000};
const unsigned char bitmap_encoder_button[] PROGMEM = {
B00000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B00000000,
B00000000, B00001000, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00111110, B00000000, B00000000,
B00000000, B01111111, B00000000, B00000000,
B00000000, B11111111, B10000000, B00000000,
B00000001, B11111111, B11000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00011100, B00000000, B00000000,
B00000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B00000000,
B00111111, B11111111, B11111110, B00000000,
B00111111, B11111111, B11111110, B00000000,
B00000011, B11111111, B11100000, B00000000,
B00000000, B00000000, B00000000, B00000000};
const unsigned char bitmap_thermal[] PROGMEM = {
B00000000, B00000000, B00000111, B10000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000010, B11000000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000010, B01100000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000011, B00110000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000001, B00011000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000001, B00001000, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B10001100, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B10000100, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B10000010, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B10000010, B00100000, B00000000, B00000000,
B00000000, B00000000, B00000000, B10000010, B00111000, B00000000, B00000000,
B00000000, B00000000, B00010000, B10000011, B00101100, B00000000, B00000000,
B00000000, B00000000, B00110000, B10000001, B00110110, B00000000, B00000000,
B00000000, B00000000, B01110000, B10000001, B00010011, B00000000, B00000000,
B00000000, B00000001, B11010001, B10000001, B00010001, B00000000, B00000000,
B00000000, B00000011, B00110001, B00000001, B00010001, B00000000, B00000000,
B00000000, B00000110, B00100011, B00000001, B00010001, B00000000, B00000000,
B00000000, B00000100, B00100110, B00000001, B00110001, B10000000, B00000000,
B00000000, B00001100, B00110100, B00000001, B11100000, B10000000, B00000000,
B00000000, B00001000, B00010100, B00000000, B11000000, B10000000, B00000000,
B00000000, B00001000, B00011000, B00000000, B00000000, B10000000, B00000000,
B00000000, B00011000, B00001000, B00000000, B00000000, B10000000, B00000000,
B00000000, B00010000, B00000000, B00000000, B00000000, B11000000, B00000000,
B00000000, B00010000, B00000000, B00000000, B00000000, B01000000, B00000000,
B00000000, B00010000, B00000000, B00000000, B00000000, B01000000, B00000000,
B00000000, B00010000, B00000000, B00000000, B00000000, B01000000, B00000000,
B00000000, B00110000, B00000000, B00000000, B00000000, B01000000, B00000000,
B00000011, B00100000, B00000000, B00000000, B00000000, B11001000, B00000000,
B00000100, B10110000, B00000000, B00000000, B00000000, B10011100, B00000000,
B00000010, B10010000, B00000000, B00000000, B00000001, B00110100, B00000000,
B00000010, B01010000, B00000000, B00000000, B00000001, B01100100, B00000000,
B00000001, B01110000, B00000000, B00000000, B00000001, B11000100, B00000000,
B00000001, B00110000, B00000000, B00000000, B00000000, B00001100, B00000000,
B00000001, B10000000, B00000000, B00000000, B00000000, B00001000, B00000000,
B00000000, B10000000, B00000000, B00000000, B00000000, B00001000, B00000000,
B00000000, B10000000, B00000000, B00000000, B00000000, B00011000, B00000000,
B00000000, B10000000, B00000000, B00000000, B00000000, B01110000, B00000000,
B00000000, B11000000, B00000000, B00000000, B00000000, B01000000, B00000000,
B00000000, B01100000, B00000000, B00000000, B00000000, B11000000, B00000000,
B00000000, B00100000, B00000000, B00000000, B00000000, B10000000, B00000000,
B00000000, B00011000, B00000000, B00000000, B00000001, B00000000, B00000000,
B00000000, B00001100, B00000000, B00000000, B00000001, B00000000, B00000000,
B00000000, B00000110, B00000000, B00000000, B00000011, B00000000, B00000000,
B00000000, B00000011, B00000000, B00000000, B00001100, B00000000, B00000000,
B00000000, B00000001, B11000000, B00000000, B00010000, B00000000, B00000000,
B00000000, B00000000, B01000000, B00000000, B00110000, B00001100, B00000000,
B00011000, B00000000, B01110000, B00000000, B11100000, B00001100, B00000000,
B00011000, B00000000, B00001100, B00000011, B00000000, B00001100, B00000000,
B00011111, B11111111, B11111111, B11111111, B11111111, B11111100, B00000000,
B00011111, B11111111, B11111111, B11111111, B11111111, B11111100, B00000000};
// ---------- OLED DISPLAY OBJECT ---------------
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// ---------- AUTO PID OBJECT -------------------
AutoPID vatPID(&vat_temperature, &set_temperature, &PID_value, 0, 7400, kp, ki, kd); // AutoPID vatPID(&vat_temperature, &PID_value, &set_temperature, kp, ki, kd, DIRECT);
// ----------- BLYNK TIMER OBJECT --------------
BlynkTimer timer;
// ----------- BLYNK LOGIC ---------------------
BLYNK_CONNECTED()
{
Blynk.syncAll();
Blynk.virtualWrite(V3, "\n\n⚝Connected to Blynk Server!\n ⤷Firmware Version: " + String(BLYNK_FIRMWARE_VERSION) + "\n");
}
BLYNK_WRITE(V2)
{
lastActionTime = millis();
int value = param.asInt();
if (edit_mode)
{
if (value >= MIN_TEMP && value <= MAX_TEMP)
set_temperature = value;
else
Blynk.virtualWrite(V2, set_temperature);
}
return;
}
BLYNK_WRITE(V5)
{
lastActionTime = millis();
int value = param.asInt();
analogWrite(FanPin, (int)(255 * (float)value / 100.0));
return;
}
BLYNK_WRITE(V4)
{
lastActionTime = millis();
int value = param.asInt();
if (value)
edit_mode = false;
else
edit_mode = true;
return;
}
BLYNK_WRITE(InternalPinDBG)
{
if (String(param.asStr()) == "reboot")
{
Blynk.virtualWrite(V3, "⟲ Starting reboot sequence...");
reboot();
}
}
void sendSensor()
{
Blynk.virtualWrite(V0, vat_temperature);
Blynk.virtualWrite(V1, heater_temperature);
}
// ---------- ESP SPECIFIC FUNCTION -----------
void reboot()
{
digitalWrite(ControllerPin, LOW);
digitalWrite(BuzzPin, LOW);
digitalWrite(FanPin, LOW);
ESP.restart();
while (1)
;
;
}
// ---------------- SETUP -----------------------
void setup()
{
Serial.begin(115200);
while (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))
Serial.println("SSD1306 allocation failed");
display.clearDisplay();
delay(500);
display.setTextSize(1);
display.setTextColor(WHITE);
pinMode(EncClock, INPUT_PULLUP);
pinMode(EncData, INPUT_PULLUP);
pinMode(EncSW, INPUT_PULLUP);
if (digitalRead(EncSW) == LOW)
{
display.setCursor(0, 0);
display.println("Air Heater Controller");
display.drawFastHLine(0, 10, SCREEN_WIDTH, WHITE);
display.setCursor(4, 20);
display.println("Release Knob Button!");
display.drawBitmap((SCREEN_WIDTH - 25) / 2, 35, bitmap_encoder_button, 25, 25, WHITE);
display.display();
}
while (digitalRead(EncSW) == LOW)
{
; // Don't proceed, loop forever
}
display.clearDisplay();
display.setCursor(0, 0);
display.println("Air Heater Controller");
display.drawFastHLine(0, 10, SCREEN_WIDTH, WHITE);
display.display();
pinMode(DetectorPin, INPUT_PULLUP);
pinMode(ControllerPin, OUTPUT);
pinMode(BuzzPin, OUTPUT);
pinMode(FanPin, OUTPUT);
analogWrite(FanPin, 255 * 0.75); // 75% Speed
digitalWrite(ControllerPin, LOW);
attachInterrupt(digitalPinToInterrupt(DetectorPin), zerocrossing, FALLING); // activate external interrupt on pin 2 at a falling edge
attachInterrupt(digitalPinToInterrupt(EncClock), shaft_moved, CHANGE);
attachInterrupt(digitalPinToInterrupt(EncSW), shaft_pressed, CHANGE);
vat_temperature = temperature(analogRead(VatTH));
heater_temperature = temperature(analogRead(HeaterTH));
sei();
draw_frame();
draw_power_frame();
draw_edit_mode_frame();
vatPID.setTimeStep(100);
Blynk.begin(auth, ssid, pass);
timer.setInterval(1000L, sendSensor);
}
double temperature(int temp) // Converts Analog value from Thermistor to Temperature in Celsius. Derived from Steinhart–Hart equation.
{
temp = 1 / (log(1 / (4096. / temp - 1)) / BETA + 1.0 / 298.15) - 273.15;
return temp;
}
// ---------- MAIN LOOP -----------------
void loop()
{
Blynk.run();
timer.run();
vat_temperature = temperature(analogRead(VatTH));
heater_temperature = temperature(analogRead(HeaterTH));
power_consumption = PID_value * 100 / 7400;
if (!edit_mode && !thermal_runaway)
vatPID.run();
else if (thermal_runaway && !warned)
{
switch (thermal_runaway)
{
case 1:
vatPID.reset();
vatPID.stop();
PID_value = 0;
break; // When Heater Temperature exceeds MAX_TEMP, May happen a lot and only needs power clamping.
case 2:
vatPID.reset();
vatPID.stop();
PID_value = 0;
digitalWrite(BuzzPin, HIGH);
draw_thermal_runaway();
break; // When the PID reaches max value and Heater exceeds MAX_TEMP but the Vat Temperature is not changing by the order it should.
case 3:
vatPID.reset();
vatPID.stop();
PID_value = 0;
draw_thermal_runaway();
digitalWrite(BuzzPin, HIGH);
; // When the PID reaches a threshold value but Heater Thermistor and Vat Thermistor show not changes.
break;
}
Blynk.virtualWrite(V6, thermal_runaway);
}
else
{
vatPID.reset();
vatPID.stop();
PID_value = 0;
}
if (heater_temperature > MAX_TEMP)
thermal_runaway = 1;
if (heater_temperature > MAX_TEMP and PID_value >= 7000)
thermal_runaway = 2;
if ((heater_temperature - vat_temperature) < 30 and PID_value >= 7000)
thermal_runaway = 3;
if (thermal_runaway == 1 && heater_temperature < MAX_TEMP)
thermal_runaway = 0;
if (old_power_consumption != power_consumption && thermal_runaway <= 1)
{
old_power_consumption = power_consumption;
draw_power_frame();
}
if ((set_temperature != old_set_temperature || edit_mode != old_edit_mode) && thermal_runaway <= 1)
{
old_edit_mode = edit_mode;
old_set_temperature = set_temperature;
draw_edit_mode_frame();
}
if ((vat_temperature != vat_old_temperature || heater_temperature != heater_old_temperature) && thermal_runaway <= 1)
{
vat_old_temperature = vat_temperature;
heater_old_temperature = heater_temperature;
draw_frame();
}
if (digitalRead(EncSW) == LOW && (millis() - lastPressTime >= 3000) && thermal_runaway > 1)
{
reboot();
}
if (used_encoder){
Blynk.virtualWrite(V2, set_temperature);
Blynk.virtualWrite(V4, !edit_mode);
used_encoder = false;
}
display_saver();
}
// ------------ ZCD INTERRUPT HANDLER ---------------
void zerocrossing()
{
int trigger_time = MAX_FIRING_TIME - PID_value;
if (!edit_mode && thermal_runaway <= 1)
{
delayMicroseconds(trigger_time);
digitalWrite(ControllerPin, HIGH);
delayMicroseconds(100);
digitalWrite(ControllerPin, LOW);
}
else
{
digitalWrite(ControllerPin, LOW);
}
}
// ----------- DISPLAY FUNCTIONS -------------------
void display_saver()
{
if (millis() - lastActionTime >= 60000)
{
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(0);
display.ssd1306_command(SSD1306_DISPLAYOFF);
}
else if (millis() - lastActionTime >= 10000)
{
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
}
else
{
display.ssd1306_command(SSD1306_DISPLAYON);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(255);
}
}
void draw_thermal_runaway()
{
clear_frame();
clear_power_frame();
clear_edit_mode_frame();
display.drawBitmap((SCREEN_WIDTH - 50) / 2, 14, bitmap_thermal, 50, 50, WHITE);
display.setCursor((SCREEN_WIDTH - 15) / 2, 45);
display.print("TR" + String(thermal_runaway));
display.display();
warned = true;
}
void clear_power_frame()
{
display.drawRect(0, 56, SCREEN_WIDTH, SCREEN_HEIGHT - 56, BLACK);
display.fillRect(0, 56, SCREEN_WIDTH, SCREEN_HEIGHT - 56, BLACK);
}
void draw_power_frame()
{
clear_power_frame();
display.setCursor(SCREEN_WIDTH - 27, 56);
if (power_consumption > 100)
power_consumption = 100;
if (power_consumption < 0)
power_consumption = 0;
display.print(power_consumption);
display.println("%");
display.drawFastVLine(0, 59, 3, WHITE);
display.drawFastVLine(SCREEN_WIDTH - 30, 59, 3, WHITE);
display.drawFastHLine(1, 60, (power_consumption * (SCREEN_WIDTH - 31) / 100), WHITE);
display.display();
}
void clear_edit_mode_frame()
{
display.drawRect(0, 40, SCREEN_WIDTH, SCREEN_HEIGHT - 56, BLACK);
display.fillRect(0, 40, SCREEN_WIDTH, SCREEN_HEIGHT - 56, BLACK);
}
void draw_edit_mode_frame()
{
clear_edit_mode_frame();
display.setCursor(0, 40);
if (edit_mode)
{
display.print("Edit Set Temp. ");
display.print(String((int)set_temperature) + " ");
}
else
display.print("Set Temp. " + String(set_temperature) + " ");
display.print((char)247);
display.println("C");
display.display();
}
void clear_frame()
{
display.drawRect(0, 11, SCREEN_WIDTH, SCREEN_HEIGHT - 40, BLACK);
display.fillRect(0, 11, SCREEN_WIDTH, SCREEN_HEIGHT - 40, BLACK);
}
void draw_frame()
{
clear_frame();
display.drawBitmap(2, 19, bitmap_vattray, 16, 16, WHITE);
display.setCursor(25, 22);
display.print(String((int)vat_temperature) + " ");
display.print((char)247);
display.println("C");
display.drawBitmap(64, 19, bitmap_heater, 16, 16, WHITE);
display.setCursor(87, 22);
display.print(String((int)heater_temperature) + " ");
display.print((char)247);
display.println("C");
display.display();
}
// ------------- ENCODER FUNCTIONS ----------------
void shaft_pressed()
{
lastActionTime = millis();
if (digitalRead(EncSW) == LOW && (millis() - lastDebounceTime >= debounceDelay))
{
edit_mode = !edit_mode;
lastDebounceTime = millis();
lastPressTime = millis();
used_encoder = true;
}
}
void shaft_moved()
{
lastActionTime = millis();
clkPinCurrent = digitalRead(EncClock);
if ((clkPinLast == LOW) && (clkPinCurrent == HIGH) && edit_mode)
{
if (digitalRead(EncData) == HIGH && set_temperature > MIN_TEMP)
{
set_temperature--;
}
if (digitalRead(EncData) == LOW && set_temperature < MAX_TEMP)
{
set_temperature++;
}
used_encoder = true;
}
clkPinLast = clkPinCurrent;
}