-
Notifications
You must be signed in to change notification settings - Fork 0
/
SousVide.ino
986 lines (872 loc) · 33.2 KB
/
SousVide.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
/*
ARDUINO SOUS VIDE AND CROCKPOT CONTROLLER v1.0.1
================================================
Zan Hecht - 6 Dec 2017
http://zansstuff.com/sous-vide */
#define VER_1 1
#define VER_2 0
#define VER_3 1
/*The following Arduino libraries are required to compile:
* TM1638.h: https://github.com/rjbatista/tm1638-library
* OneWire.h: https://github.com/PaulStoffregen/OneWire
* DallasTemperature.h: https://github.com/milesburton/Arduino-Temperature-Control-Library
* PID_v1.h: https://github.com/br3ttb/Arduino-PID-Library/
Concept and original code inspired by the article "Turn your crock pot into a
PID controlled sous vide cooker for $25" by [email protected] at
http://chiefmarley.com/Arduino/?p=3
PID settings for coffee urn and "aggressive mode" taken from the Ember source
code from Lower East Kitchen https://github.com/abefetterman/Ember-Kit
Temperature input via Dallas DS18B20 One-Wire temperature sensor. Datasheet at
http://www.maxim-ic.com/datasheet/index.mvp/id/2812
UI is via a TM1638-based I/O module with an 8-character 7-seg display, 8 red/
green LEDs, and 8 buttons. This is available from DealExtreme as "8X Digital
Tube + 8X Key + 8X Double Color LED Module" at
http://dealextreme.com/p/module-81873
Written for Arduino 1.0 http://arduino.cc/hu/Main/Software
This program contains code by Brett Beauregard to communicate with the
Processing PID front end using the code available at
http://code.google.com/p/arduino-pid-library/ and
the Processing software available at http://processing.org/download
INSTRUCTIONS
------------
Key assignments on the TM1638 are as follows (LSB to MSB):
TEMP DOWN | TEMP UP | HRS DOWN | HOURS UP | MINS DOWN | MINS UP | MODE | SET
LEDs show the power being applied to the crock-pot via PWM. 8 LEDs = 100%, no
LEDs = 0%. If the LEDs are red, the relay is on. If the LEDs are green, the
relay is off.
Hit "MODE" to switch between modes. Modes are described below:
### ELAPSED TIME ("tinEr ")
> Timer counts hours, minutes, and seconds. By default, the crock pot is off.
> * SET: Toggle between timer display and "PErcEnt" setting mode described below
> * MINS DOWN: Reset Time
> * HRS DOWN: Reset Time
> * TEMP UP: Turn on crock-pot at power percentage specified in "PErcEnt"
> > setting. Display will read ("On##.##.##").
> * TEMP DOWN: Turn off crock-pot
>
> #### Settings:
> > ##### COOKING POWER PERCENTAGE ("PErcEnt")
> > Scale the crockpot cooking power by the specified percentage. Use TEMP UP
> > and TEMP DOWN to adjust the percentage up or down by 5%.
### SOUS VIDE ("SOUSUIdE")
> Sous Vide cooking mode. This mode can only be accessed when a temperature
> sensor is detected at startup.
>
> Crock-pot or urn should be set on HIGH, filled with water, and have some
> sort of forced circulation (pump or bubbler). Temperature sensor shold be
> in the water away from the sides or bottom.
>
> Temperatures are diplayed in Celcius. Temperature on the left is the
> setpoint, temperature on the right is the current temperature.
>
> * SET: Cycle through settings described below
> * TEMP UP: Raise the setpoint by 0.5°
> * TEMP DOWN: Lower the setpoint by 0.5°
>
> #### Settings:
> > ##### SOUS VIDE TIMER ("S.U. tinEr")
> > Functions much like the TIMER mode above. Reset by using HRS DOWN or MINS
> > DOWN.
> >
> > ##### PROPORTIONAL ("Proport. ")
> > Set the "P" coefficient in the PID loop. Use TEMP UP and TEMP DOWN to
> > increase and decrease the coefficient by 100
> >
> > ##### INTEGRAL ("IntEgrAL")
> > Set the "I" coefficient in the PID loop. Use TEMP UP and TEMP DOWN to
> > increase and decrease the coefficient by 0.05
> >
> > ##### DERIVATIVE ("dErIvAt. ")
> > Set the "D" coefficient in the PID loop. Use TEMP UP and TEMP DOWN to
> > increase and decrease the coefficient by 1
> >
> > ##### CALIBRATE TEMPERATURE ("CALibrAt.")
> > Offset the reading from the temperature probe by the amount shown. Use
> > TEMP UP and TEMP DOWN to increase and decrease the offset by 0.1 degrees
> > celsius. Calibration offset is saved to flash memory when you press SET.
### COOK AND HOLD ("CrockPot")
> Acts like the timer function on more expensive crock-pots. Cooks at power
> power specified in the Percent setting until the time runs out and then
> decreases the power to the equivalent of the crock-pot WARM setting. As
> measured on my crock-pot, WARM is approximately 30% of the crock-pot's HIGH
> setting and 40% of the crock-pot's LOW setting. There is no need to start or
> stop the timer -- it is always running. The first two letters displayed
> indicate whether the keep warm mode is calibrated for the Crockpot's knob
> being set to High ("CH") or to Low ("CL").
>
> **IMPORTANT: YOU MUST SPECIFY WHETHER THE CROCKPOT'S KNOB IS SET TO HIGH**
> **("CH") OR LOW ("CL") OR THE KEEP WARM MODE MAY NOT KEEP YOUR FOOD HOT**
> **ENOUGH TO BE SAFE! THE "CH" AND "CL" SETTINGS DO NOT CHANGE THE**
> **TEMPERATURE OF THE CROCKPOT -- YOU MUST DO THAT YOURSELF WITH THE**
> **CROCKPOT'S KNOB.**
>
> * SET: Toggle between countdown display and "PErcEnt" setting mode
> * HRS UP, HRS DOWN, MINS UP, MINS DOWN: Set the countdown timer time
> * TEMP UP: Set to "Crockpot on HIGH" ("CH")
> * TEMP DOWN: Set to "Crockpot on LOW" ("CL")
>
> #### Settings:
> > ##### COOKING POWER PERCENTAGE ("PErcEnt")
> > Scale the crockpot cooking power by the specified percentage. Use TEMP UP
> > and TEMP DOWN to adjust the percentage up or down by 5%.
### DELAYED START ("DELAY St.")
> Turns the crockpot on to power specified in Percent setting after the time
> runs out. There is no need to start or stop the timer -- it is always running.
> Once the time elapses, the Crockpot will stay on until more time is added, the
> controller is unplugged, or the controller is switched to another mode such as
> SOUS VIDE or ELAPSED TIME.
>
> **WARNING: DO NOT USE THIS MODE WITH PERISHIBLE FOOD IN A THE CROCKPOT!**
> **FOOD IN THE CROCKPOT CAN TAKE TWO OR THREE HOURS TO COME TO A SAFE**
> **TEMPERATURE ONCE THE HEAT IS TURNED ON, MAKING IT EASY FOR THIS MODE TO**
> **CAUSE FOOD TO SIT IN THE "DANGER ZONE" FOR TOO LONG.**
>
> * SET: Toggle between countdown display and "PErcEnt" setting mode
> * HRS UP, HRS DOWN: Adjust the hours until the crockpot starts
> * MINS UP, MINS DOWN: Adjust the minutes until the crockpot starts
>
> #### Settings:
> > ##### COOKING POWER PERCENTAGE ("PErcEnt")
> > Scale the crockpot cooking power by the specified percentage. Use TEMP UP
> > and TEMP DOWN to adjust the percentage up or down by 5%.
### COOK TO TEMPERATURE ("DonEtEnP")
> Turns the crockpot on to power specified in Percent setting until target
> temperature is reached. Once target temperature is reached, turn the crockpot
> to warm mode.
>
> The first character of the display shows the keep warm setting -- "H"
> indicates that it is calibrated for the CrockPot's knob being set to high, "L"
> indicates that it is calibrated for the knob being on low. This is adjustable
> through the SETTINGS button. The display then shows the target temperature
> in whole degrees followed by the current temperature. The target temperature
> display will change to "--" when the target temperature is reached, and will
> reset back to the default temperature if any temperature adjust button is
> pressed.
>
> **IMPORTANT: YOU MUST SPECIFY WHETHER THE CROCKPOT'S KNOB IS SET TO HIGH**
> **("CH") OR LOW ("CL") OR THE KEEP WARM MODE MAY NOT KEEP YOUR FOOD HOT**
> **ENOUGH TO BE SAFE! THE "CH" AND "CL" SETTINGS DO NOT CHANGE THE**
> **TEMPERATURE OF THE CROCKPOT -- YOU MUST DO THAT YOURSELF WITH THE**
> **CROCKPOT'S KNOB.**
>
> * SET: Cycle through settings described below
> * TEMP UP: Raise the target temperature by 1°
> * TEMP DOWN: Lower the target temperature by 1°
>
> #### Settings:
> > ##### COOKING POWER PERCENTAGE ("PErcEnt")
> > Scale the crockpot cooking power by the specified percentage. Use TEMP UP
> > and TEMP DOWN to adjust the percentage up or down by 5%.
> >
> > ##### CROCKPOT KNOB SETTING ("CP Hi Lo")
> > Calibrate the keep-warm temperature for the Crockpot's knob being on High
> > or Low. Press TEMP UP for High ("Hi") and TEMP DOWN for Low ("Lo").
*/
/*
TM1638 Pinout
GND VCC
DIO CLK
SB1 SB0
SB3 SB2
SB5 SB4
By default, DI0-->8, CLK-->9, SB0-->7
*/
#include <TM1638.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <PID_v1.h>
#include <stdio.h>
#include <EEPROM.h>
// Enable serial output to Processing UI (0 to disable, 1 to enable)
#define SERIAL_ENABLE 1
// One-wire data wire is plugged into pin 2 on the Arduino
#define ONE_WIRE_BUS 2
// define TM1638 module connected to data pin 8, clock pin 9, and strobe n 7
#define T1638_D 8
#define T1638_C 9
#define T1638_S 7
// initial set point
#define INITIAL_SET_POINT 62.5
// pin to trigger relay
#define TRIGGER_PIN 10
// Temp sensor resolution in bits
#define SENSOR_RESOLUTION 11
// Temp sensor polling time in milliseconds
// (allow 750ms for 12-bit temp conversion, 375ms for 11-bit, etc.)
#define TEMP_TIME 400
// Number of temperature readings to average
#define TEMP_NUM_READINGS 5
// PWM Period in seconds
#define WINDOW_SIZE 8
// PID Parameters
// For Rival 6-qt crock pot set on "High": KP=2000, KI=0.25, KD=0
// For coffee urn: KP=1500, KI=1, KD=0
#define INITIAL_KP 1500
#define INITIAL_KI 1
#define INITIAL_KD 0
// PID parameters for "aggressive mode", which is activated whenever the
// temperature is > AGR_INT from the setpoint
#define AGR_KP 3000
#define AGR_KI 0
#define AGR_KD 0
#define AGR_INT 3
// CrockPot Cooking Settings
#define CP_HIGH_PCT 100 // Percent Power for crockpot on 'High'. Should be 100.
#define CP_LOW_PCT 70 // Percent Power for crockpot on 'Low'
#define CP_WARM_PCT 30 // Percent Power for crockpot on 'Warm'
#define CP_ADJ_PCT 100 // Crocopot offset for all levels
// Time to display mode labels, in milliseconds
#define LABEL_TIME 1250
// Debounce time for buttons, in milliseconds
#define DEBOUNCE_TIME 200
//Keep warm percentages
const int cpLowOutput = (float(CP_WARM_PCT)/float(CP_LOW_PCT)) * WINDOW_SIZE * 10;
const int cpHighOutput= (float(CP_WARM_PCT)/float(CP_HIGH_PCT)) * WINDOW_SIZE * 10;
// Setup a oneWire instance to communicate with any OneWire devices
OneWire oneWire(ONE_WIRE_BUS);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);
DeviceAddress tempDeviceAddress;
// define a module on data pin 8, clock pin 9 and strobe pin 7
TM1638 module(T1638_D, T1638_C, T1638_S);
//Define and initialize variables we'll be connecting to
double Setpoint = INITIAL_SET_POINT, Input = -127, Output = 0;
//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint,INITIAL_KP,INITIAL_KI,INITIAL_KD,DIRECT);
unsigned long serialTime; //this will help us know when to talk with processing
unsigned long windowStartTime; //PWM Window start time
unsigned long tempTime; //time to check the thermometer
unsigned long keyTime = 0; //When a button was last pressed
unsigned long labelTime = millis(); //When a label was last displayed
unsigned long tReset = 0; //Variable for resetting timer
double tempReadings[TEMP_NUM_READINGS] = { 0 }; //Array of historical temps
char tempIndex = 0; //Where to store the current temperature in the array
double tempTotal = 0; //Running total of temperature array
long countDn = 0; //Variable for Countdown timer
char dispStr[9]; //String to display on LED
char highLow = 'L'; //For countdown timer, is crockpot on High or Low?
byte ledDots = 0, mode=0, settings=0, onOff=0;
bool isSensor=false; //Flag for determining if a sensor is connected
int cpPercent=CP_ADJ_PCT; //Crockpot temperature scaling factor for non-Sous Vide
byte tOff, oldTOff; //Temperature Calibration Offset
/**********************************************
* Setup *
**********************************************/
void setup() {
if(SERIAL_ENABLE) { // start serial port
Serial.begin(9600);
Serial.println(F("ARDUINO SOUS VIDE AND CROCKPOT CONTROLLER"));
Serial.print(F(" VERSION "));
Serial.print(VER_1); Serial.print(F("."));
Serial.print(VER_2); Serial.print(F("."));
Serial.print(VER_3); Serial.println(F(" \n"));
}
//Read from and initialize (if necessary) EEPROM
tOff = EEPROM.read(0);
oldTOff = tOff;
//Check for signature of current version.
//Byte 0 is the calibration offset (27 to 227),
//Byte 1 is the first part of the version number,
//Byte 2 is the section part of the version number,
//Byte 42 should be set to 42.
if((tOff != 255) && (EEPROM.read(1) == VER_1)
&& (EEPROM.read(2) == VER_2) && (EEPROM.read(42) == 42)) {
if(SERIAL_ENABLE) {
Serial.print(F("EEPROM Signature Check OK: "));
Serial.print(tOff);
SerialSpace();
Serial.print(EEPROM.read(1));
SerialSpace();
Serial.print(EEPROM.read(2));
SerialSpace();
Serial.println(EEPROM.read(42));
}
} else {
if(SERIAL_ENABLE) {
Serial.print(F("EEPROM Signature Check FAILED: "));
Serial.print(tOff);
SerialSpace();
Serial.print(EEPROM.read(1));
SerialSpace();
Serial.print(EEPROM.read(2));
SerialSpace();
Serial.println(EEPROM.read(42));
Serial.print(F("Writing to EEPROM... "));
}
tOff=127;
oldTOff=tOff;
EEPROM.write(0,tOff);
EEPROM.write(1,VER_1);
EEPROM.write(2,VER_2);
EEPROM.write(42,42);
if(SERIAL_ENABLE) {Serial.println(F("DONE"));}
}
//Initialize PWM Timer
windowStartTime = millis();
if(SERIAL_ENABLE) {Serial.println(F("Initializing Temperature Sensor..."));}
//Start Dallas Library and initialize temp sensor
sensors.begin();
isSensor = sensors.getAddress(tempDeviceAddress, 0);
sensors.setWaitForConversion(false); // async mode
if(isSensor) {
if(SERIAL_ENABLE) {
Serial.print(F("Temperature Sensor Found: "));
Serial.println(tempDeviceAddress[0]);
}
sensors.setResolution(tempDeviceAddress, SENSOR_RESOLUTION);
sensors.requestTemperatures(); // Send the command to get temperatures
} else if(SERIAL_ENABLE) {
Serial.println(F("No Temperature Sensor Found"));
}
tempTime = millis()+TEMP_TIME; // Set the timer to retrieve temps
//Initialize PID
myPID.SetOutputLimits(0, WINDOW_SIZE*1000); //set PID output to correct range
myPID.SetSampleTime(TEMP_TIME); //set sample time in milliseconds
//turn on display to brightness 0 (0-7)
module.setupDisplay(true, 0);
//activate relay pin as an output
digitalWrite(TRIGGER_PIN, LOW); // sets the relay off
pinMode(TRIGGER_PIN, OUTPUT);
if(SERIAL_ENABLE) {Serial.println(F("Controller Ready\n"));}
delay(5);
}
/**********************************************
* Loop *
**********************************************/
void loop() {
// Record current time
unsigned long now = millis();
//Register button presses
byte keys = 0;
if(now - keyTime > DEBOUNCE_TIME) {
keys = module.getButtons();
if(keys){
keyTime = now;
switch (keys) { //Switch settings and modes
case 0b10000000:
if ((mode == 1 && settings < 5)
|| (mode == 4 && settings < 2)
|| (mode != 1 && mode != 4 && settings < 1)) {
settings++;
} else {
settings = 0;
}
labelTime = now;
break;
case 0b01000000:
if (mode < 4) {
mode++;
} else {
mode = 0;
}
settings = 0;
labelTime = now;
break;
}
}
}
// Calculate output mapping to LEDs
word outToLed = (1 << (char)((Output/1000) +0.5)) - 1;
// Poll temp sensor every TEMP_TIME milliseconds and apply calibration offset
if(now>tempTime && isSensor) Input = getTemps() + (double)(tOff-127)/10;
//Call PID Function.
//Most of the time it will just return without doing anything.
//At a frequency specified by SetSampleTime it will calculate a new Output.
if ((abs(Setpoint-Input)>=AGR_INT)) { //Check for "aggressive mode"
double p, i, d; //Read current tuning
p = myPID.GetKp();
i = myPID.GetKi();
d = myPID.GetKd();
myPID.SetTunings(AGR_KP, AGR_KI, AGR_KD); //Set aggressive mode tunings
myPID.Compute(); //Call PID Function.
myPID.SetTunings(p, i, d); //Restore old tunings
} else {
myPID.Compute(); //Call PID Function.
}
//turn the output pin on/off based on pid output
//If one PWM time window has ended, it's time to shift the window
if((now - windowStartTime)>(WINDOW_SIZE * 1000)) windowStartTime += (WINDOW_SIZE * 1000);
if(Output > now - windowStartTime) { //Turn on relay during "on" period
digitalWrite(TRIGGER_PIN,HIGH);
} else {//Turn off relay
digitalWrite(TRIGGER_PIN,LOW);
outToLed = outToLed << 8; //Shift LED bar to make LEDs green
}
//Determine which mode we are in and perform the appropriate actions
switch (mode) {
case 0:
doTimer(keys); break; //Simple time-elapsed Mode with crockpot off.
case 1:
doSousVide(keys); break; // Sous Vide Mode
case 2:
doCountdown(keys); break; // Countdown Timer
case 3:
doDelayStart(keys); break; // Delayed Start
case 4:
doDoneTemp(keys); break; // Cook to Temperature
}
//Display label if mode recently switched
if ((now - labelTime) < LABEL_TIME) {
doLabel();
if(SERIAL_ENABLE && (labelTime == now)) {
Serial.print(F("Mode: ")); Serial.print(mode);
Serial.print(F("; Settings: ")); Serial.println(settings);
}
}
//write to display
//if(SERIAL_ENABLE) {Serial.println(dispStr);}
module.setDisplayToString(dispStr,ledDots);
module.setLEDs(outToLed);
//send-receive with processing if it's time
if(millis()>serialTime && SERIAL_ENABLE && mode==1) {
SerialReceive();
SerialSend();
serialTime+=(TEMP_TIME*10);
}
}
/**********************************************
* Input/Output Functions *
**********************************************/
// Query temp sensor, perform a running average, and send request for next poll
double getTemps() {
tempTotal -= tempReadings[tempIndex]; //subtract oldest temperature from running total
tempReadings[tempIndex] = sensors.getTempC(tempDeviceAddress); //read sensor at tempDeviceAddress
tempTotal += tempReadings[tempIndex]; //add reading to running total
tempIndex++; //Advance to the next slot in the array for averaging
if (tempIndex>=TEMP_NUM_READINGS) tempIndex=0; //Wrap around if at end of array
sensors.setResolution(tempDeviceAddress, SENSOR_RESOLUTION); //Set resolution again for safety
sensors.requestTemperatures(); //Send the command to request temperatures
tempTime = millis()+TEMP_TIME; //Increment timer to retrieve temps
return tempTotal / TEMP_NUM_READINGS; //Average the temperature array to get the running average
}
//Display mode label
void doLabel() {
switch (mode) {
case 0:
switch (settings) {
case 0:
sprintf(dispStr,"tinEr "); ledDots=0b00000000; break; //Elapsed Time
case 1:
sprintf(dispStr,"PErcEnt "); ledDots=0b00000000; break; // Set Cooking Percent
}
break;
case 1:
switch (settings) {
case 0:
sprintf(dispStr,"SOUSUIdE"); ledDots=0b00000000; break; //Sous Vide Mode
case 1:
sprintf(dispStr,"SU tinEr"); ledDots=0b11000000; break; //Elapsed Sous Vide Time
case 2:
sprintf(dispStr,"Proport "); ledDots=0b00000010; break; //Set Proportional Coefficient
case 3:
sprintf(dispStr,"IntEgrAL"); ledDots=0b00000000; break; //Set Integral Coefficient
case 4:
sprintf(dispStr,"dEriuAt "); ledDots=0b00000010; break; //Set Derivative Coefficient
case 5:
sprintf(dispStr,"CALibrAt"); ledDots=0b00000001; break; //Set temperature offset
}
break;
case 2:
switch (settings) {
case 0:
sprintf(dispStr,"CrockPot"); ledDots=0b00000000; break; // Cooking Time
case 1:
sprintf(dispStr,"PErcEnt "); ledDots=0b00000000; break; // Set Cooking Percent
}
break;
case 3:
switch (settings) {
case 0:
sprintf(dispStr,"DELAY St"); ledDots=0b00000001; break; // Delayed Start
case 1:
sprintf(dispStr,"PErcEnt "); ledDots=0b00000000; break; // Set Cooking Percent
}
break;
case 4:
switch (settings) {
case 0:
sprintf(dispStr,"DonEtEnP"); ledDots=0b00000000; break; // Cook to Temperature
case 1:
sprintf(dispStr,"PErcEnt "); ledDots=0b00000000; break; // Set Cooking Percent
case 2:
sprintf(dispStr,"CP Hi Lo"); ledDots=0b00000000; break; // Set Crockpot Knob Setting
}
break;
}
}
//Simple time-elapsed Mode with crockpot off.
void doTimer(byte keys) {
myPID.SetMode(MANUAL); //Turn PID off
if(settings == 0) {
unsigned long now = millis();
unsigned long tTime = (now - tReset)/1000; //Get elapsed time in seconds
if(keys & 0b10010100) tReset = now; //reset if any time down or the Set button is pushed
if(keys == 0b00000010) onOff = 1; //Turn on crockpot if Temperature Up button is pushed
else if(keys == 0b00000001) onOff = 0; //Turn off crockpot if Temperature Down button is pushed
// Calculate hours, minutes, and seconds
int tSec = tTime % 60;
int tMin = (tTime / 60) % 60;
int tHr = tTime / 3600;
if (onOff) { //Toggle output and display based on whether crockpot is on or off
sprintf(dispStr,"On%02hi%02hi%02hi\n",tHr,tMin,tSec); //Create output string
Output = WINDOW_SIZE * 10 * cpPercent;
} else {
sprintf(dispStr," %02hi%02hi%02hi\n",tHr,tMin,tSec); //Create output string
Output = 0;
}
ledDots = 0b00010100; //Set dots to separate hours, minutes, and seconds.
} else if (settings == 1) {
setPct(keys);
}
}
//Sous Vide Mode
void doSousVide(byte keys) {
//Read in current pid tunings
double p, i, d;
p = myPID.GetKp();
i = myPID.GetKi();
d = myPID.GetKd();
switch (settings) {
case 0: //Temperature Display
sprintf(dispStr,"---*---*\n");
ledDots = 0b00000000;
//Write new tOff to EEPROM if necessary
if (tOff != oldTOff) {
EEPROM.write(0,tOff);
oldTOff = tOff;
if(SERIAL_ENABLE) {
Serial.print(F("Updating EEPROM byte 0: "));
Serial.println(tOff);
}
}
break;
case 1:
setTimer(keys); break; //Timer
case 2:
p=setPro(keys, p); break; //Set Proportional Coefficient
case 3:
i=setInt(keys, i); break; //Set Integral Coefficient
case 4:
d=setDer(keys, d); break; //Set Derivative Coefficient
case 5:
tOff=setCal(keys, tOff); break; //Set temperature offset
}
if(settings>=2 && settings<=4) myPID.SetTunings(p, i, d); //Set tunings if in p, i, or d mode
if(isSensor && Input > 0) { // Is there a sensor and valid temperature data?
myPID.SetMode(AUTOMATIC); //Turn PID on once valid temperature data exists
if(settings == 0) setSousVide(keys); //Temperature Display
}
}
// Countdown Timer
void doCountdown(byte keys) {
myPID.SetMode(MANUAL); //Turn PID off
long tempTime = (long)millis();
long cMs = countDn - tempTime;
if (cMs < 0) cMs = 0;
long cHr = cMs / 3600000;
cMs -= (cHr * 3600000);
long cMin = cMs / 60000;
cMs -= (cMin * 60000);
long cSec = cMs / 1000;
cMs -= (cSec * 1000);
if (settings == 0) {
switch (keys) {
case 0b00001000:
cHr = (cHr+1)%24; break; //Hour Up
case 0b00000100:
cHr = (cHr-1)%24; break; //Hour Down
case 0b00100000:
cMin = (cMin+1); break; //Min Up
case 0b00010000:
cMin = (cMin-1); break; //Min Down
case 0b1000000:
highLow = (highLow == 'L') ? 'H' : 'L'; break; // Toggle High/Low settings
case 0b00000010:
highLow = 'H'; break;
case 0b00000001:
highLow = 'L'; break;
}
sprintf(dispStr,"C%c%02hi%02hi%02hi\n",highLow,(int)cHr,(int)cMin,(int)cSec);
ledDots = 0b00010100;
} else if (settings == 1) {
setPct(keys);
}
cMs = (cHr*3600000)+(cMin*60000)+(cSec*1000)+cMs;
countDn = cMs + tempTime;
if (cMs > 500) {
Output = WINDOW_SIZE * 10 * cpPercent;
} else {
if (highLow == 'L') Output = cpLowOutput * cpPercent;
else Output = cpHighOutput * cpPercent;
}
}
// Delayed Start
void doDelayStart(byte keys) {
myPID.SetMode(MANUAL); //Turn PID off
long tempTime = (long)millis();
long cMs = countDn - tempTime;
if (cMs < 0) cMs = 0;
long cHr = cMs / 3600000;
cMs -= (cHr * 3600000);
long cMin = cMs / 60000;
cMs -= (cMin * 60000);
long cSec = cMs / 1000;
cMs -= (cSec * 1000);
if (settings == 0) {
switch (keys) {
case 0b00001000:
cHr = (cHr+1)%24; break; //Hour Up
case 0b00000100:
cHr = (cHr-1)%24; break; //Hour Down
case 0b00100000:
cMin = (cMin+1); break; //Min Up
case 0b00010000:
cMin = (cMin-1); break; //Min Down
}
sprintf(dispStr,"dL%02hi%02hi%02hi\n",(int)cHr,(int)cMin,(int)cSec);
ledDots = 0b00010100;
} else if (settings == 1) {
setPct(keys);
}
cMs = (cHr*3600000)+(cMin*60000)+(cSec*1000)+cMs;
countDn = cMs + tempTime;
if (cMs > 500) Output = 0;
else Output = WINDOW_SIZE * 10 * cpPercent;
}
void doDoneTemp(byte keys) {
myPID.SetMode(MANUAL); //Turn PID off
if(settings == 0) { //Temperature Display
if (isSensor && Input > 0) { // Is there a sensor and valid temperature data?
if (Setpoint) {
if (keys == 0b00000010 && Setpoint <= 99) Setpoint=((int)Setpoint)+1;
else if (keys == 0b00000001 && Setpoint >= 10) Setpoint=((int)Setpoint)-1;
} else if (keys & 0b00000011) { //Setpoint is 0 and Up or Down pressed
Setpoint = (int)(INITIAL_SET_POINT+0.5);
}
if (Setpoint) {
sprintf(dispStr,"%c%02d*%03d*\n",highLow,(int)((Setpoint)+0.5),(int)((Input*10)+0.5));
ledDots = 0b00000100; //Set decimal points in temperatures
} else {
sprintf(dispStr,"%c--*%03d*\n",highLow,(int)((Input*10)+0.5));
ledDots = 0b00000100; //Set decimal points in temperatures
}
} else {
sprintf(dispStr,"%c--*---*\n",highLow);
ledDots = 0b00000000;
}
} else if (settings == 1) {
setPct(keys);
} else if (settings == 2) {
setHighLow(keys);
}
if (Input >= Setpoint) Setpoint = 0;
if (Setpoint) {
Output = WINDOW_SIZE * 10 * cpPercent;
} else {
if (highLow == 'L') Output = cpLowOutput * cpPercent;
else Output = cpHighOutput * cpPercent;
}
}
/********************************************
* Settings Modes *
********************************************/
void setSousVide(byte keys) { //Temperature Display
/*
// display Fahrenheit formatted temperatures
sprintf(dispStr,"%3hi%3hi\n",(int)(sensors.toFahrenheit(Setpoint)*10),(int)(sensors.toFahrenheit(Input)*10));
*/
// display Celsius formatted temperatures
sprintf(dispStr,"%03d*%03d*\n",(int)((Setpoint*10)+0.5),(int)((Input*10)+0.5));
ledDots = 0b01000100; //Set decimal points in temperatures
//Process Buttons
// Setpoint up or down
if ((keys == 0b00000010) && (Setpoint <= 99)) Setpoint=Setpoint+0.5;
else if ((keys == 0b00000001) && (Setpoint >= 10.0)) Setpoint=Setpoint-0.5;
}
void setTimer(byte keys) { //Timer
unsigned long tTime = (millis() - tReset)/1000;
if(keys & 0b00010100) tReset = millis(); //reset if time dn buttons pushed
int tSec = tTime % 60;
int tMin = (tTime / 60) % 60;
int tHr = tTime / 3600;
sprintf(dispStr,"t %02hi%02hi%02hi\n",tHr,tMin,tSec);
ledDots = 0b10010100; //Set dots to separate hours, minutes, and seconds.
}
double setPro(byte keys, double p) { //Set Proportional Coefficient
sprintf(dispStr,"Pro %4hi\n",(int)p);
ledDots = 0b00100000;
//Process Buttons
if ((keys == 0b00000010) && (p < 9900)) p+=100; //Increase coeff.
else if ((keys == 0b00000001) && (p >= 100)) p-=100; //Decrease coeff.
return p;
}
double setInt(byte keys, double i) { //Set Integral Coefficient
sprintf(dispStr,"Int %4hi\n",(int)((i*100)+0.5));
ledDots = 0b00100100;
//Process Buttons
if ((keys == 0b00000010) && (i < 99.95)) i+=0.05; //Increase coeff.
else if ((keys == 0b00000001) && (i >= 0.05)) i-=0.05; //Decrease coeff.
return i;
}
double setDer(byte keys, double d) { //Set Derivative Coefficient
sprintf(dispStr,"dEr %4hi\n",(int)d);
ledDots = 0b00100000;
//Process Buttons
if ((keys == 0b00000010) && (d < 9999)) d++; //Increase coefficient
else if ((keys == 0b00000001) && (d >= 1)) d--; //Decrease coefficient
return d;
}
double setCal(byte keys, byte cal) { //Set temp sensor calibration
sprintf(dispStr,"CAL %+03hi\n", (int)cal-127);
ledDots = 0b00100010;
//Process Buttons
if ((keys == 0b00000010) && (cal < 226)) cal+=1; //Increase temp offset
else if ((keys == 0b00000001) && (cal > 28)) cal-=1; //Decrease temp offset
return cal;
}
void setPct(byte keys) { //Set Cooking Percent
sprintf(dispStr,"Pct %3hi\n",cpPercent);
ledDots = 0b00100000;
//Process Buttons
if ((keys == 0b00000010)&&(cpPercent <= 95)) cpPercent+=5; //Increase
else if ((keys == 0b00000001)&&(cpPercent >= 10)) cpPercent-=5; //Decrease
}
void setHighLow (byte keys) { //Set hold temp for Crockpot Knob on high or low
ledDots = 0b00000000;
if (highLow == 'H') strcpy(dispStr,"CP on Hi");
else if (highLow == 'L') strcpy(dispStr,"CP on Lo");
if (keys == 0b00000010) highLow = 'H';
else if (keys == 0b00000001) highLow = 'L';
}
/**********************************************
* Serial Communication functions / helpers *
**********************************************/
union { // This Data structure lets
byte asBytes[24]; // us take the byte array
float asFloat[6]; // sent from processing and
} // easily convert it to a
foo; // float array
// the bytes coming from the arduino follow the following
// format:
// 0: 0=Manual, 1=Auto, else = ? error ?
// 1: 0=Direct, 1=Reverse, else = ? error ?
// 2-5: float setpoint
// 6-9: float input
// 10-13: float output
// 14-17: float P_Param
// 18-21: float I_Param
// 22-245: float D_Param
void SerialReceive()
{
// read the bytes sent from Processing
int index=0;
byte Auto_Man = -1;
byte Direct_Reverse = -1;
while(Serial.available()&&index<26)
{
if(index == 0) Auto_Man = Serial.read();
else if(index == 1) Direct_Reverse = Serial.read();
else foo.asBytes[index-2] = Serial.read();
index++;
}
// if the information we got was in the correct format,
// read it into the system
if(index == 26 && (Auto_Man == 0 || Auto_Man == 1)
&& (Direct_Reverse == 0 || Direct_Reverse == 1))
{
Setpoint=double(foo.asFloat[0]);
//Input=double(foo.asFloat[1]); // * the user has the ability to send the
// value of "Input". In most cases (as
// in this one) this is not needed.
if(Auto_Man == 0) // * only change the output if we
{ // are in manual mode. Otherwise
Output=double(foo.asFloat[2]); // we'll get an output blip, then
} // the controller will overwrite.
double p, i, d; // * read in and set the controller tunings
p = double(foo.asFloat[3]); //
i = double(foo.asFloat[4]); //
d = double(foo.asFloat[5]); //
myPID.SetTunings(p, i, d); //
if(Auto_Man == 0) myPID.SetMode(MANUAL); // * set the controller mode
else myPID.SetMode(AUTOMATIC); //
if(Direct_Reverse == 0) { // * set the controller
myPID.SetControllerDirection(DIRECT); // direction
} else { //
myPID.SetControllerDirection(REVERSE); //
}
}
Serial.flush(); // * clear any random data from the serial buffer
}
void SerialSend()
{
Serial.print(F("PID "));
Serial.print(Setpoint);
SerialSpace();
Serial.print(Input);
SerialSpace();
Serial.print(Output);
SerialSpace();
Serial.print(myPID.GetKp());
SerialSpace();
Serial.print(myPID.GetKi());
SerialSpace();
Serial.print(myPID.GetKd());
SerialSpace();
if(myPID.GetMode() == AUTOMATIC) Serial.print(F("Automatic "));
else Serial.print(F("Manual "));
if(myPID.GetDirection() == DIRECT) Serial.println(F("Direct"));
else Serial.println(F("Reverse"));
}
inline void SerialSpace()
{
Serial.print(" ");
}
/*
Copyright
---------
### COPYRIGHT 2012-2017 ZAN HECHT
ARDUINO CROCKPOT SOUS VIDE AND TIMER is licensed under a Creative-
Commons Attribution Share-Alike License.
http://creativecommons.org/licenses/by-sa/3.0/
You are free:
to Share — to copy, distribute and transmit the work
to Remix — to adapt the work
to make commercial use of the work
Under the following conditions:
* Attribution — You must attribute the work in the manner specified
by the author or licensor (but not in any way that suggests that
they endorse you or your use of the work).
* Share Alike — If you alter, transform, or build upon this work,
you may distribute the resulting work only under the same or similar
license to this one.
With the understanding that:
* Waiver — Any of the above conditions can be waived if you get
permission from the copyright holder.
* Public Domain — Where the work or any of its elements is in the
public domain under applicable law, that status is in no way
affected by the license.
* Other Rights — In no way are any of the following rights affected
by the license:
* Your fair dealing or fair use rights, or other applicable
copyright exceptions and limitations;
* The author's moral rights;
* Rights other persons may have either in the work itself or in
how the work is used, such as publicity or privacy rights.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full
text of the Create-Commons license linked to above for more details.
CHANGELOG
---------
* 0.7 First Public Release
* 0.7.1 Fixed typo in instructions
* 0.7.2 Simplified code to output to LED bar
* 0.8 Change to use Celsius internally, fixed temp sensor polling, added moving average for temperature, added "aggressive mode"
* 0.8.1 Split code into functions. Skip sous vide mode if temperature sensor is missing. Tweaked aggressive mode.
* 0.8.2 Formatted instructions with Markdown
* 0.8.3 Bug Fixes
* 0.8.4 Changed aggressive mode I parameter to 0 to reduce overshoot.
* 0.9 Add mode labels, delayed start, and calibration
* 1.0 Optimize code. Add cooking percentage and cook until done temp mode.
* 1.0.1 Fix typos in instructions, optimize code for when SERIAL_ENABLE is set to 0.
*/