Here's a link to the similar touchscreen:
https://www.adafruit.com/product/1770
I used Claude 4.6 to resize the buttons and graphics on the screen. It actually seemed to be a lot of changes. Unfortunately, the code and libraries were too large to fit onto the flash memory of an Arduinio UNO (or at least the model I have) so I got rid of the custom fonts and actually got rid of the curves on the displayed reflow profile (replaced with straight lines), but only for the display not for the actual PID part. Despite all the changes this code 100% works! I know this is hacky but I dont know github that well so im just going to paste the ino file after this and maybe someone who knows the whole push/fork/commit thing can do it if they care to. Email me at gkratm@gmail.com with any questions/concerns. Cheers!
#define THERMO_CS 8
#define SSR_PIN 2
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST -1 // RST can be set to -1 if you tie it to Arduino's reset
// Note the X and Y pin numbers are opposite from what is printed on the TFT display. This was done to align with the screen rotation.
#define YP A0 // must be an analog pin, use "An" notation!
#define XM A1 // must be an analog pin, use "An" notation!
#define YM 7 // can be a digital pin
#define XP 6 // can be a digital pin
// Touchscreen calibration for Adafruit 1770 (ILI9341 2.8" resistive)
// Raw X axis maps to pixel Y, Raw Y axis maps to pixel X (landscape rotation 1)
#define TS_MINX 155
#define TS_MINY 115
#define TS_MAXX 865
#define TS_MAXY 890
#include <PID_v1.h>
#include <Adafruit_MAX31856.h>
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <stdint.h>
#include "TouchScreen.h"
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
const int displayWidth = 320, displayHeight = 240;
const int gridSize = 53; // 6 cols x 53 = 318, 4 rows x 53 = 212 (small margins on 320x240)
// For better pressure precision, we need to know the resistance
// between X+ and X- Use any multimeter to read it
// For the one we're using, its 300 ohms across the X plate
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
TSPoint touchpoint;
void readTouch() {
touchpoint = ts.getPoint();
// Restore pin modes so TFT SPI works after touch read
pinMode(XM, OUTPUT);
pinMode(YP, OUTPUT);
digitalWrite(XM, LOW);
digitalWrite(YP, HIGH);
}
bool setupMenu = false, editMenu = false, reflowMenu = false;
const int touchHoldLimit = 500;
// use hardware SPI, just pass in the CS pin
Adafruit_MAX31856 maxthermo = Adafruit_MAX31856(THERMO_CS);
unsigned long timeSinceReflowStarted;
unsigned long timeTempCheck = 1000;
unsigned long lastTimeTempCheck = 0;
double preheatTemp = 180, soakTemp = 150, reflowTemp = 230, cooldownTemp = 25;
unsigned long preheatTime = 120000, soakTime = 60000, reflowTime = 120000, cooldownTime = 120000, totalTime = preheatTime + soakTime + reflowTime + cooldownTime;
bool preheating = false, soaking = false, reflowing = false, coolingDown = false, newState = false;
uint16_t gridColor = 0x7BEF;
uint16_t preheatColor = ILI9341_RED, soakColor = 0xFBE0, reflowColor = 0xDEE0, cooldownColor = ILI9341_BLUE; // colors for plotting
uint16_t preheatColor_d = 0xC000, soakColor_d = 0xC2E0, reflowColor_d = 0xC600, cooldownColor_d = 0x0018; // desaturated colors
// Define Variables we'll be connecting to
double Setpoint, Input, Output;
// Specify the links and initial tuning parameters
double Kp=2, Ki=5, Kd=1;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
void setup() {
Serial.begin(115200);
while (!Serial)
delay(10);
Serial.println(F("Solder Reflow Oven"));
delay(100);
// Ensure both SPI CS pins are HIGH (deselected) before initializing either device
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
pinMode(THERMO_CS, OUTPUT);
digitalWrite(THERMO_CS, HIGH);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0,0);
tft.setTextSize(1);
if (!maxthermo.begin()) {
Serial.println(F("Could not initialize thermocouple."));
while (1) delay(10);
}
maxthermo.setThermocoupleType(MAX31856_TCTYPE_K);
maxthermo.setConversionMode(MAX31856_ONESHOT_NOWAIT);
Setpoint = cooldownTemp;
// tell the PID to range between 0 and the full window size
myPID.SetOutputLimits(0, 1);
// turn the PID on
myPID.SetMode(AUTOMATIC);
pinMode(SSR_PIN, OUTPUT);
digitalWrite(SSR_PIN,LOW);
}
void loop() {
digitalWrite(SSR_PIN,LOW);
///* Setup Menu ///
tft.fillScreen(ILI9341_BLACK);
drawSetupMenu();
setupMenu = true;
while(setupMenu){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
int setupMenuXPos = getGridCellX(), setupMenuYPos = getGridCellY();
if(setupMenuYPos < 3){ // Somewhere other than the start button
editMenu = true;
bool editingPreheat = false, editingSoak = false, editingReflow = false;
if(setupMenuXPos < 2 ){ // Somwhere within the preheat zone
editingPreheat = true;
tft.fillScreen(preheatColor);
drawEditMenu("Preheat");
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
else if(setupMenuXPos > 3 ){// Somwhere within the reflow zone
editingReflow = true;
tft.fillScreen(reflowColor);
drawEditMenu("Reflow");
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
else{ // Somwhere within the soak zone
editingSoak = true;
tft.fillScreen(soakColor);
drawEditMenu("Soak");
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
while(editMenu){// Stay in this loop until the save button is pressed
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
int editMenuXPos = getGridCellX(), editMenuYPos = getGridCellY();
if(editMenuYPos == 1){ // One of the up arrows was pressed
if(editMenuXPos < 3){ // The Temp up arrow was pressed
tft.fillRoundRect(2gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTemp < 300);
preheatTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
}
if(editingSoak){
if(soakTemp < 300);
soakTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
}
if(editingReflow){
if(reflowTemp < 300);
reflowTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
}
}
else{// The Time up arrow was pressed
tft.fillRoundRect(5gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTime < 300000)
preheatTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
if(editingSoak){
if(soakTime < 300000)
soakTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
if(editingReflow){
if(reflowTime < 300000)
reflowTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
}
}
else if(editMenuYPos == 2){// One of the down arrows was pressed
if(editMenuXPos < 3){ // The Temp down arrow was pressed
tft.fillRoundRect(2gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTemp > 100)
preheatTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
}
if(editingSoak){
if(soakTemp > 100)
soakTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
}
if(editingReflow){
if(reflowTemp > 100)
reflowTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
}
}
else{// The Time down arrow was pressed
tft.fillRoundRect(5gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTime > 30000)
preheatTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
else if(editingSoak){
if(soakTime > 30000)
soakTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
else if(editingReflow){
if(reflowTime > 30000)
reflowTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
}
}
else if(editMenuYPos == 3){ // Save button was pressed
tft.fillScreen(ILI9341_BLACK);
drawSetupMenu();
editMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
}
else{// Start button was pressed
setupMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
/// Reflow Menu *///
tft.fillScreen(ILI9341_BLACK);
drawReflowMenu();
drawButton(0,3,2,1, ILI9341_GREEN, ILI9341_WHITE, "Start");
bool start = false;
while(!start){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() <2 && getGridCellY() == 3){
start = true;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
drawButton(0,3,2,1, ILI9341_RED, ILI9341_WHITE, "Stop");
unsigned long reflowStarted = millis();
maxthermo.triggerOneShot(); // trigger first conversion before entering loop
reflowMenu = true;
while(reflowMenu){
timeSinceReflowStarted = millis() - reflowStarted;
if(timeSinceReflowStarted - lastTimeTempCheck > timeTempCheck){
lastTimeTempCheck = timeSinceReflowStarted;
// check for conversion complete and read temperature
if (maxthermo.conversionComplete()) {
Input = maxthermo.readThermocoupleTemperature();
myPID.Compute();
if(Output < 0.5){
digitalWrite(SSR_PIN,LOW);
}
if(Output > 0.5){
digitalWrite(SSR_PIN,HIGH);
}
plotDataPoint();
}
// trigger next conversion, returns immediately
maxthermo.triggerOneShot();
printState();
}
if(timeSinceReflowStarted > totalTime){
reflowMenu = false;
}
else if(timeSinceReflowStarted > (preheatTime + soakTime + reflowTime)){ // cooldown
if(!coolingDown){
newState = true;
preheating = false, soaking = false, reflowing = false, coolingDown = true;
}
Setpoint = cooldownTemp;
}
else if(timeSinceReflowStarted > (preheatTime + soakTime)){ // reflow
if(!reflowing){
newState = true;
preheating = false, soaking = false, reflowing = true, coolingDown = false;
}
Setpoint = reflowTemp;
}
else if(timeSinceReflowStarted > preheatTime){ // soak
if(!soaking){
newState = true;
preheating = false, soaking = true, reflowing = false, coolingDown = false;
}
Setpoint = soakTemp;
}
else{ // preheat
if(!preheating){
newState = true;
preheating = true, soaking = false, reflowing = false, coolingDown = false;
}
Setpoint = preheatTemp;
}
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() < 2 && getGridCellY() == 3){
reflowMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
drawButton(0,3,2,1, ILI9341_GREEN, ILI9341_WHITE, "Done");
bool done = false;
while(!done){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() < 2 && getGridCellY() == 3){
done = true;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
}
void printState(){
String time = formatTime(timeSinceReflowStarted);
String tempStr = String(Input, 1);
// Clear right portion of status bar (columns 4-5)
tft.fillRoundRect(4gridSize+2, 3gridSize+2, 2*gridSize-4, gridSize-4, 10, ILI9341_BLACK);
tft.setFont();
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
// Time label + value on top row
tft.setCursor(4gridSize+6, 3gridSize+6);
tft.print(time);
// Temp label + value on bottom row
tft.setCursor(4gridSize+6, 3gridSize+gridSize-20);
tft.print(tempStr);
// Draw degree symbol as small circle, then C at same text size
int16_t cx = tft.getCursorX();
int16_t cy = tft.getCursorY();
tft.drawCircle(cx+3, cy+2, 2, ILI9341_WHITE);
tft.setCursor(cx+8, cy);
tft.print("C");
const char* currentState = "";
if(preheating) currentState = "Preheat";
if(soaking) currentState = "Soak";
if(reflowing) currentState = "Reflow";
if(coolingDown) currentState = "CoolDown";
if(newState){
newState = false;
tft.fillRoundRect(2gridSize+2, 3gridSize+2, 2*gridSize-4, gridSize-4, 10, ILI9341_BLACK);
centerText(2,3,2,1,ILI9341_WHITE,String(currentState));
}
}
void drawGrid(){
tft.setFont();
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.drawRect(0,0,displayWidth,displayHeight-gridSize,gridColor);
for(int i=1; i<6; i++){
tft.drawFastVLine(igridSize,0,displayHeight-gridSize,gridColor);
}
for(int j=1; j<4; j++){
tft.drawFastHLine(0,jgridSize,displayWidth,gridColor);
}
tft.setCursor(4,4); tft.print(F("300"));
tft.setCursor(4,1gridSize+4); tft.print(F("200"));
tft.setCursor(4,2gridSize+4); tft.print(F("100"));
tft.setCursor(1gridSize+4,3gridSize-7-4); tft.print(formatTime(totalTime/6));
tft.setCursor(2gridSize+4,3gridSize-7-4); tft.print(formatTime(2totalTime/6));
tft.setCursor(3gridSize+4,3gridSize-7-4); tft.print(formatTime(3totalTime/6));
tft.setCursor(4gridSize+4,3gridSize-7-4); tft.print(formatTime(4totalTime/6));
tft.setCursor(5gridSize+4,3gridSize-7-4); tft.print(formatTime(5totalTime/6));
plotReflowProfile();
}
void drawButton(int x, int y, int w, int h, uint16_t backgroundColor, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
if(backgroundColor == ILI9341_BLACK){
tft.drawRoundRect(xgridSize+2, ygridSize+2, wgridSize-4, hgridSize-4, 10, ILI9341_WHITE);
}
else{
tft.fillRoundRect(xgridSize+2, ygridSize+2, wgridSize-4, hgridSize-4, 10, backgroundColor);
}
if(text == "UP_ARROW"){
tft.fillTriangle(xgridSize+(wgridSize-36)/2, ygridSize+(hgridSize-30)/2+30, xgridSize+(wgridSize-36)/2+36, ygridSize+(hgridSize-30)/2+30, xgridSize+wgridSize/2, ygridSize+(hgridSize-30)/2, textColor);
}
else if(text == "DOWN_ARROW"){
tft.fillTriangle(xgridSize+(wgridSize-36)/2, ygridSize+(hgridSize-30)/2, xgridSize+(wgridSize-36)/2+36, ygridSize+(hgridSize-30)/2, xgridSize+wgridSize/2, ygridSize+(hgridSize-30)/2+30, textColor);
}
else if(text.length() > 0){
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2); tft.setTextColor(textColor); tft.print(text);
}
}
void centerText(int x, int y, int w, int h, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2);
tft.setTextColor(textColor); tft.print(text);
}
void centerText(int x, int y, int w, int h, int justification, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
switch(justification){
case 0: //top justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize + 4);
break;
case 1: //center justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2);
break;
case 2: //bottom justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+h*gridSize-textBoundHeight - 4);
break;
}
tft.setTextColor(textColor); tft.print(text);
}
void drawSetupMenu(){
tft.setFont();
tft.setTextSize(2);
drawButton(0,0,2,3, preheatColor, ILI9341_WHITE, ""); drawButton(2,0,2,3, soakColor, ILI9341_WHITE, ""); drawButton(4,0,2,3, reflowColor, ILI9341_WHITE, "");
centerText(0,0,2,1, ILI9341_WHITE, "Preheat"); centerText(2,0,2,1, ILI9341_WHITE, "Soak"); centerText(4,0,2,1, ILI9341_WHITE, "Reflow");
centerText(0,1,2,1,0, ILI9341_WHITE, String(int(preheatTemp)) + "C"); centerText(2,1,2,1,0, ILI9341_WHITE, String(int(soakTemp)) + "C"); centerText(4,1,2,1,0, ILI9341_WHITE, String(int(reflowTemp)) + "C");
centerText(0,1,2,1,2, ILI9341_WHITE, formatTime(preheatTime)); centerText(2,1,2,1,2, ILI9341_WHITE, formatTime(soakTime));centerText(4,1,2,1,2, ILI9341_WHITE, formatTime(reflowTime));
drawButton(0,3,6,1, ILI9341_GREEN, ILI9341_WHITE, "Confirm");
}
void drawReflowMenu(){
tft.setFont();
tft.setTextSize(2);
drawGrid();
}
void drawEditMenu(String stage){
tft.setFont();
tft.setTextSize(2);
centerText(0,0,2,1,0, ILI9341_WHITE, stage); centerText(0,0,2,1, ILI9341_WHITE, "Temp:"); drawButton(0,1,3,1, ILI9341_WHITE, ILI9341_BLACK, "UP_ARROW"); drawButton(0,2,3,1, ILI9341_WHITE, ILI9341_BLACK, "DOWN_ARROW");
centerText(3,0,2,1,0, ILI9341_WHITE, stage); centerText(3,0,2,1, ILI9341_WHITE, "Time:"); drawButton(3,1,3,1, ILI9341_WHITE, ILI9341_BLACK, "UP_ARROW"); drawButton(3,2,3,1, ILI9341_WHITE, ILI9341_BLACK, "DOWN_ARROW");
drawButton(0,3,6,1, ILI9341_GREEN, ILI9341_WHITE, "Save");
}
int getGridCellX(){
// In landscape rotation 1, raw Y maps to pixel X
int xpixel = map(touchpoint.y, TS_MINY, TS_MAXY, 0, displayWidth-1);
xpixel = constrain(xpixel, 0, displayWidth-1);
return xpixel / gridSize;
}
int getGridCellY(){
// In landscape rotation 1, raw X maps to pixel Y (high raw X = top)
int ypixel = map(touchpoint.x, TS_MAXX, TS_MINX, 0, displayHeight-1);
ypixel = constrain(ypixel, 0, displayHeight-1);
return ypixel / gridSize;
}
String formatTime(unsigned long milliseconds) {
unsigned long totalSeconds = milliseconds / 1000;
unsigned int minutes = totalSeconds / 60;
unsigned int seconds = totalSeconds % 60;
String formattedTime = (minutes < 10 ? "0" : "") + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds);
return formattedTime;
}
void plotDataPoint(){
uint16_t color = gridColor;
if(preheating) color = preheatColor;
if(soaking) color = soakColor;
if(reflowing) color = reflowColor;
if(coolingDown) color = cooldownColor;
tft.fillCircle(map(timeSinceReflowStarted,0,totalTime,0,displayWidth),map(Input,0,300,3*gridSize,0),2, color);
}
// Simplified profile plot using straight lines instead of cosine curves
// This avoids pulling in the floating-point trig library (~2-4KB savings)
void plotReflowProfile(){
int x0, x1, y0, y1;
// Preheat: cooldownTemp -> preheatTemp
x0 = 0;
x1 = map(preheatTime,0,totalTime,0,displayWidth);
y0 = map(cooldownTemp,0,300,3gridSize,0);
y1 = map(preheatTemp,0,300,3gridSize,0);
tft.drawLine(x0, y0, x1, y1, preheatColor_d);
// Soak: preheatTemp -> soakTemp
x0 = x1;
x1 = map(preheatTime+soakTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(soakTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, soakColor_d);
// Reflow: soakTemp -> reflowTemp
x0 = x1;
x1 = map(preheatTime+soakTime+reflowTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(reflowTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, reflowColor_d);
// Cooldown: reflowTemp -> cooldownTemp
x0 = x1;
x1 = map(totalTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(cooldownTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, cooldownColor_d);
}
Here's a link to the similar touchscreen:
https://www.adafruit.com/product/1770
I used Claude 4.6 to resize the buttons and graphics on the screen. It actually seemed to be a lot of changes. Unfortunately, the code and libraries were too large to fit onto the flash memory of an Arduinio UNO (or at least the model I have) so I got rid of the custom fonts and actually got rid of the curves on the displayed reflow profile (replaced with straight lines), but only for the display not for the actual PID part. Despite all the changes this code 100% works! I know this is hacky but I dont know github that well so im just going to paste the ino file after this and maybe someone who knows the whole push/fork/commit thing can do it if they care to. Email me at gkratm@gmail.com with any questions/concerns. Cheers!
#define THERMO_CS 8
#define SSR_PIN 2
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST -1 // RST can be set to -1 if you tie it to Arduino's reset
// Note the X and Y pin numbers are opposite from what is printed on the TFT display. This was done to align with the screen rotation.
#define YP A0 // must be an analog pin, use "An" notation!
#define XM A1 // must be an analog pin, use "An" notation!
#define YM 7 // can be a digital pin
#define XP 6 // can be a digital pin
// Touchscreen calibration for Adafruit 1770 (ILI9341 2.8" resistive)
// Raw X axis maps to pixel Y, Raw Y axis maps to pixel X (landscape rotation 1)
#define TS_MINX 155
#define TS_MINY 115
#define TS_MAXX 865
#define TS_MAXY 890
#include <PID_v1.h>
#include <Adafruit_MAX31856.h>
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <stdint.h>
#include "TouchScreen.h"
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
const int displayWidth = 320, displayHeight = 240;
const int gridSize = 53; // 6 cols x 53 = 318, 4 rows x 53 = 212 (small margins on 320x240)
// For better pressure precision, we need to know the resistance
// between X+ and X- Use any multimeter to read it
// For the one we're using, its 300 ohms across the X plate
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
TSPoint touchpoint;
void readTouch() {
touchpoint = ts.getPoint();
// Restore pin modes so TFT SPI works after touch read
pinMode(XM, OUTPUT);
pinMode(YP, OUTPUT);
digitalWrite(XM, LOW);
digitalWrite(YP, HIGH);
}
bool setupMenu = false, editMenu = false, reflowMenu = false;
const int touchHoldLimit = 500;
// use hardware SPI, just pass in the CS pin
Adafruit_MAX31856 maxthermo = Adafruit_MAX31856(THERMO_CS);
unsigned long timeSinceReflowStarted;
unsigned long timeTempCheck = 1000;
unsigned long lastTimeTempCheck = 0;
double preheatTemp = 180, soakTemp = 150, reflowTemp = 230, cooldownTemp = 25;
unsigned long preheatTime = 120000, soakTime = 60000, reflowTime = 120000, cooldownTime = 120000, totalTime = preheatTime + soakTime + reflowTime + cooldownTime;
bool preheating = false, soaking = false, reflowing = false, coolingDown = false, newState = false;
uint16_t gridColor = 0x7BEF;
uint16_t preheatColor = ILI9341_RED, soakColor = 0xFBE0, reflowColor = 0xDEE0, cooldownColor = ILI9341_BLUE; // colors for plotting
uint16_t preheatColor_d = 0xC000, soakColor_d = 0xC2E0, reflowColor_d = 0xC600, cooldownColor_d = 0x0018; // desaturated colors
// Define Variables we'll be connecting to
double Setpoint, Input, Output;
// Specify the links and initial tuning parameters
double Kp=2, Ki=5, Kd=1;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
void setup() {
Serial.begin(115200);
while (!Serial)
delay(10);
Serial.println(F("Solder Reflow Oven"));
delay(100);
// Ensure both SPI CS pins are HIGH (deselected) before initializing either device
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
pinMode(THERMO_CS, OUTPUT);
digitalWrite(THERMO_CS, HIGH);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0,0);
tft.setTextSize(1);
if (!maxthermo.begin()) {
Serial.println(F("Could not initialize thermocouple."));
while (1) delay(10);
}
maxthermo.setThermocoupleType(MAX31856_TCTYPE_K);
maxthermo.setConversionMode(MAX31856_ONESHOT_NOWAIT);
Setpoint = cooldownTemp;
// tell the PID to range between 0 and the full window size
myPID.SetOutputLimits(0, 1);
// turn the PID on
myPID.SetMode(AUTOMATIC);
pinMode(SSR_PIN, OUTPUT);
digitalWrite(SSR_PIN,LOW);
}
void loop() {
digitalWrite(SSR_PIN,LOW);
///* Setup Menu ///
tft.fillScreen(ILI9341_BLACK);
drawSetupMenu();
setupMenu = true;
while(setupMenu){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
int setupMenuXPos = getGridCellX(), setupMenuYPos = getGridCellY();
if(setupMenuYPos < 3){ // Somewhere other than the start button
editMenu = true;
bool editingPreheat = false, editingSoak = false, editingReflow = false;
if(setupMenuXPos < 2 ){ // Somwhere within the preheat zone
editingPreheat = true;
tft.fillScreen(preheatColor);
drawEditMenu("Preheat");
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
else if(setupMenuXPos > 3 ){// Somwhere within the reflow zone
editingReflow = true;
tft.fillScreen(reflowColor);
drawEditMenu("Reflow");
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
else{ // Somwhere within the soak zone
editingSoak = true;
tft.fillScreen(soakColor);
drawEditMenu("Soak");
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
while(editMenu){// Stay in this loop until the save button is pressed
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
int editMenuXPos = getGridCellX(), editMenuYPos = getGridCellY();
if(editMenuYPos == 1){ // One of the up arrows was pressed
if(editMenuXPos < 3){ // The Temp up arrow was pressed
tft.fillRoundRect(2gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTemp < 300);
preheatTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
}
if(editingSoak){
if(soakTemp < 300);
soakTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
}
if(editingReflow){
if(reflowTemp < 300);
reflowTemp += 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
}
}
else{// The Time up arrow was pressed
tft.fillRoundRect(5gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTime < 300000)
preheatTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
if(editingSoak){
if(soakTime < 300000)
soakTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
if(editingReflow){
if(reflowTime < 300000)
reflowTime += 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
}
}
else if(editMenuYPos == 2){// One of the down arrows was pressed
if(editMenuXPos < 3){ // The Temp down arrow was pressed
tft.fillRoundRect(2gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTemp > 100)
preheatTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(preheatTemp)));
}
if(editingSoak){
if(soakTemp > 100)
soakTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(soakTemp)));
}
if(editingReflow){
if(reflowTemp > 100)
reflowTemp -= 10;
centerText(2,0,1,1,ILI9341_WHITE,String(int(reflowTemp)));
}
}
else{// The Time down arrow was pressed
tft.fillRoundRect(5gridSize+2, 0gridSize+2, gridSize-4, gridSize-4, 10, ILI9341_BLACK);
if(editingPreheat){
if(preheatTime > 30000)
preheatTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(preheatTime));
}
else if(editingSoak){
if(soakTime > 30000)
soakTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(soakTime));
}
else if(editingReflow){
if(reflowTime > 30000)
reflowTime -= 10000;
centerText(5,0,1,1,ILI9341_WHITE, formatTime(reflowTime));
}
}
}
else if(editMenuYPos == 3){ // Save button was pressed
tft.fillScreen(ILI9341_BLACK);
drawSetupMenu();
editMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
}
else{// Start button was pressed
setupMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
/// Reflow Menu *///
tft.fillScreen(ILI9341_BLACK);
drawReflowMenu();
drawButton(0,3,2,1, ILI9341_GREEN, ILI9341_WHITE, "Start");
bool start = false;
while(!start){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() <2 && getGridCellY() == 3){
start = true;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
drawButton(0,3,2,1, ILI9341_RED, ILI9341_WHITE, "Stop");
unsigned long reflowStarted = millis();
maxthermo.triggerOneShot(); // trigger first conversion before entering loop
reflowMenu = true;
while(reflowMenu){
timeSinceReflowStarted = millis() - reflowStarted;
if(timeSinceReflowStarted - lastTimeTempCheck > timeTempCheck){
lastTimeTempCheck = timeSinceReflowStarted;
// check for conversion complete and read temperature
if (maxthermo.conversionComplete()) {
Input = maxthermo.readThermocoupleTemperature();
myPID.Compute();
if(Output < 0.5){
digitalWrite(SSR_PIN,LOW);
}
if(Output > 0.5){
digitalWrite(SSR_PIN,HIGH);
}
plotDataPoint();
}
// trigger next conversion, returns immediately
maxthermo.triggerOneShot();
printState();
}
if(timeSinceReflowStarted > totalTime){
reflowMenu = false;
}
else if(timeSinceReflowStarted > (preheatTime + soakTime + reflowTime)){ // cooldown
if(!coolingDown){
newState = true;
preheating = false, soaking = false, reflowing = false, coolingDown = true;
}
Setpoint = cooldownTemp;
}
else if(timeSinceReflowStarted > (preheatTime + soakTime)){ // reflow
if(!reflowing){
newState = true;
preheating = false, soaking = false, reflowing = true, coolingDown = false;
}
Setpoint = reflowTemp;
}
else if(timeSinceReflowStarted > preheatTime){ // soak
if(!soaking){
newState = true;
preheating = false, soaking = true, reflowing = false, coolingDown = false;
}
Setpoint = soakTemp;
}
else{ // preheat
if(!preheating){
newState = true;
preheating = true, soaking = false, reflowing = false, coolingDown = false;
}
Setpoint = preheatTemp;
}
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() < 2 && getGridCellY() == 3){
reflowMenu = false;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
drawButton(0,3,2,1, ILI9341_GREEN, ILI9341_WHITE, "Done");
bool done = false;
while(!done){
readTouch();
if(touchpoint.z > ts.pressureThreshhold){
if(getGridCellX() < 2 && getGridCellY() == 3){
done = true;
}
delay(touchHoldLimit); // so holding the button down doesn't read multiple presses
}
}
}
void printState(){
String time = formatTime(timeSinceReflowStarted);
String tempStr = String(Input, 1);
// Clear right portion of status bar (columns 4-5)
tft.fillRoundRect(4gridSize+2, 3gridSize+2, 2*gridSize-4, gridSize-4, 10, ILI9341_BLACK);
tft.setFont();
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
// Time label + value on top row
tft.setCursor(4gridSize+6, 3gridSize+6);
tft.print(time);
// Temp label + value on bottom row
tft.setCursor(4gridSize+6, 3gridSize+gridSize-20);
tft.print(tempStr);
// Draw degree symbol as small circle, then C at same text size
int16_t cx = tft.getCursorX();
int16_t cy = tft.getCursorY();
tft.drawCircle(cx+3, cy+2, 2, ILI9341_WHITE);
tft.setCursor(cx+8, cy);
tft.print("C");
const char* currentState = "";
if(preheating) currentState = "Preheat";
if(soaking) currentState = "Soak";
if(reflowing) currentState = "Reflow";
if(coolingDown) currentState = "CoolDown";
if(newState){
newState = false;
tft.fillRoundRect(2gridSize+2, 3gridSize+2, 2*gridSize-4, gridSize-4, 10, ILI9341_BLACK);
centerText(2,3,2,1,ILI9341_WHITE,String(currentState));
}
}
void drawGrid(){
tft.setFont();
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.drawRect(0,0,displayWidth,displayHeight-gridSize,gridColor);
for(int i=1; i<6; i++){
tft.drawFastVLine(igridSize,0,displayHeight-gridSize,gridColor);
}
for(int j=1; j<4; j++){
tft.drawFastHLine(0,jgridSize,displayWidth,gridColor);
}
tft.setCursor(4,4); tft.print(F("300"));
tft.setCursor(4,1gridSize+4); tft.print(F("200"));
tft.setCursor(4,2gridSize+4); tft.print(F("100"));
tft.setCursor(1gridSize+4,3gridSize-7-4); tft.print(formatTime(totalTime/6));
tft.setCursor(2gridSize+4,3gridSize-7-4); tft.print(formatTime(2totalTime/6));
tft.setCursor(3gridSize+4,3gridSize-7-4); tft.print(formatTime(3totalTime/6));
tft.setCursor(4gridSize+4,3gridSize-7-4); tft.print(formatTime(4totalTime/6));
tft.setCursor(5gridSize+4,3gridSize-7-4); tft.print(formatTime(5totalTime/6));
plotReflowProfile();
}
void drawButton(int x, int y, int w, int h, uint16_t backgroundColor, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
if(backgroundColor == ILI9341_BLACK){
tft.drawRoundRect(xgridSize+2, ygridSize+2, wgridSize-4, hgridSize-4, 10, ILI9341_WHITE);
}
else{
tft.fillRoundRect(xgridSize+2, ygridSize+2, wgridSize-4, hgridSize-4, 10, backgroundColor);
}
if(text == "UP_ARROW"){
tft.fillTriangle(xgridSize+(wgridSize-36)/2, ygridSize+(hgridSize-30)/2+30, xgridSize+(wgridSize-36)/2+36, ygridSize+(hgridSize-30)/2+30, xgridSize+wgridSize/2, ygridSize+(hgridSize-30)/2, textColor);
}
else if(text == "DOWN_ARROW"){
tft.fillTriangle(xgridSize+(wgridSize-36)/2, ygridSize+(hgridSize-30)/2, xgridSize+(wgridSize-36)/2+36, ygridSize+(hgridSize-30)/2, xgridSize+wgridSize/2, ygridSize+(hgridSize-30)/2+30, textColor);
}
else if(text.length() > 0){
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2); tft.setTextColor(textColor); tft.print(text);
}
}
void centerText(int x, int y, int w, int h, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2);
tft.setTextColor(textColor); tft.print(text);
}
void centerText(int x, int y, int w, int h, int justification, uint16_t textColor, String text){
tft.setFont();
tft.setTextSize(2);
int16_t textBoundX, textBoundY;
uint16_t textBoundWidth, textBoundHeight;
tft.getTextBounds(text,0,0,&textBoundX, &textBoundY, &textBoundWidth, &textBoundHeight);
switch(justification){
case 0: //top justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize + 4);
break;
case 1: //center justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+(hgridSize-textBoundHeight)/2);
break;
case 2: //bottom justified
tft.setCursor(xgridSize+(wgridSize-textBoundWidth)/2, ygridSize+h*gridSize-textBoundHeight - 4);
break;
}
tft.setTextColor(textColor); tft.print(text);
}
void drawSetupMenu(){
tft.setFont();
tft.setTextSize(2);
drawButton(0,0,2,3, preheatColor, ILI9341_WHITE, ""); drawButton(2,0,2,3, soakColor, ILI9341_WHITE, ""); drawButton(4,0,2,3, reflowColor, ILI9341_WHITE, "");
centerText(0,0,2,1, ILI9341_WHITE, "Preheat"); centerText(2,0,2,1, ILI9341_WHITE, "Soak"); centerText(4,0,2,1, ILI9341_WHITE, "Reflow");
centerText(0,1,2,1,0, ILI9341_WHITE, String(int(preheatTemp)) + "C"); centerText(2,1,2,1,0, ILI9341_WHITE, String(int(soakTemp)) + "C"); centerText(4,1,2,1,0, ILI9341_WHITE, String(int(reflowTemp)) + "C");
centerText(0,1,2,1,2, ILI9341_WHITE, formatTime(preheatTime)); centerText(2,1,2,1,2, ILI9341_WHITE, formatTime(soakTime));centerText(4,1,2,1,2, ILI9341_WHITE, formatTime(reflowTime));
drawButton(0,3,6,1, ILI9341_GREEN, ILI9341_WHITE, "Confirm");
}
void drawReflowMenu(){
tft.setFont();
tft.setTextSize(2);
drawGrid();
}
void drawEditMenu(String stage){
tft.setFont();
tft.setTextSize(2);
centerText(0,0,2,1,0, ILI9341_WHITE, stage); centerText(0,0,2,1, ILI9341_WHITE, "Temp:"); drawButton(0,1,3,1, ILI9341_WHITE, ILI9341_BLACK, "UP_ARROW"); drawButton(0,2,3,1, ILI9341_WHITE, ILI9341_BLACK, "DOWN_ARROW");
centerText(3,0,2,1,0, ILI9341_WHITE, stage); centerText(3,0,2,1, ILI9341_WHITE, "Time:"); drawButton(3,1,3,1, ILI9341_WHITE, ILI9341_BLACK, "UP_ARROW"); drawButton(3,2,3,1, ILI9341_WHITE, ILI9341_BLACK, "DOWN_ARROW");
drawButton(0,3,6,1, ILI9341_GREEN, ILI9341_WHITE, "Save");
}
int getGridCellX(){
// In landscape rotation 1, raw Y maps to pixel X
int xpixel = map(touchpoint.y, TS_MINY, TS_MAXY, 0, displayWidth-1);
xpixel = constrain(xpixel, 0, displayWidth-1);
return xpixel / gridSize;
}
int getGridCellY(){
// In landscape rotation 1, raw X maps to pixel Y (high raw X = top)
int ypixel = map(touchpoint.x, TS_MAXX, TS_MINX, 0, displayHeight-1);
ypixel = constrain(ypixel, 0, displayHeight-1);
return ypixel / gridSize;
}
String formatTime(unsigned long milliseconds) {
unsigned long totalSeconds = milliseconds / 1000;
unsigned int minutes = totalSeconds / 60;
unsigned int seconds = totalSeconds % 60;
String formattedTime = (minutes < 10 ? "0" : "") + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds);
return formattedTime;
}
void plotDataPoint(){
uint16_t color = gridColor;
if(preheating) color = preheatColor;
if(soaking) color = soakColor;
if(reflowing) color = reflowColor;
if(coolingDown) color = cooldownColor;
tft.fillCircle(map(timeSinceReflowStarted,0,totalTime,0,displayWidth),map(Input,0,300,3*gridSize,0),2, color);
}
// Simplified profile plot using straight lines instead of cosine curves
// This avoids pulling in the floating-point trig library (~2-4KB savings)
void plotReflowProfile(){
int x0, x1, y0, y1;
// Preheat: cooldownTemp -> preheatTemp
x0 = 0;
x1 = map(preheatTime,0,totalTime,0,displayWidth);
y0 = map(cooldownTemp,0,300,3gridSize,0);
y1 = map(preheatTemp,0,300,3gridSize,0);
tft.drawLine(x0, y0, x1, y1, preheatColor_d);
// Soak: preheatTemp -> soakTemp
x0 = x1;
x1 = map(preheatTime+soakTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(soakTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, soakColor_d);
// Reflow: soakTemp -> reflowTemp
x0 = x1;
x1 = map(preheatTime+soakTime+reflowTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(reflowTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, reflowColor_d);
// Cooldown: reflowTemp -> cooldownTemp
x0 = x1;
x1 = map(totalTime,0,totalTime,0,displayWidth);
y0 = y1;
y1 = map(cooldownTemp,0,300,3*gridSize,0);
tft.drawLine(x0, y0, x1, y1, cooldownColor_d);
}