-
Notifications
You must be signed in to change notification settings - Fork 82
/
Reflow_Master_v2.ino
2394 lines (1985 loc) · 58.8 KB
/
Reflow_Master_v2.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
---------------------------------------------------------------------------
Reflow Master Control - v2.01 - 20/12/2020.
AUTHOR/LICENSE:
Created by Seon Rozenblum - seon@unexpectedmaker.com
Copyright 2016 License: GNU GPL v3 http://www.gnu.org/licenses/gpl-3.0.html
LINKS:
Project home: github.com/unexpectedmaker/reflowmaster
Blog: unexpectedmaker.com
PURPOSE:
This controller is the software that runs on the Reflow Master toaster oven controller made by Unexpected Maker
HISTORY:
01/08/2018 v1.0 - Initial release.
13/08/2018 v1.01 - Settings UI button now change to show SELECT or CHANGE depending on what is selected
27/08/2018 v1.02 - Added tangents to the curve for ESP32 support, Improved graph curves, fixed some UI glitches, made end graph time value be the end profile time
28/08/2018 v1.03 - Added some graph smoothing
20/05/2019 v1.04 - Increased max curve to support profiles up to 8mins
- Added fan on time after reflow for cooldown settings
- Added extra profile for Ju Feng Medium temp paste
07/09/2019 v1.05 - Fixed some bugs, Thanks Tablatronix!
16/09/2019 v1.06 - Fixed probe offset temp not changing in settings
02/07/2020 v1.07 - Cleaned up some Fan control and tracking code
- Cleaned up some debug messages
13/07/2020 v1.08 - Fixed bug in DEBUG mode
17/07/2020 v1.09 - Added TC error display in menu if an error is found
- Prevent starting reflow or oven check if there is a TC error
- Code refactor for easier reading? maybe?
19/12/2020 v2.00 - Added baking mode
- Lots of code cleanup and simplification
- Included the more obscure libraries inside the sketch
20/12/2020 v2.01 - Forgot to hookup minBakeTemp, minBakeTime, maxBakeTemp, maxBakeTemp variables to buttons
- Increased max bake time to 3 hours
- Added long press for Bake Time & Temp to quickly change values, clamped at max, so it won't loop
- Oven was not turned off correctly after the bake ended
---------------------------------------------------------------------------
*/
/*
NOTE: This is a work in progress...
*/
#include <SPI.h>
#include "spline.h"
#include "Adafruit_GFX.h" // Add from Library Manager
#include "Adafruit_ILI9341.h" // Add from Library Manager
#include "MAX31855.h"
#include "OneButton.h" // Add from Library Manager
#include "ReflowMasterProfile.h"
#include "FlashStorage.h"
// used to obtain the size of an array of any type
#define ELEMENTS(x) (sizeof(x) / sizeof(x[0]))
// used to show or hide serial debug output
#define DEBUG
// TFT SPI pins
#define TFT_DC 0
#define TFT_CS 3
#define TFT_RESET 1
// MAX 31855 Pins
#define MAXDO 11
#define MAXCS 10
#define MAXCLK 12
#define BUTTON0 A0 // menu buttons
#define BUTTON1 A1 // menu buttons
#define BUTTON2 A2 // menu buttons
#define BUTTON3 A3 // menu buttons
#define BUZZER A4 // buzzer
#define RELAY 5 // relay control
#define FAN A5 // fan control
// Just a bunch of re-defined colours
#define BLUE 0x001F
#define TEAL 0x0438
#define GREEN 0x07E0
#define CYAN 0x07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define ORANGE 0xFC00
#define PINK 0xF81F
#define PURPLE 0x8010
#define GREY 0xC618
#define WHITE 0xFFFF
#define BLACK 0x0000
#define DKBLUE 0x000D
#define DKTEAL 0x020C
#define DKGREEN 0x03E0
#define DKCYAN 0x03EF
#define DKRED 0x6000
#define DKMAGENTA 0x8008
#define DKYELLOW 0x8400
#define DKORANGE 0x8200
#define DKPINK 0x9009
#define DKPURPLE 0x4010
#define DKGREY 0x4A49
// Save data struct
typedef struct {
boolean valid = false;
boolean useFan = true;
int fanTimeAfterReflow = 60;
byte paste = 0;
float power = 1;
int lookAhead = 6;
int lookAheadWarm = 1;
int tempOffset = 0;
long bakeTime = 1200; // 20 mins
float bakeTemp = 45; // Degrees C
int bakeTempGap = 3; // Aim for the desired temp minus this value to compensate for overrun
bool startFullBlast = false;
bool beep = true;
} Settings;
// UI and runtime states
enum states {
BOOT = 0,
WARMUP = 1,
REFLOW = 2,
FINISHED = 3,
MENU = 10,
SETTINGS = 11,
SETTINGS_PASTE = 12,
SETTINGS_RESET = 13,
OVENCHECK = 15,
OVENCHECK_START = 16,
BAKE_MENU = 20,
BAKE = 21,
BAKE_DONE = 22,
ABORT = 99
} state;
const String ver = "2.01";
bool newSettings = false;
long nextTempRead;
long nextTempAvgRead;
int avgReadCount = 0;
unsigned long keepFanOnTime = 0;
double timeX = 0;
double tempOffset = 60;
// Bake variables
long currentBakeTime = 0; // Used to countdown the bake time
byte currentBakeTimeCounter = 0;
int lastTempDirection = 0;
long minBakeTime = 600; // 10 mins in seconds
long maxBakeTime = 10800; // 3 hours in seconds
float minBakeTemp = 45; // 10 mins in seconds
float maxBakeTemp = 100; // 2 hours in seconds
byte settings_pointer = 0;
// Initialise an array to hold 5 profiles
// Increase this array if you plan to add more
ReflowGraph solderPaste[5];
// Index into the current profile
int currentGraphIndex = 0;
// Calibration data - currently diabled in this version
int calibrationState = 0;
long calibrationSeconds = 0;
long calibrationStatsUp = 0;
long calibrationStatsDown = 300;
bool calibrationUpMatch = false;
bool calibrationDownMatch = false;
float calibrationDropVal = 0;
float calibrationRiseVal = 0;
// Runtime reflow variables
int tcError = 0;
bool tcWasError = false;
bool tcWasGood = false;
float currentDuty = 0;
float currentTemp = 0;
float cachedCurrentTemp = 0;
float currentTempAvg = 0;
float lastTemp = -1;
float currentDetla = 0;
unsigned int currentPlotColor = GREEN;
bool isCuttoff = false;
bool isFanOn = false;
float lastWantedTemp = -1;
int buzzerCount = 5;
// Graph Size for UI
int graphRangeMin_X = 0;
int graphRangeMin_Y = 30;
int graphRangeMax_X = -1;
int graphRangeMax_Y = 165;
int graphRangeStep_X = 30;
int graphRangeStep_Y = 15;
// Create a spline reference for converting profile values to a spline for the graph
Spline baseCurve;
// Initialise the TFT screen
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RESET);
// Initialise the MAX31855 IC for thermocouple tempterature reading
MAX31855 tc(MAXCLK, MAXCS, MAXDO);
// Initialise the buttons using OneButton library
OneButton button0(BUTTON0, false);
OneButton button1(BUTTON1, false);
OneButton button2(BUTTON2, false);
OneButton button3(BUTTON3, false);
// UI button positions and sizes
int buttonPosY[] = { 19, 74, 129, 184 };
int buttonHeight = 16;
int buttonWidth = 4;
// Initiliase a reference for the settings file that we store in flash storage
Settings set;
// Initialise flash storage
FlashStorage(flash_store, Settings);
// This is where we initialise each of the profiles that will get loaded into the Reflkow Master
void LoadPaste()
{
/*
Each profile is initialised with the follow data:
Paste name ( String )
Paste type ( String )
Paste Reflow Temperature ( int )
Profile graph X values - time
Profile graph Y values - temperature
Length of the graph ( int, how long if the graph array )
*/
float baseGraphX[7] = { 1, 90, 180, 210, 240, 270, 300 }; // time
float baseGraphY[7] = { 27, 90, 130, 138, 165, 138, 27 }; // value
solderPaste[0] = ReflowGraph( "CHIPQUIK", "No-Clean Sn42/Bi57.6/Ag0.4", 138, baseGraphX, baseGraphY, ELEMENTS(baseGraphX) );
float baseGraphX1[7] = { 1, 90, 180, 225, 240, 270, 300 }; // time
float baseGraphY1[7] = { 25, 150, 175, 190, 210, 125, 50 }; // value
solderPaste[1] = ReflowGraph( "CHEMTOOLS L", "No Clean 63CR218 Sn63/Pb37", 183, baseGraphX1, baseGraphY1, ELEMENTS(baseGraphX1) );
float baseGraphX2[6] = { 1, 75, 130, 180, 210, 250 }; // time
float baseGraphY2[6] = { 25, 150, 175, 210, 150, 50 }; // value
solderPaste[2] = ReflowGraph( "CHEMTOOLS S", "No Clean 63CR218 Sn63/Pb37", 183, baseGraphX2, baseGraphY2, ELEMENTS(baseGraphX2) );
float baseGraphX3[7] = { 1, 60, 120, 160, 210, 260, 310 }; // time
float baseGraphY3[7] = { 25, 105, 150, 150, 220, 150, 20 }; // value
solderPaste[3] = ReflowGraph( "DOC SOLDER", "No Clean Sn63/Pb36/Ag2", 187, baseGraphX3, baseGraphY3, ELEMENTS(baseGraphX3) );
float baseGraphX4[6] = { 1, 90, 165, 225, 330, 360 }; // time
float baseGraphY4[6] = { 25, 150, 175, 235, 100, 25 }; // value
solderPaste[4] = ReflowGraph( "CHEMTOOLS SAC305 HD", "Sn96.5/Ag3.0/Cu0.5", 225, baseGraphX4, baseGraphY4, ELEMENTS(baseGraphX4) );
//TODO: Think of a better way to initalise these baseGraph arrays to not need unique array creation
}
// Obtain the temp value of the current profile at time X
int GetGraphValue( int x )
{
return ( CurrentGraph().reflowTemp[x] );
}
int GetGraphTime( int x )
{
return ( CurrentGraph().reflowTime[x] );
}
// Obtain the current profile
ReflowGraph CurrentGraph()
{
return solderPaste[ currentGraphIndex ];
}
// Set the current profile via the array index
void SetCurrentGraph( int index )
{
currentGraphIndex = index;
graphRangeMax_X = CurrentGraph().MaxTime();
graphRangeMax_Y = CurrentGraph().MaxTempValue() + 5; // extra padding
graphRangeMin_Y = CurrentGraph().MinTempValue();
debug_print("Setting Paste: ");
debug_println( CurrentGraph().n );
debug_println( CurrentGraph().t );
// Initialise the spline for the profile to allow for smooth graph display on UI
timeX = 0;
baseCurve.setPoints(CurrentGraph().reflowTime, CurrentGraph().reflowTemp, CurrentGraph().reflowTangents, CurrentGraph().len);
baseCurve.setDegree( Hermite );
// Re-interpolate data based on spline
for ( int ii = 0; ii <= graphRangeMax_X; ii += 1 )
{
solderPaste[ currentGraphIndex ].wantedCurve[ii] = baseCurve.value(ii);
}
// calculate the biggest graph movement delta
float lastWanted = -1;
for ( int i = 0; i < solderPaste[ currentGraphIndex ].offTime; i++ )
{
float wantedTemp = solderPaste[ currentGraphIndex ].wantedCurve[ i ];
if ( lastWanted > -1 )
{
float wantedDiff = (wantedTemp - lastWanted );
if ( wantedDiff > solderPaste[ currentGraphIndex ].maxWantedDelta )
solderPaste[ currentGraphIndex ].maxWantedDelta = wantedDiff;
}
lastWanted = wantedTemp;
}
}
void setup()
{
// Setup all GPIO
pinMode( BUZZER, OUTPUT );
pinMode( RELAY, OUTPUT );
pinMode( FAN, OUTPUT );
pinMode( BUTTON0, INPUT );
pinMode( BUTTON1, INPUT );
pinMode( BUTTON2, INPUT );
pinMode( BUTTON3, INPUT );
// Turn of Green Debug LED
pinMode( 13, INPUT );
// Turn off the SSR - duty cycle of 0
SetRelayFrequency( 0 );
#ifdef DEBUG
Serial.begin(115200);
#endif
// just wait a bit before we try to load settings from FLASH
delay(500);
// load settings from FLASH
set = flash_store.read();
// If no settings were loaded, initialise data and save
if ( !set.valid )
{
SetDefaults();
newSettings = true;
flash_store.write(set);
}
// Attatch button IO for OneButton
button0.attachClick(button0Press);
button1.attachClick(button1Press);
button2.attachClick(button2Press);
button3.attachClick(button3Press);
// New long press buttons for changing bake time and temp values
button2.attachLongPressStart(button2LongPressStart);
button2.attachDuringLongPress(button2LongPress);
button3.attachLongPressStart(button3LongPressStart);
button3.attachDuringLongPress(button3LongPress);
debug_println("TFT Begin...");
// Start up the TFT and show the boot screen
tft.begin(32000000);
BootScreen();
BuzzerStart();
delay(200);
// Start up the MAX31855
debug_println("Thermocouple Begin...");
tc.begin();
// delay for initial temp probe read to be garbage
delay(500);
// Load up the profiles
LoadPaste();
// Set the current profile based on last selected
SetCurrentGraph( set.paste );
delay(500);
// Show the main menu
ShowMenu();
}
// Helper method to display the temperature on the TFT
void DisplayTemp( bool center = false )
{
if ( center )
{
tft.setTextColor( YELLOW, BLACK );
tft.setTextSize(5);
println_Center( tft, " " + String( round( currentTemp ) ) + "c ", tft.width() / 2, ( tft.height() / 2 ) + 10 );
}
else
{
if ( round( currentTemp ) != round( cachedCurrentTemp ) )
{
// display the previous temp in black to clear it
tft.setTextColor( BLACK, BLACK );
tft.setTextSize(6);
tft.setCursor( 20, ( tft.height() / 2 ) - 25 );
tft.println( String( round( cachedCurrentTemp ) ) + "c" );
// display the new temp in green
tft.setTextColor( GREEN, BLACK );
tft.setCursor( 20, ( tft.height() / 2 ) - 25 );
tft.println( String( round( currentTemp ) ) + "c" );
// cache the current temp
cachedCurrentTemp = currentTemp;
}
}
}
void loop()
{
// Used by OneButton to poll for button inputs
button0.tick();
button1.tick();
button2.tick();
button3.tick();
// Current activity state machine
if ( state == BOOT ) // BOOT
{
return;
}
else if ( state == WARMUP ) // WARMUP - We sit here until the probe reaches the starting temp for the profile
{
if ( nextTempRead < millis() ) // we only read the probe every second
{
nextTempRead = millis() + 1000;
ReadCurrentTemp();
MatchTemp();
if ( currentTemp >= GetGraphValue(0) )
{
// We have reached the starting temp for the profile, so lets start baking our boards!
lastTemp = currentTemp;
avgReadCount = 0;
currentTempAvg = 0;
StartReflow();
}
else if ( currentTemp > 0 )
{
// Show the current probe temp so we can watch as it reaches the minimum starting value
DisplayTemp( true );
}
}
return;
}
else if ( state == FINISHED ) // FINISHED
{
// do we keep the fan on after reflow finishes to help cooldown?
KeepFanOnCheck();
return;
}
else if ( state == SETTINGS || state == SETTINGS_PASTE || state == SETTINGS_RESET ) // SETTINGS
{
// Currently not used
return;
}
else if ( state == MENU ) // MENU
{
if ( nextTempRead < millis() )
{
nextTempRead = millis() + 1000;
// We show the current probe temp in the men screen just for info
ReadCurrentTemp();
if ( tcError > 0 )
{
// Clear TC Temp background if there was one!
if ( tcWasGood )
{
tcWasGood = false;
tft.fillRect( 0, tft.height() / 2 - 10, 320, 37, BLACK );
}
tcWasError = true;
tft.setTextColor( RED, BLACK );
tft.setTextSize(3);
int third = tft.width() / 4;
println_Center( tft, " TC ERROR: #" + String( tcError ) + " ", tft.width() / 2, ( tft.height() / 2 ) + 12 );
}
else if ( currentTemp > 0 )
{
// Clear error background if there was one!
if ( tcWasError )
{
tcWasError = false;
tft.fillRect( 0, tft.height() / 2 - 15, 320, 30, BLACK );
}
tcWasGood = true;
DisplayTemp();
}
}
// do we keep the fan on after reflow finishes to help cooldown?
KeepFanOnCheck();
return;
}
else if ( state == BAKE_MENU || state == BAKE_DONE )
{
// do nothing in these states
}
else if ( state == BAKE )
{
if ( currentBakeTime > 0 )
{
if ( nextTempAvgRead < millis() )
{
nextTempAvgRead = millis() + 100;
ReadCurrentTempAvg();
}
if ( nextTempRead < millis() )
{
nextTempRead = millis() + 1000;
// Set the temp from the average
currentTemp = ( currentTempAvg / avgReadCount );
// clear the variables for next run
avgReadCount = 0;
currentTempAvg = 0;
// Control the SSR
MatchTemp_Bake();
if ( currentTemp > 0 )
currentBakeTime--;
}
UpdateBake();
}
else
{
BakeDone();
}
}
else if ( state == OVENCHECK )
{
// Currently not used
return;
}
else if ( state == OVENCHECK_START ) // calibration - not currently used
{
if ( nextTempRead < millis() )
{
nextTempRead = millis() + 1000;
ReadCurrentTemp();
MatchCalibrationTemp();
if ( calibrationState < 2 )
{
tft.setTextColor( CYAN, BLACK );
tft.setTextSize(2);
if ( calibrationState == 0 )
{
if ( currentTemp < GetGraphValue(0) )
println_Center( tft, "WARMING UP", tft.width() / 2, ( tft.height() / 2 ) - 15 );
else
println_Center( tft, "HEAT UP SPEED", tft.width() / 2, ( tft.height() / 2 ) - 15 );
println_Center( tft, "TARGET " + String( GetGraphValue(1) ) + "c in " + String( GetGraphTime(1) ) + "s", tft.width() / 2, ( tft.height() - 18 ) );
}
else if ( calibrationState == 1 )
{
println_Center( tft, "COOL DOWN LEVEL", tft.width() / 2, ( tft.height() / 2 ) - 15 );
tft.fillRect( 0, tft.height() - 30, tft.width(), 30, BLACK );
}
// only show the timer when we have hit the profile starting temp
if (currentTemp >= GetGraphValue(0) )
{
// adjust the timer colour based on good or bad values
if ( calibrationState == 0 )
{
if ( calibrationSeconds <= GetGraphTime(1) )
tft.setTextColor( WHITE, BLACK );
else
tft.setTextColor( ORANGE, BLACK );
}
else
{
tft.setTextColor( WHITE, BLACK );
}
tft.setTextSize(4);
println_Center( tft, " " + String( calibrationSeconds ) + " secs ", tft.width() / 2, ( tft.height() / 2 ) + 20 );
}
tft.setTextSize(5);
tft.setTextColor( YELLOW, BLACK );
println_Center( tft, " " + String( round( currentTemp ) ) + "c ", tft.width() / 2, ( tft.height() / 2 ) + 65 );
}
else if ( calibrationState == 2 )
{
calibrationState = 3;
tft.setTextColor( GREEN, BLACK );
tft.setTextSize(2);
tft.fillRect( 0, (tft.height() / 2 ) - 45, tft.width(), (tft.height() / 2 ) + 45, BLACK );
println_Center( tft, "RESULTS!", tft.width() / 2, ( tft.height() / 2 ) - 45 );
tft.setTextColor( WHITE, BLACK );
tft.setCursor( 20, ( tft.height() / 2 ) - 10 );
tft.print( "RISE " );
if ( calibrationUpMatch )
{
tft.setTextColor( GREEN, BLACK );
tft.print( "PASS" );
}
else
{
tft.setTextColor( ORANGE, BLACK );
tft.print( "FAIL " );
tft.setTextColor( WHITE, BLACK );
tft.print( "REACHED " + String( round(calibrationRiseVal * 100) ) + "%") ;
}
tft.setTextColor( WHITE, BLACK );
tft.setCursor( 20, ( tft.height() / 2 ) + 20 );
tft.print( "DROP " );
if ( calibrationDownMatch )
{
tft.setTextColor( GREEN, BLACK );
tft.print( "PASS" );
tft.setTextColor( WHITE, BLACK );
tft.print( "DROPPED " + String( round(calibrationDropVal * 100) ) + "%") ;
}
else
{
tft.setTextColor( ORANGE, BLACK );
tft.print( "FAIL " );
tft.setTextColor( WHITE, BLACK );
tft.print( "DROPPED " + String( round(calibrationDropVal * 100) ) + "%") ;
tft.setTextColor( WHITE, BLACK );
tft.setCursor( 20, ( tft.height() / 2 ) + 40 );
tft.print( "RECOMMEND ADDING FAN") ;
}
}
}
}
else // state is REFLOW
{
if ( nextTempAvgRead < millis() )
{
nextTempAvgRead = millis() + 100;
ReadCurrentTempAvg();
}
if ( nextTempRead < millis() )
{
nextTempRead = millis() + 1000;
// Set the temp from the average
currentTemp = ( currentTempAvg / avgReadCount );
// clear the variables for next run
avgReadCount = 0;
currentTempAvg = 0;
// Control the SSR
MatchTemp();
if ( currentTemp > 0 )
{
timeX++;
if ( timeX > CurrentGraph().completeTime )
{
EndReflow();
}
else
{
Graph(tft, timeX, currentTemp, 30, 220, 270, 180 );
if ( timeX < CurrentGraph().fanTime )
{
float wantedTemp = CurrentGraph().wantedCurve[ (int)timeX ];
DrawHeading( String( round( currentTemp ) ) + "/" + String( (int)wantedTemp ) + "c", currentPlotColor, BLACK );
}
}
}
}
}
}
// This is where the SSR is controlled via PWM
void SetRelayFrequency( int duty )
{
// calculate the wanted duty based on settings power override
currentDuty = ((float)duty * set.power );
// Write the clamped duty cycle to the RELAY GPIO
analogWrite( RELAY, constrain( round( currentDuty ), 0, 255) );
debug_print("RELAY Duty Cycle: ");
debug_println( String( ( currentDuty / 256.0 ) * 100) + "%" + " Using Settings Power: " + String( round( set.power * 100 )) + "%" );
}
/*
SOME CALIBRATION CODE THAT IS CURRENTLY USED FOR THE OVEN CHECK SYSTEM
Oven Check currently shows you hoe fast your oven can reach the initial pre-soak temp for your selected profile
*/
void MatchCalibrationTemp()
{
if ( calibrationState == 0 ) // temp speed
{
// Set SSR to full duty cycle - 100%
SetRelayFrequency( 255 );
// Only count seconds from when we reach the profile starting temp
if ( currentTemp >= GetGraphValue(0) )
calibrationSeconds ++;
if ( currentTemp >= GetGraphValue(1) )
{
debug_println("Cal Heat Up Speed " + String( calibrationSeconds ) );
calibrationRiseVal = ( (float)currentTemp / (float)( GetGraphValue(1) ) );
calibrationUpMatch = ( calibrationSeconds <= GetGraphTime(1) );
calibrationState = 1; // cooldown
SetRelayFrequency( 0 );
StartFan( false );
Buzzer( 2000, 50 );
}
}
else if ( calibrationState == 1 )
{
calibrationSeconds --;
SetRelayFrequency( 0 );
if ( calibrationSeconds <= 0 )
{
Buzzer( 2000, 50 );
debug_println("Cal Cool Down Temp " + String( currentTemp ) );
// calc calibration drop percentage value
calibrationDropVal = ( (float)( GetGraphValue(1) - currentTemp ) / (float)GetGraphValue(1) );
// Did we drop in temp > 33% of our target temp? If not, recomment using a fan!
calibrationDownMatch = ( calibrationDropVal > 0.33 );
calibrationState = 2; // finished
StartFan( true );
}
}
}
/*
END
SOME CALIBRATION CODE THAT IS CURRENTLY USED FOR THE OVEN CHECK SYSTEM
*/
void KeepFanOnCheck()
{
// do we keep the fan on after reflow finishes to help cooldown?
if ( set.useFan && millis() < keepFanOnTime )
StartFan( true );
else
StartFan( false );
}
void ReadCurrentTempAvg()
{
int status = tc.read();
if (status != 0 )
{
tcError = status;
debug_print("TC Read Error Status: ");
debug_println( status );
}
else
{
tcError = 0;
float internal = tc.getInternal();
currentTempAvg += tc.getTemperature() + set.tempOffset;
avgReadCount++;
}
}
// Read the temp probe
void ReadCurrentTemp()
{
int status = tc.read();
if (status != 0 )
{
tcError = status;
debug_print("TC Read Error Status: ");
debug_println( status );
}
else
{
tcError = 0;
float internal = tc.getInternal();
currentTemp = tc.getTemperature() + set.tempOffset;
currentTemp = constrain(currentTemp, -10, 350);
debug_print("TC Read: ");
debug_println( currentTemp );
}
}
void MatchTemp_Bake()
{
float duty = 0;
float tempDiff = 0;
float perc = 0;
float wantedTemp = set.bakeTemp - set.bakeTempGap;
// If the last temperature direction change is dropping, we want to aim for the bake temp without the gap
if ( currentTemp < set.bakeTemp && lastTempDirection < 0 )
wantedTemp = set.bakeTemp + 1; // We need to ramp a little more as we've likely been off for a while relying on thermal mass to hold temp
debug_print( "Bake T: " );
debug_print( currentBakeTime);
debug_print( " Current: " );
debug_print( currentTemp );
debug_print( " LastDir: " );
debug_print( lastTempDirection );
if ( currentTemp >= wantedTemp)
{
duty = 0;
perc = 0;
if ( set.useFan )
{
if (currentTemp > set.bakeTemp + 3)
StartFan( true );
else
StartFan( false );
}
}
else
{
// if current temp is less thn half of wanted, then boost at 100%
if ( currentTemp < ( wantedTemp / 2 ) )
perc = 1;
else
{
tempDiff = wantedTemp - currentTemp;
perc = tempDiff / wantedTemp;
}
duty = 256 * perc;
}
debug_print( " Perc: " );
debug_print( perc );
debug_print( " Duty: " );
debug_print( duty );
debug_print( " " );
duty = constrain( duty, 0, 256 );
currentPlotColor = GREEN;
SetRelayFrequency( duty );
}
// This is where the magic happens for temperature matching
void MatchTemp()
{
float duty = 0;
float wantedTemp = 0;
float wantedDiff = 0;
float tempDiff = 0;
float perc = 0;
// if we are still before the main flow cut-off time (last peak)
if ( timeX < CurrentGraph().offTime )
{
// We are looking XXX steps ahead of the ideal graph to compensate for slow movement of oven temp
if ( timeX < CurrentGraph().reflowTime[2] )
wantedTemp = CurrentGraph().wantedCurve[ (int)timeX + set.lookAheadWarm ];
else
wantedTemp = CurrentGraph().wantedCurve[ (int)timeX + set.lookAhead ];
wantedDiff = (wantedTemp - lastWantedTemp );
lastWantedTemp = wantedTemp;
tempDiff = ( currentTemp - lastTemp );
lastTemp = currentTemp;
perc = wantedDiff - tempDiff;
debug_print( "T: " );
debug_print( timeX );
debug_print( " Current: " );
debug_print( currentTemp );
debug_print( " Wanted: " );
debug_print( wantedTemp );
debug_print( " T Diff: " );
debug_print( tempDiff );
debug_print( " W Diff: " );
debug_print( wantedDiff );
debug_print( " Perc: " );
debug_print( perc );
isCuttoff = false;
// have to passed the fan turn on time?
if ( !isFanOn && timeX >= CurrentGraph().fanTime )
{
debug_print( "COOLDOWN: " );
// If we are usng the fan, turn it on
if ( set.useFan )
{
DrawHeading( "COOLDOWN!", GREEN, BLACK );
Buzzer( 2000, 2000 );
StartFan ( true );
}
else // otherwise YELL at the user to open the oven door
{
if ( buzzerCount > 0 )
{
DrawHeading( "OPEN OVEN", RED, BLACK );
Buzzer( 2000, 2000 );
buzzerCount--;
}
}
}
}
else
{
// YELL at the user to open the oven door
if ( !isCuttoff && set.useFan )
{
DrawHeading( "OPEN OVEN", GREEN, BLACK );
Buzzer( 2000, 2000 );
debug_print( "CUTOFF: " );
}
else if ( !set.useFan )
{
StartFan ( false );
}
isCuttoff = true;
}
currentDetla = (wantedTemp - currentTemp);
debug_print( " Delta: " );
debug_print( currentDetla );
float base = 128;
if ( currentDetla >= 0 )
{
base = 128 + ( currentDetla * 5 );
}
else if ( currentDetla < 0 )
{
base = 32 + ( currentDetla * 15 );
}
base = constrain( base, 0, 256 );
debug_print(" Base: ");
debug_print( base );
debug_print( " -> " );
duty = base + ( 172 * perc );
duty = constrain( duty, 0, 256 );
// override for full blast at start only if the current Temp is less than the wanted Temp, and it's in the ram before pre-soak starts.