-
Notifications
You must be signed in to change notification settings - Fork 71
/
aArtisanQ_PID.ino
1620 lines (1432 loc) · 55.9 KB
/
aArtisanQ_PID.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
// aArtisanQ_PID.ino
// ------------
// Written to support the Artisan roasting scope //http://code.google.com/p/artisan/
// Heater is controlled from OT1 using a zero cross SSR (integral pulse control)
// AC fan is controlled from OT2 using a random fire SSR (phase angle control)
// zero cross detector (true on logic low) is connected to I/O3
// *** BSD License ***
// ------------------------------------------------------------------------------------------
// Copyright (c) 2011, MLG Properties, LLC
// All rights reserved.
//
// Contributor: Jim Gallt
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// Neither the name of the copyright holder(s) nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// ------------------------------------------------------------------------------------------
// Revision history:
// 20110408 Created.
// 20110409 Reversed the BT and ET values in the output stream.
// Shortened the banner display time to avoid timing issues with Artisan
// Echo all commands to the LCD
// 20110413 Added support for Artisan 0.4.1
// 20110414 Reduced filtering levels on BT, ET
// Improved robustness of checkSerial() for stops/starts by Artisan
// Revised command format to include newline character for Artisan 0.4.x
// 20110528 New command language added (major revision)
// Use READ command to poll the device for up to 4 temperature channels
// Change temperature scale using UNITS command
// Map physical channels on ADC to logical channels using the CHAN command
// Select SSR output duty cycle with OT1 and OT2 commands
// Select PWM logic level output on I/O3 using IO3 command
// Directly control digital pins using DPIN command (WARNING -- this might not be smart)
// Directly control analog pins using APIN command (WARNING -- this might not be smart)
// 20110601 Major rewrite to use cmndproc.h library
// RF2000 and RC2000 set channel mapping to 1200
// 20110602 Added ACKS_ON to control verbose output
// ----------- Version 1.10
// 20111011 Added support for type J and type T thermocouples
// Better error checking on EEPROM reads
// ----------- aArtsianQ version 0.xx
// 20111031 Created.
// ----------- aArtisanQ beta1
// 20111101 Beta 1 release
// ----------- aArtisanQ_PID (Brad Collins)
// 20120915 Created.
// Added PID Library
// Added code for analogue inputs
// 20120916 Added PID command allowing PID control to be activated and deactivated from Artisan
// Added roast clock. Can be reset with PID;TIME command
// Removed LCD ambient temp display and added roast clock display
// 20120918 Added code to read profile from EEPROM and interpolate to calculate setpoint. Time/Temp profiles
// Added code to end PID control when end of profile is reached
// Added additional PID command allowing roast profile to be selected
// Added additional PID command allowing PID tunings to be adjusted on the fly (required MAX_TOKENS 5 in cmndproc.h library)
// 20120920 Added RoR calcs
// 20120921 Updated RoR calcs to better handle first loop issue (RoR not calculated in first loop)
// Stopped ANLG1 being read during PID control
// Added code to convert PID Setpoint temps to correct units. Added temperature units data to profile format
// Serial command echo to LCD now optional
// 20120922 Added support for LCDapter buttons and LEDs (button 1 currently activates or deactivates PID control if enabled)
// Added code to allow power to OT1 to be cut if OT2 is below HTR_CUTOFF_FAN_VAL percentage as defined in user.h. For heater protection if required. Required modification to phase_ctrl.cpp
// Added code to allow OT2 to range between custom min and max percentages (defined in user.h)
// 20121007 Fixed PID tuning command so it handles doubles
// Added inital PID tuning parameters in user.h
// 20121013 Added code to allow Artisan plotting of levelOT1 and levelOT2 if PLOT_POWER is defined in user.h
// Swapped location of T1 and T2 on LCD display and renamed to ET and BT
// 20121014 Enhanced LCD display code and added support for 4x20 LCDs. Define LCD_4x20 in user.h
// 20121021 Added optional limits for Analogue1
// 20121120 Added support for pBourbon logging
// 20121213 Added UP and DOWN parameters for OT1 and OT2 commands. Increments or decrements power levels by 5%
// 20130116 Added user adjustable analogue input rounding (DUTY_STEP) in user.h
// 20130119 aArtisanQ_PID release 3_5
// Added code to allow for additional LCD display modes
// 20130120 Added ability to change roast profile using LCD and buttons
// 20130121 Tidied up button press code
// 20130203 Permits use of different TC types on individual channels as in aArtisan 2.10
// Updated temperature sample filtering to match aArtisan 2.10
// 20130406 Added GO and STOP commands to use with Artisan 'Charge' and 'End' buttons
// 20140127 aArtisanQ_PID release 4_0 created
// Added support for Roastlogger roasting software (responds to LOAD, POWER and FAN commands. Sends rorT1=, T1=, rorT2=, T2= and power levels to roastlogger)
// 20140128 Improved handling of heater and fan power limits
// 20140213 aArtisanQ_PID release 4_2
// 20140214 Added option in user.h to define software mode (Artisan, Roastlogger or pBourbon)
// Fixed? bug causing crashes when receiving READ commands
// 20140214 aArtisanQ_PID release 4_3
// 20150328 Replaced pBourbon option with Android option
// Bug fix in LCD menu code. Menu code disabled when not roasting stand alone
// Changed default PID channel to 0
// Added SV to values sent to Android app
// 20150328 aArtisanQ_PID release 5_0
// 20150403 Added PID;SV command from aArtisan
// Changed default PID roast profile to 0 when using logging software
// Reduced SRAM usage
// 20150404 Added PID;CHAN PID;CT and FILT commands
// Added Heater, Fan and SV values to Artisan Logger if PID active
// Removed PLOT_POWER option. Send power levels and SV if PID is active for Artisan. Send all by default for Android
// 20150409 Made loop time variable. Default = 1000ms (1s) but changes to 2000ms (2s) if needed for reading 4 temperature channels
// Adjusted Read command to match aArtisan. Runs logger() from command to provide immediate response to Artisan
// 20150426 Removed io3, rf2000 and rc2000 commands to save memory
// Other small compile changes to save memory
// Compile directive change to ensure output levels are displayed when analogue pots are not active
// 20160416 Added fast PWM (3.922kHz) available in Phase Angle Control Mode on IO3. Enable using IO3_HTR_PAC in user.h.
// Moved default zero-cross interrupt to IO2 to allow PWM out on IO3.
// Also does 3.922kHz PWM for DC fan on IO3 in PWM mode.
// Added Min and Max for IO3 output
// aArtisanQ_PID version 6_2 released for testing
// 20161214 Added some compile directives to disable the pwmio3 variable when IO3_HTR_PAC is not defined (JGG)
// This was causing the pullup on IO3 to be disabled and intefered with use of IO3 as the ZCD input
// 20161216 Changes to user.h (and others) to implement pre-defined configurations
// 20171004 Changed definition of PID_CHAN from logical channel to physical channel
// This was causing issues with Artisan Roasting Scope logic when doing background follow with TC4 PID and logical channels set using CHAN;2100
// Artisan Roasting Scope is assuming X in PID;CHAN;X command is a physical channel
// Adjusted ROR_CHAN code to match physical channel approach
// Adjusted command.txt to suit above changes
// Adjusted Logger() so Heater Duty and Fan Duty are always sent in serial stream regardless od PID state
// 20180214 Added support for cheap I2C LCDs
// Added support for two directly connected buttons for reseting timer and toggle PID.
// 20180529 Added support for another 2 buttons for menu navigation
// Modified compile directives to allow IO3 PWM control and DCFAN command in CONFIG_PAC2 mode
// Minor bug fix in logger()
// 20180831 Added support for PID_V1.2 library with Proportional on Measurement mode option
// Added flag in user.h to set default PID mode
// Modified PID tuning serial command. Now accepts PID;T_POM;p;i;d to set P_ON_M mode. PID;T;p;i;d now switches to P_ON_E mode.
// Added new PID;LIMIT;min;max serial command to allow the PID output limits to be set.
// Cleaned code to remove compile warnings
// 20180913 Version 6_6 released
// 20181130 Added compile directives to allow CONFIG_PAC3 mode to compile
// Minor changes to user.h defaults
// Version 6_7 released
// 20210108 Added new CONFIG_PAC2_IO3FAN mode wich uses ICC heater control on OT1 and PWM fan control on IO3
// 20211215 Bug fix in PID;P;x command
// Bug fix for MAX_OT2 limit check in OT2 command
// Reassigned RS pin for parallel LCD connection from pin 2 to pin 5 to eliminate conflict between LCD and IO2
// 20220225 Changed PID profile selection command format from PID,Px to PID,P,x
// Bug fixes in PID,SV and PID,P,x commands
// 20230328 Bug fix in IO3;xxx command. MIN_IO3 and MAX_IO3 checks added.
#define BANNER_ARTISAN "aArtisanQ_PID 6_8"
// this library included with the arduino distribution
#include <Wire.h>
// The user.h file contains user-definable and some other global compiler options
// It must be located in the same folder as aArtisan.pde
#include "user.h"
// command processor declarations -- must be in same folder as aArtisan
#include "cmndreader.h"
#ifdef MEMORY_CHK
// debugging memory problems
#include "MemoryFree.h"
#endif
#ifdef PHASE_ANGLE_CONTROL
// code for integral cycle control and phase angle control
#include "phase_ctrl.h"
#endif
#include <PWM16.h> // for SSR output
// these "contributed" libraries must be installed in your sketchbook's arduino/libraries folder
#include <cmndproc.h> // for command interpreter
#include <thermocouple.h> // type K, type J, and type T thermocouple support
#include <cADC.h> // MCP3424
#if defined LCD_PARALLEL || defined LCDAPTER
#include <cLCD.h> // required only if LCD is used
#endif
#ifdef LCD_I2C
#include <LiquidCrystal_I2C.h>
#endif
// ------------------------ other compile directives
#define MIN_DELAY 300 // ms between ADC samples (tested OK at 270)
#define DP 1 // decimal places for output on serial port
#define D_MULT 0.001 // multiplier to convert temperatures from int to float
#define DELIM "; ,=" // command line parameter delimiters
#include <mcEEPROM.h>
mcEEPROM eeprom;
calBlock caldata;
float AT; // ambient temp
float T[NC]; // final output values referenced to physical channels 0-3
int32_t ftemps[NC]; // heavily filtered temps
int32_t ftimes[NC]; // filtered sample timestamps
int32_t ftemps_old[NC]; // for calculating derivative
int32_t ftimes_old[NC]; // for calculating derivative
float RoR[NC]; // final RoR values
uint8_t actv[NC]; // identifies channel status, 0 = inactive, n = physical channel + 1
#ifdef CELSIUS // only affects startup conditions
boolean Cscale = true;
#else
boolean Cscale = false;
#endif
int levelOT1, levelOT2; // parameters to control output levels
#if !(defined PHASE_ANGLE_CONTROL && (INT_PIN == 3) )
int levelIO3;
#endif
#ifdef MEMORY_CHK
uint32_t checktime;
#endif
#ifdef ANALOGUE1
uint8_t anlg1 = 0; // analog input pins
int32_t old_reading_anlg1; // previous analogue reading
boolean analogue1_changed;
#endif
#ifdef ANALOGUE2
uint8_t anlg2 = 1; // analog input pins
int32_t old_reading_anlg2; // previous analogue reading
boolean analogue2_changed;
#endif
#ifdef PID_CONTROL
#include <PID_v1.h>
//Define PID Variables we'll be connecting to
double Setpoint, Input, Output, SV; // SV is for roasting software override of Setpoint
//Specify the links and initial tuning parameters
#ifdef POM
PID myPID(&Input, &Output, &Setpoint,2,5,1,P_ON_M,DIRECT);
#else
PID myPID(&Input, &Output, &Setpoint,2,5,1,P_ON_E,DIRECT);
#endif
uint8_t pid_chan = PID_CHAN; // identify PV and set default value from user.h
int profile_number; // number of the profile for PID control
int profile_ptr; // EEPROM pointer for profile data
char profile_name[40];
char profile_description[80];
int profile_number_new; // used when switching between profiles
int times[2], temps[2]; // time and temp values read from EEPROM for setpoint calculation
char profile_CorF; // profile temps stored as Centigrade or Fahrenheit
#endif
uint32_t counter; // second counter
uint32_t next_loop_time; //
boolean first;
uint16_t looptime = 1000;
// class objects
cADC adc( A_ADC ); // MCP3424
ambSensor amb( A_AMB ); // MCP9800
filterRC fT[NC]; // filter for logged ET, BT
filterRC fRise[NC]; // heavily filtered for calculating RoR
filterRC fRoR[NC]; // post-filtering on RoR values
#ifndef PHASE_ANGLE_CONTROL
PWM16 ssr; // object for SSR output on OT1, OT2
#endif
// -----------------------------------------
// revised 14-Dec-2016 by JGG to disable constructor of pwmio3 when IO3_HTR_PAC not used
// revised 24-Sep-2017 by Brad changed from #ifdef IO3_HTR_PAC to #ifndef CONFIG_PAC3
#ifndef CONFIG_PAC3
PWM_IO3 pwmio3;
#endif
// --------------------------------------
CmndInterp ci( DELIM ); // command interpreter object
// array of thermocouple types
tcBase * tcp[4];
TC_TYPE1 tc1;
TC_TYPE2 tc2;
TC_TYPE3 tc3;
TC_TYPE4 tc4;
// ---------------------------------- LCD interface definition
#if defined LCD_PARALLEL || defined LCDAPTER || defined LCD_I2C
// LCD output strings
char st1[6],st2[6];
int LCD_mode = 0;
#endif
#ifdef LCDAPTER
#include <cButton.h>
cButtonPE16 buttons; // class object to manage button presses
#define BACKLIGHT lcd.backlight();
cLCD lcd; // I2C LCD interface
#endif
#ifdef LCD_I2C
// Set the pins on the I2C chip used for LCD connections:
// addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(LCD_I2C_ADDRESS, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); // Set the LCD I2C address
#define BACKLIGHT lcd.backlight();
#endif
#ifdef LCD_PARALLEL
#define BACKLIGHT ;
#define RS 5
#define ENABLE 4
#define D4 7
#define D5 8
#define D6 12
#define D7 13
LiquidCrystal lcd( RS, ENABLE, D4, D5, D6, D7 ); // standard 4-bit parallel interface
#endif
unsigned long debounceDelay = 50;
#ifdef RESET_TIMER_BUTTON
unsigned long lastDebounceTimeRESET_TIMER_BUTTON = 0;
int buttonStateRESET_TIMER_BUTTON; // the current reading from the input pin
int lastButtonStateRESET_TIMER_BUTTON = HIGH; // the previous reading from the input pin
#endif
#ifdef TOGGLE_PID_BUTTON
unsigned long lastDebounceTimeTOGGLE_PID_BUTTON = 0;
int buttonStateTOGGLE_PID_BUTTON; // the current reading from the input pin
int lastButtonStateTOGGLE_PID_BUTTON = HIGH; // the previous reading from the input pin
#endif
#ifdef MODE_BUTTON
unsigned long lastDebounceTimeMODE_BUTTON = 0;
int buttonStateMODE_BUTTON; // the current reading from the input pin
int lastButtonStateMODE_BUTTON = HIGH; // the previous reading from the input pin
#endif
#ifdef ENTER_BUTTON
unsigned long lastDebounceTimeENTER_BUTTON = 0;
int buttonStateENTER_BUTTON; // the current reading from the input pin
int lastButtonStateENTER_BUTTON = HIGH; // the previous reading from the input pin
#endif
// --------------------------------------------- end LCD interface
// T1, T2 = temperatures x 1000
// t1, t2 = time marks, milliseconds
// ---------------------------------------------------
float calcRise( int32_t T1, int32_t T2, int32_t t1, int32_t t2 ) {
int32_t dt = t2 - t1;
if( dt == 0 ) return 0.0; // fixme -- throw an exception here?
float dT = ( convertUnits( T2 ) - convertUnits( T1 ) ) * D_MULT;
float dS = dt * 0.001; // convert from milli-seconds to seconds
return ( dT / dS ) * 60.0; // rise per minute
}
// ------------- wrapper for the command interpreter's serial line reader
void checkSerial() {
const char* result = ci.checkSerial();
if( result != NULL ) { // some things we might want to do after a command is executed
#if defined LCD && defined COMMAND_ECHO
lcd.setCursor( 0, 0 ); // echo all commands to the LCD
lcd.print( result );
#endif
#ifdef MEMORY_CHK
Serial.print(F("# freeMemory()="));
Serial.print(freeMemory());
Serial.print(F(" , "));
Serial.println( result );
#endif
}
}
// ----------------------------------
void checkStatus( uint32_t ms ) { // this is an active delay loop
uint32_t tod = millis();
while( millis() < tod + ms ) {
checkSerial();
#if ( !defined( PHASE_ANGLE_CONTROL ) ) || ( INT_PIN != 3 ) // disable when PAC active and pin 3 reads the ZCD
dcfan.slew_fan(); // keep the fan smoothly increasing in speed
#endif
#ifdef LCDAPTER
#if not ( defined ROASTLOGGER || defined ARTISAN || defined ANDROID )
checkButtons();
#endif
#endif
#if not ( defined ROASTLOGGER || defined ARTISAN || defined ANDROID ) // Stops buttons being read unless in standalone mode. Added to fix crash (due to low memory?).
checkButtonPins();
#endif
}
}
// ----------------------------------------------------
float convertUnits ( float t ) {
if( Cscale ) return F_TO_C( t );
else return t;
}
// ------------------------------------------------------------------
void logger() {
#ifdef ARTISAN
// print ambient
Serial.print( convertUnits( AT ), DP );
// print active channels
for( uint8_t jj = 0; jj < NC; ++jj ) {
uint8_t k = actv[jj];
if( k > 0 ) {
--k;
Serial.print(F(","));
Serial.print( convertUnits( T[k] ),DP );
}
}
Serial.print(F(","));
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // send 0 if OT1 has been cut off
Serial.print( 0 );
}
else {
Serial.print( HEATER_DUTY );
}
Serial.print(F(","));
Serial.print( FAN_DUTY );
if( myPID.GetMode() != MANUAL ) { // If PID in AUTOMATIC mode
Serial.print(F(","));
Serial.print( Setpoint );
} else {
Serial.print(F(","));
Serial.print( 0 ); // send 0 if PID is off
}
Serial.println();
#endif
#ifdef ROASTLOGGER
for( uint8_t jj = 0; jj < NC; ++jj ) {
uint8_t k = actv[jj];
if( k > 0 ) {
--k;
Serial.print(F("rorT"));
Serial.print(k+1);
Serial.print(F("="));
Serial.println( RoR[k], DP );
Serial.print(F("T"));
Serial.print(k+1);
Serial.print(F("="));
Serial.println( convertUnits( T[k] ) );
}
}
Serial.print(F("Power%="));
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // send 0 if OT1 has been cut off
Serial.println( 0 );
}
else {
Serial.println( HEATER_DUTY );
}
Serial.print(F("Fan="));
Serial.print( FAN_DUTY );
#endif
#ifdef ANDROID
// print counter
//Serial.print( counter );
//Serial.print( F(",") );
// print ambient
Serial.print( convertUnits( AT ), DP );
// print active channels
for( uint8_t jj = 0; jj < NC; ++jj ) {
uint8_t k = actv[jj];
if( k > 0 ) {
--k;
Serial.print(F(","));
Serial.print( convertUnits( T[k] ) );
Serial.print(F(","));
Serial.print( RoR[k], DP );
}
}
//#ifdef PLOT_POWER
Serial.print(F(","));
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // send 0 if OT1 has been cut off
Serial.print( 0 );
}
else {
Serial.print( HEATER_DUTY );
}
Serial.print(F(","));
Serial.print( FAN_DUTY );
//#endif
#ifdef PID_CONTROL
Serial.print(F(","));
Serial.print( Setpoint );
#endif
Serial.println();
#endif
}
// --------------------------------------------------------------------------
void get_samples() // this function talks to the amb sensor and ADC via I2C
{
int32_t v;
tcBase * tc;
float tempF;
int32_t itemp;
float rx;
uint16_t dly = amb.getConvTime(); // use delay based on slowest conversion
uint16_t dADC = adc.getConvTime();
dly = dly > dADC ? dly : dADC;
for( uint8_t jj = 0; jj < NC; jj++ ) { // one-shot conversions on both chips
uint8_t k = actv[jj]; // map logical channels to physical ADC channels
if( k > 0 ) {
--k;
tc = tcp[k]; // each channel may have its own TC type
adc.nextConversion( k ); // start ADC conversion on physical channel k
amb.nextConversion(); // start ambient sensor conversion
checkStatus( dly ); // give the chips time to perform the conversions
if( !first ) { // on first loop dont save zero values
ftemps_old[k] = ftemps[k]; // save old filtered temps for RoR calcs
ftimes_old[k] = ftimes[k]; // save old timestamps for filtered temps for RoR calcs
}
ftimes[k] = millis(); // record timestamp for RoR calculations
amb.readSensor(); // retrieve value from ambient temp register
v = adc.readuV(); // retrieve microvolt sample from MCP3424
tempF = tc->Temp_F( 0.001 * v, amb.getAmbF() ); // convert uV to Celsius
// filter on direct ADC readings, not computed temperatures
v = fT[k].doFilter( v << 10 ); // multiply by 1024 to create some resolution for filter
v >>= 10;
AT = amb.getAmbF();
T[k] = tc->Temp_F( 0.001 * v, AT ); // convert uV to Fahrenheit;
ftemps[k] =fRise[k].doFilter( tempF * 1000 ); // heavier filtering for RoR
if ( !first ) { // on first loop dont calc RoR
rx = calcRise( ftemps_old[k], ftemps[k], ftimes_old[k], ftimes[k] );
RoR[k] = fRoR[k].doFilter( rx / D_MULT ) * D_MULT; // perform post-filtering on RoR values
}
}
}
first = false;
};
#if defined LCD_PARALLEL || defined LCDAPTER || defined LCD_I2C
// --------------------------------------------
void updateLCD() {
if( LCD_mode == 0 ) { // Display normal LCD screen
lcd.setCursor(0,0);
if(counter/60 < 10) lcd.print(F("0")); lcd.print(counter/60); // Prob can do this better. Check aBourbon.
lcd.print(F(":")); // make this blink?? :)
if(counter - (counter/60)*60 < 10) lcd.print(F("0")); lcd.print(counter - (counter/60)*60);
#ifdef LCD_4x20
#ifdef COMMAND_ECHO
lcd.print(F(" ")); // overwrite artisan commands
#endif
// display the first 2 active channels encountered, normally BT and ET
int it01;
uint8_t jj,j;
uint8_t k;
for( jj = 0, j = 0; jj < NC && j < 2; ++jj ) {
k = actv[jj];
if( k != 0 ) {
++j;
it01 = round( convertUnits( T[k-1] ) );
if( it01 > 999 )
it01 = 999;
else
if( it01 < -999 ) it01 = -999;
sprintf( st1, "%4d", it01 );
if( j == 1 ) {
lcd.setCursor( 13, 0 );
lcd.print(F("ET:"));
}
else {
lcd.setCursor( 13, 1 );
lcd.print( F("BT:") );
}
lcd.print(st1);
}
}
// AT
it01 = round( convertUnits( AT ) );
if( it01 > 999 )
it01 = 999;
else
if( it01 < -999 ) it01 = -999;
sprintf( st1, "%3d", it01 );
lcd.setCursor( 6, 0 );
lcd.print(F("AT:"));
lcd.print(st1);
#ifdef PID_CONTROL
if( myPID.GetMode() != MANUAL ) { // if PID is on then display PID: nnn% instead of OT1:
lcd.setCursor( 0, 2 );
lcd.print( F("PID:") );
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // display 0% if OT1 has been cut off
sprintf( st1, "%4d", (int)0 );
}
else {
sprintf( st1, "%4d", (int)HEATER_DUTY );
}
lcd.print( st1 ); lcd.print(F("%"));
lcd.setCursor( 13, 2 ); // display setpoint if PID is on
lcd.print( F("SP:") );
sprintf( st1, "%4d", (int)Setpoint );
lcd.print( st1 );
}
else {
//#ifdef ANALOGUE1
lcd.setCursor( 13, 2 );
lcd.print(F(" ")); // blank out SP: nnn if PID is off
//#else
// lcd.setCursor( 0, 2 );
// lcd.print(F(" ")); // blank out PID: nnn% and SP: nnn if PID is off and ANALOGUE1 isn't defined
//#endif // end ifdef ANALOGUE1
}
#endif // end ifdef PID_CONTROL
// RoR
lcd.setCursor( 0, 1 );
lcd.print( F("RoR:"));
sprintf( st1, "%4d", (int)RoR[ROR_CHAN - 1] ); // adjust ROR_CHAN for 0-based array index
lcd.print( st1 );
//#ifdef ANALOGUE1
#ifdef PID_CONTROL
if( myPID.GetMode() == MANUAL ) { // only display OT2: nnn% if PID is off so PID display isn't overwriten
lcd.setCursor( 0, 2 );
lcd.print(F("HTR:"));
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // display 0% if OT1 has been cut off
sprintf( st1, "%4d", (int)0 );
}
else {
sprintf( st1, "%4d", (int)HEATER_DUTY );
}
lcd.print( st1 ); lcd.print(F("%"));
}
#else // if PID_CONTROL isn't defined then always display OT1: nnn%
lcd.setCursor( 0, 2 );
lcd.print(F("HTR:"));
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // display 0% if OT1 has been cut off
sprintf( st1, "%4d", (int)0 );
}
else {
sprintf( st1, "%4d", (int)HEATER_DUTY );
}
lcd.print( st1 ); lcd.print(F("%"));
#endif // end ifdef PID_CONTROL
//#endif // end ifdef ANALOGUE1
//#ifdef ANALOGUE2
lcd.setCursor( 0, 3 );
lcd.print(F("FAN:"));
sprintf( st1, "%4d", (int)FAN_DUTY );
lcd.print( st1 ); lcd.print(F("%"));
//#endif
#else // if not def LCD_4x20 ie if using a standard 2x16 LCD
#ifdef COMMAND_ECHO
lcd.print(F(" ")); // overwrite artisan commands
#endif
// display the first 2 active channels encountered, normally BT and ET
int it01;
uint8_t jj,j;
uint8_t k;
for( jj = 0, j = 0; jj < NC && j < 2; ++jj ) {
k = actv[jj];
if( k != 0 ) {
++j;
it01 = round( convertUnits( T[k-1] ) );
if( it01 > 999 )
it01 = 999;
else
if( it01 < -999 ) it01 = -999;
sprintf( st1, "%4d", it01 );
if( j == 1 ) {
lcd.setCursor( 9, 0 );
lcd.print(F("ET:"));
}
else {
lcd.setCursor( 9, 1 );
lcd.print( F("BT:") );
}
lcd.print(st1);
}
}
#ifdef PID_CONTROL
if( myPID.GetMode() != MANUAL ) {
lcd.setCursor( 0, 1 );
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // display 0% if OT1 has been cut off
lcd.print( F(" 0") );
}
else {
sprintf( st1, "%3d", (int)HEATER_DUTY );
lcd.print( st1 );
}
lcd.print(F("%"));
sprintf( st1, "%4d", (int)Setpoint );
lcd.print(st1);
}
else {
lcd.setCursor( 0, 1 );
lcd.print( F("RoR:"));
sprintf( st1, "%4d", (int)RoR[ROR_CHAN - 1] ); // adjust ROR_CHAN for 0-based array index
lcd.print( st1 );
}
#else
lcd.setCursor( 0, 1 );
lcd.print( F("RoR:"));
sprintf( st1, "%4d", (int)RoR[ROR_CHAN - 1] ); // adjust ROR_CHAN for 0-based array index
lcd.print( st1 );
#endif // end ifdef PID_CONTROL
#ifdef ANALOGUE1
if( analogue1_changed == true ) { // overwrite RoR or PID values
lcd.setCursor( 0, 1 );
lcd.print(F("HTR: "));
lcd.setCursor( 4, 1 );
if( FAN_DUTY < HTR_CUTOFF_FAN_VAL ) { // display 0% if OT1 has been cut off
sprintf( st1, "%3d", (int)0 );
}
else {
sprintf( st1, "%3d", (int)HEATER_DUTY );
}
lcd.print( st1 ); lcd.print(F("%"));
}
#endif //ifdef ANALOGUE1
#ifdef ANALOGUE2
if( analogue2_changed == true ) { // overwrite RoR or PID values
lcd.setCursor( 0, 1 );
lcd.print(F("FAN: "));
lcd.setCursor( 4, 1 );
sprintf( st1, "%3d", (int)FAN_DUTY );
lcd.print( st1 ); lcd.print(F("%"));
}
#endif // end ifdef ANALOGUE2
#endif // end of ifdef LCD_4x20
}
else if( LCD_mode == 1 ) { // Display alternative 1 LCD display
#ifdef PID_CONTROL
#ifdef LCD_4x20
lcd.setCursor( 0, 0 );
for( int i = 0; i < 20; i++ ) {
if( profile_name[i] != 0 ) lcd.print( profile_name[i] );
}
lcd.setCursor( 0, 1 );
for( int i = 0; i < 20; i++ ) {
if( profile_description[i] != 0 ) lcd.print( profile_description[i] );
}
lcd.setCursor( 0, 2 );
for( int i = 20; i < 40; i++ ) {
if( profile_description[i] != 0 ) lcd.print( profile_description[i] );
}
lcd.setCursor( 0, 3 );
lcd.print(F("PID: ")); lcd.print( myPID.GetKp() ); lcd.print(F(",")); lcd.print( myPID.GetKi() ); lcd.print(F(",")); lcd.print( myPID.GetKd() );
#else // if not def LCD_4x20 ie if using a standard 2x16 LCD
lcd.setCursor( 0, 0 );
for( int i = 0; i < 20; i++ ) {
if( profile_name[i] != 0 ) lcd.print( profile_name[i] );
}
lcd.setCursor( 0, 1 );
lcd.print(F("P:")); lcd.print( myPID.GetKp() ); lcd.print(F(",")); lcd.print( myPID.GetKi() ); lcd.print(F(",")); lcd.print( myPID.GetKd() );
#endif // end ifdef LCD_4x20
#endif // end ifdef PID_CONTROL
}
} // end of updateLCD()
#endif
#if defined ANALOGUE1 || defined ANALOGUE2
// -------------------------------- reads analog value and maps it to 0 to 100
// -------------------------------- rounded to the nearest DUTY_STEP value
int32_t getAnalogValue( uint8_t port ) {
int32_t mod, trial, min_anlg1, max_anlg1, min_anlg2, max_anlg2;
#ifdef PHASE_ANGLE_CONTROL
#ifdef IO3_HTR_PAC
min_anlg1 = MIN_IO3;
max_anlg1 = MAX_IO3;
min_anlg2 = MIN_OT2;
max_anlg2 = MAX_OT2;
#endif
#ifdef CONFIG_PAC2_IO3FAN
min_anlg1 = MIN_OT1;
max_anlg1 = MAX_OT1;
min_anlg2 = MIN_IO3;
max_anlg2 = MAX_IO3;
#else
min_anlg1 = MIN_OT1;
max_anlg1 = MAX_OT1;
min_anlg2 = MIN_OT2;
max_anlg2 = MAX_OT2;
#endif
#else // PWM Mode
min_anlg1 = MIN_OT1;
max_anlg1 = MAX_OT1;
min_anlg2 = MIN_IO3;
max_anlg2 = MAX_IO3;
#endif
float aval;
aval = analogRead( port );
#ifdef ANALOGUE1
if( port == anlg1 ) {
aval = min_anlg1 * 10.24 + ( aval / 1024 ) * 10.24 * ( max_anlg1 - min_anlg1 ) ; // scale analogue value to new range
if ( aval == ( min_anlg1 * 10.24 ) ) aval = 0; // still allow OT1 to be switched off at minimum value. NOT SURE IF THIS FEATURE IS GOOD???????
mod = min_anlg1;
}
#endif
#ifdef ANALOGUE2
if( port == anlg2 ) {
aval = min_anlg2 * 10.24 + ( aval / 1024 ) * 10.24 * ( max_anlg2 - min_anlg2 ) ; // scale analogue value to new range
if ( aval == ( min_anlg2 * 10.24 ) ) aval = 0; // still allow OT2 to be switched off at minimum value. NOT SURE IF THIS FEATURE IS GOOD???????
mod = min_anlg2;
}
#endif
trial = ( aval + 0.001 ) * 100; // to fix weird rounding error from previous calcs?????
trial /= 1023;
trial = ( trial / DUTY_STEP ) * DUTY_STEP; // truncate to multiple of DUTY_STEP
if( trial < mod ) trial = 0;
// mod = trial % DUTY_STEP;
// trial = ( trial / DUTY_STEP ) * DUTY_STEP; // truncate to multiple of DUTY_STEP
// if( mod >= DUTY_STEP / 2 )
// trial += DUTY_STEP;
return trial;
}
#endif // end if defined ANALOGUE1 || defined ANALOGUE2
#ifdef ANALOGUE1
// ---------------------------------
void readAnlg1() { // read analog port 1 and adjust OT1 output
char pstr[5];
int32_t reading;
reading = getAnalogValue( anlg1 );
if( reading <= 100 && reading != old_reading_anlg1 ) { // did it change?
analogue1_changed = true;
old_reading_anlg1 = reading; // save reading for next time
#ifdef PHASE_ANGLE_CONTROL
#ifdef IO3_HTR_PAC
levelIO3 = reading;
outIO3();
#else
levelOT1 = reading;
outOT1();
#endif
#else // PWM Mode
levelOT1 = reading;
outOT1();
#endif
}
else {
analogue1_changed = false;
}
}
#endif // end ifdef ANALOGUE1
#ifdef ANALOGUE2
// ---------------------------------
void readAnlg2() { // read analog port 2 and adjust OT2 output
char pstr[5];
int32_t reading;
reading = getAnalogValue( anlg2 );
if( reading <= 100 && reading != old_reading_anlg2 ) { // did it change?
analogue2_changed = true;
old_reading_anlg2 = reading; // save reading for next time
#ifdef PHASE_ANGLE_CONTROL
#ifdef CONFIG_PAC2_IO3FAN
levelIO3 = reading;
outIO3(); // update fan output on IO3
#else
levelOT2 = reading;
outOT2(); // update fan output on OT2
#endif
#else // PWM Mode
levelIO3 = reading;
outIO3(); // update fan output on IO3
#endif
}
else {
analogue2_changed = false;
}
}
#endif // end ifdef ANALOGUE2
#ifdef PID_CONTROL
// ---------------------------------
void updateSetpoint() { //read profile data from EEPROM and calculate new setpoint
if(profile_number > 0 ){
while( counter < times[0] || counter >= times[1] ) { // if current time outside currently loaded interval then adjust profile pointer before reading new interval data from EEPROM
if( counter < times[0] ) {
profile_ptr = profile_ptr - 2; // two bytes per int
}
else {
profile_ptr = profile_ptr + 2; // two bytes per int
}
eeprom.read( profile_ptr, (uint8_t*)×, sizeof(times) ); // read two profile times
eeprom.read( profile_ptr + 100, (uint8_t*)&temps, sizeof(temps) ); // read two profile temps. 100 = size of time data
if( times[1] == 0 ) {
Setpoint = 0;
myPID.SetMode(MANUAL); // deactivate PID control
Output = 0; // set PID output to 0
break;
}
}
float x = (float)( counter - times[0] ) / (float)( times[1] - times[0] ); // can probably be tidied up?? Calcs proportion of time through current profile interval
Setpoint = temps[0] + x * ( temps[1] - temps[0] ); // then applies the proportion to the temps
if( profile_CorF == 'F' && Cscale ) { // make setpoint units match current units
Setpoint = convertUnits( Setpoint ); // convert F to C
}
else if( profile_CorF == 'C' & !Cscale) { // make setpoint units match current units
Setpoint = Setpoint * 9 / 5 + 32; // convert C to F
}
}
else {
Setpoint = SV;
}
}
void setProfile() { // set profile pointer and read initial profile data
profile_number_new = profile_number; // sync profile_number_new in case PID;P;x command was recieved
if( profile_number > 0 ) {
profile_ptr = 1024 + ( 400 * ( profile_number - 1 ) ) + 4; // 1024 = start of profile storage in EEPROM. 400 = size of each profile. 4 = location of profile C or F data
eeprom.read( profile_ptr, (uint8_t*)&profile_CorF, sizeof(profile_CorF) ); // read profile temp type
getProfileDescription(profile_number); // read profile name and description data from eeprom for active profile number
profile_ptr = 1024 + ( 400 * ( profile_number - 1 ) ) + 125; // 1024 = start of profile storage in EEPROM. 400 = size of each profile. 125 = size of profile header data
eeprom.read( profile_ptr, (uint8_t*)×, sizeof(times) ); // read 1st two profile times
eeprom.read( profile_ptr + 100, (uint8_t*)&temps, sizeof(temps) ); // read 1st two profile temps. 100 = size of time data
// profile_ptr is left set for profile temp/time reads
}
//else //do something?
}
void getProfileDescription(int pn) { // read profile name and description data from eeprom
if( profile_number > 0 ) {
int pp = 1024 + ( 400 * ( pn - 1 ) ) + 5; // 1024 = start of profile storage in EEPROM. 400 = size of each profile. 5 = location of profile name
eeprom.read( pp, (uint8_t*)&profile_name, sizeof(profile_name) ); // read profile name
pp = 1024 + ( 400 * ( pn - 1 ) ) + 45; // 1024 = start of profile storage in EEPROM. 400 = size of each profile. 45 = location of profile description
eeprom.read( pp, (uint8_t*)&profile_description, sizeof(profile_description) ); // read profile name
}
//else //do something?
}
#endif // end ifdef PID_CONTROL