As part of something I'm doing elsewhere I have made a Sous Vide cooker, designed and made a force-displacement device for measuring elastic modulus of material, and cooked some meat for a long time.
This thread will contain lots of information about all of these things. You might be interested in one or more of them and I would be happy to discuss them in the discussion thread (https://www.electronicspoint.com/sous-vide-cooker-elasticity-and-meat-t267512.html#post1600675).
I intend to break this up into several parts:
1) Overview (what I'm doing and why)
2) Force-displacement device (design, manufacture, calibration, and use)
3) Sous Vide Cooker (design, calibration, programming, and use)
4) Steps in the cooking of the meat (from raw to meal in about 50 hours!)
5) Raw data (data logging, weight, stress, strain, time, etc.)
6) Results
1) Overview (what I'm doing and why)
I've wanted to cook using the sous vide method. However these things are pretty expensive where I live, so I decided to make one instead.
This also worked well with a course that I was doing where I needed to do a final assignment, so I worked them in together.
The first step was to determine how to make and calibrate the controller for the sous vide cooker. This is not as trivial as it seems because the slow cooker has a large thermal mass which introduces a large delay, PID control works, but it's tricky to get it to both stabilize quickly *AND* come up to temperature within a reasonable time.
After I had managed to control this enough to use the cooker at a constant temperature, I needed to determine an appropriate experiment.
I decided to look at elasticity of meat and how it changes over time in low temperature sous vide cooking.
This is interesting because there are some statements made about maximum cooking times which (when one eliminated food safety issues) seem to be based more on myth than science.
An early generalization about sous vide was that it was impossible to overcook food. That is clearly wrong when you look at things like fish which can easily be cooked to the point at which they become mushy (although I've never done this). Various web sites make the same claim for beef, and because beef is a more robust product it is easier to measure the elasticity. In addition to this the cooking time for fish can be so short that it would be hard to get enough readings taken.
A cooking time of 33 hours (which was extended to 36 hours) was chosen because it worked in well with my work schedule. In retrospect this could have been extended by another 24 or 48 hours.
2) Force-displacement device (design, manufacture, calibration, and use)
For those playing along at home, I have also attached the DXF file used to laser cut the MDF. Note that there's an error in the DXF. It cuts an additional blank and doesn't cut the platform to put the mass on. The supports and the central shaft are not included in this either.
Design
I wanted a method to make reliable and reproducible measures of stress and strain.
The method I used initially was to alter the weight placed on the test piece over a known area and measure the displacement. The problems with this were the measurement of the masses, and the inability to keep things aligned. Consequently the measurement of displacement was very inaccurate.
For this experiment I decided to use a fixed mass and alter the displacement. The weight as read on the scales would represent the force applied by the test piece to the plunger.
The next issue is to keep the plunger vertical and make the selection of displacement simple.
The final design is a plunger passing unrestricted through a vertical channel other than by a wing nut which determines the maximum displacement.
Manufacture
All of the platforms were cut on a laser cutter. The vertical columns were cut using a drop saw. The distances between the platforms, and that of the plunger pieces is not critical. The plunger was assembled by screwing the pieces together whilst inside the channel. Only the top platform was unfixed and allowed to rotate. After the three pieces of the plunger were screwed together the top platform was nailed and glued into place.
Here is the assembled product:
(1) Is the single mass used to provide the force onto the item under test.
(2) Is the platform the mass is placed on.
(3) Is the top section of the plunger. This is a rigid piece that transfers force directly to the piece under test.
(4) Is the top reference. The difference between this platform and the mass platform is measured to determine the displacement. (This needs calibration to determine an offset).
(5) Adjustment platform. The wing nut on the threaded rod allows the displacement to be accurately adjusted. The normal adjustment is 1/2 a turn between readings. The actual adjustment can be calibrated, but I measured the displacement rather than by counting turns.
(6) Lower plunger. This is the 30mm x 30mm plunger used to press against the test piece. The pairs of platforms the wooden plunger pieces pass through prevent the plunger from rotating and maintain it in a vertical orientation. The threaded rod between the two wooden pieces is screwed about 50mm in to each piece.
(7) Is the piece under test. In this case it is the unsealed, uncooked meat.
(8) Is the scales used to measure the difference in force applied to the meat (it is tared after the test piece is placed on it)
(9) is the base board.
The distance between (2) and (4) is measured using the depth gauge of a digital micrometer.
Calibration
There are to calibrations required. The first one is to determine the constant offset between the measurement from the top of the mass platform (2) to the top of the top platform (4) compared to the measurement between the bottom of the plunger (6) to the top of the scales (8).
This is done by winding the wing nut to the top of its travel so that the plunger rests on the scales. The distance between the (2) and (4) is measured and subtracted from all subsequent measurements. In the case of this device, this offset is 12.13mm
The second calibration is to wind the wing nut to about mid-way and take a series of measurements between (2) and (4) as the wing nut is turned 1/2 a turn at a time. For this thread, each half turn lifted or dropped the plunger by 0.75mm
Use
(1) The plunger is wound up clear of the item under test
(2) The item to be tested is placed on the scales, weighed (if required) and the scales tared.
(3) A suitable mass is placed on the mass platform
(4) The plunger is lowered until the scales read between 20g and 50g. The location on the test piece should be chosen so that the entire plunger makes contact.
(5) The displacement (2) to (4) is measured and noted against the weight reading.
(6) The plunger is lowered 1/2 a turn at a time. After the reading on the scales has steadied, the displacement and weight are measured and recorded.
Example
Here is an example of the calculations performed to determine elasticity
Let's look at the 5th row.Code:g mm (raw) N mm (corr) area m^2 stress strain elasticity 20 53.93 0.196 41.83 0.0009 72 53.29 0.7056 41.19 0.0009 566.2222222 0.011867235 47713.06944 132 52.69 1.2936 40.59 0.0009 653.3333333 0.011259148 58026.88889 205 52.02 2.009 39.92 0.0009 794.8888889 0.012715885 62511.4859 285 51.42 2.793 39.32 0.0009 871.1111111 0.011534025 75525.33333 380 50.81 3.724 38.71 0.0009 1034.444444 0.011863088 87198.57923 488 50.09 4.7824 37.99 0.0009 1176 0.014170439 82989.66667 614 49.44 6.0172 37.34 0.0009 1372 0.012976642 105728.4308 760 48.71 7.448 36.61 0.0009 1589.777778 0.014765372 107669.3333 78420.34845
The first column represents the weight in grams shown on the scales (285g).
The second column is the raw height measured from the top of the mass platform to the top if the top platform. (51.52mm) These first 2 columns represent the raw data captured whilst measuring. The other columns are derived from these and from various constants.
The third column is a direct conversion from g to newtons. To do this, column 1 is divided by 1000 (to get kg) and then multiplied by 9.8 to convert to newtons. In this case the figure is 2.793
Column 4 is the distance between the bottom of the plunger and the scales. This is done by applying the calibration factor determined during calibration. 12.1 was used. The value was measured as 12.13, however the final digit was considered too unreliable). In this case the figure is 39.32mm.
The 4th column contains the area of the plunger in square metres. The plunger is 30mm square which is 0.0009 square metres.
The fifth column is the stress. This is calculated as the difference in pressure in N divided by the plunger area (871).
The sixth column is the strain. This is the difference in height between the previous and current reading expressed as a proportion of the previous height (0.0115)
The last column represents the elastic modulus, this is the stress divided by the strain. In this case it is 75500.
Finally all the elasticity measurements are averaged giving 78400 in this case.
Problems
The main problem is that as the displacement increases the item under test takes longer and longer to reach equilibrium.
After performing this experiment I discovered that the best way to reach equilibrium was to drop the plunger by 1 1/2 turns and then lift it by 1 turn. This results in a reading which stabilizes almost immediately.
Caution needs to be taken because some scales don't correctly deal with slowly changing weights. To test this, place a cup on the scales then pour a cup of sugar quickly into it. Note the weight. Then repeat the measurement but pour the sugar *VERY* slowly (over a minute perhaps) into the cup. If the difference in weight reading is more than a few grams (some will be out by over 100g) then DONT use this set of scales in this application.
3) Sous Vide Cooker (design, calibration, programming, and use)
The starting point is a slow cooker:
The controller is an arduino, and currently built on a breadboard:
The code was based on the Adafruit Sous Vide controller, but it has a number of changes.
1) Uses a standard LCD, switches, and LEDs rather than the special Adafruit LDC/keypad/backlight
2) Averages the temperature over time to give higher resolution.
3) Several bugs fixed
4) Enhanced logging
5) Added code to facilitate calibration
6) Dumb slewing to within a few degrees of the set point before enabling PID control
The source code documents the interface to the hardware.
Here is the source code:
I've made a few small changes in some of the libraries, so if you actually want to compile this contact me for the updated libraries.Code:/* Lots of code based on AdaFruit SousVide controller. This is the AdaCoreSousVide controller because it lacks some of the AdaFruit stuff. Based on: //------------------------------------------------------------------- // // Sous Vide Controller // Bill Earl - for Adafruit Industries // // Based on the Arduino PID and PID AutoTune Libraries // by Brett Beauregard //------------------------------------------------------------------ The LCD circuit: * LCD pin 1 (gnd) to gnd * LCD pin 2 (Vcc) to +5v * LCD pin 3 (VO) to wiper of 20k pot between Vcc and gnd (contrast adj) * LCD pin 4 (RS) to digital pin 7 * LCD pin 5 (R/W) to gnd * LCD pin 6 (clock/enable) to digital pin 6 * LCD pin 7 (D0) UNUSED * LCD pin 8 (D1) UNUSED * LCD pin 9 (D2) UNUSED * LCD pin 10 (D3) UNUSED * LCD pin 11 (D4) to digital pin 5 * LCD pin 12 (D5) to digital pin 4 * LCD pin 13 (D6) to digital pin 3 * LCD pin 14 (D7) to digital pin 2 http://www.arduino.cc/en/Tutorial/LiquidCrystal - - - - - - - - - - - - - - - - - - The DS18B20 circuit: * DS18B20 pin 1 to gnd * DS18B20 pin 2 to digital pin 9 * DS18B20 pin 3 to Vcc http://www.hobbytronics.co.uk/ds18b20-arduino - - - - - - - - - - - - - - - - - - The 4 direction switches are on pins 14 to 17 all pulled low with 10k resistors * left - digital pin 14 * right - digital pin 15 * up - digital pin 16 * down - digital pin 17 And the shift button on 8 * shift - digital pin 8 And the relay output is on 10 * relay - digital pin 10 - - - - - - - - - - - - - - - - - - * Red LED - digital pin 13 * Green LED - digital pin 12 * Blue LED - digital pin 11 */ // PID Library #include <PID_v1.h> #include <PID_AutoTune_v0.h> // include the library code for LCD: #include <LiquidCrystal.h> #include <SPI.h> #include "printf.h" // include library code for DS18B20 #include <OneWire.h> #include <DallasTemperature.h> // include the switch debouncing library #include <Bounce2.h> // So we can save and retrieve settings #include <EEPROM.h> // LCD constants and definitions //------------------------------ const uint8_t num_rows = 2; uint8_t num_cols = 16; uint8_t scan_wide = (num_rows - 1) * num_cols; // initialize the library with the numbers of the interface pins LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // DS18B20 constants and definitions //---------------------------------- // Data wire is plugged into pin 2 on the Arduino #define ONE_WIRE_BUS 9 // Setup a oneWire instance to communicate with any OneWire devices // (not just Maxim/Dallas temperature ICs) OneWire oneWire(ONE_WIRE_BUS); // Pass our oneWire reference to Dallas Temperature. DallasTemperature sensors(&oneWire); // The address of the DS18B20 DeviceAddress tempSensor; // We'll use this variable to store a found device address // Standard LED identification //---------------------------- // Pin 13 has an LED connected on most Arduino boards. // give it a name: #define led 13; //bool bLedOn = false; #define LED_R 13 #define LED_G 12 #define LED_B 11 // These #defines make it easy to set the backlight color #define BLACK 0x0 #define RED 0x1 #define YELLOW 0x3 #define GREEN 0x2 #define TEAL 0x6 #define BLUE 0x4 #define VIOLET 0x5 #define WHITE 0x7 // Switch definitions //------------------- Bounce SW_Left(14, 75, true); Bounce SW_Right(15, 75, true); Bounce SW_Up(16, 75, true); Bounce SW_Down(17, 75, true); Bounce SW_Shift(8, 75, true); // Output Relay //------------- #define RelayPin 10 // ************************************************ // PID Variables and constants // ************************************************ //Define Variables we'll be connecting to double Setpoint; double Raw; double Input, Input0, Input1, InputLast; double Output; long MaxTime, MinTime; double MaxTemp, MinTemp; volatile long onTime = 0; // pid tuning parameters double Kp; double Ki; double Kd; // EEPROM addresses for persisted data const int SpAddress = 0; const int KpAddress = 8; const int KiAddress = 16; const int KdAddress = 24; //Specify the links and initial tuning parameters PID myPID(&Input1, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); // 10 second Time Proportional Output window int WindowSize = 10000; unsigned long windowStartTime; // ************************************************ // Auto Tune Variables and constants // ************************************************ byte ATuneModeRemember=2; double aTuneStep=500; double aTuneNoise=1; unsigned int aTuneLookBack=20; boolean tuning = false; PID_ATune aTune(&Input, &Output); unsigned long lastInput = 0; // last button press byte degree[8] = // define the degree symbol { B00110, B01001, B01001, B00110, B00000, B00000, B00000, B00000 }; byte progress_0[8] = // define a progress symbol { B00000, B00000, B00000, B00100, B00000, B00000, B00000, B00000 }; byte progress_1[8] = // define a progress symbol { B00000, B00000, B00100, B01010, B00100, B00000, B00000, B00000 }; byte progress_2[8] = // define a progress symbol { B00000, B00100, B01010, B10001, B01010, B00100, B00000, B00000 }; // The highest temperature we want to ztep/zoom up to #define HIGH_TEMP 85 // The start for step/zoom #define LOW_TEMP 35 // Splash screen interval #define SPLASH_TIME 3000 // log every 30 seconds in TIME mode #define LOG_INTERVAL_MS 30000 // log every 1 degree in TEMP mode #define LOG_INTERVAL_TEMP 0.125 // smooth temperatures over 32 readings #define SMOOTH_TEMP 32 // delay temperatures over 128 readings #define DELAY_TEMP 128 // ************************************************ // States for state machine // ************************************************ bool TestSequence = false; // true if the tests are to be run as a sequence enum operatingState { OFF = 0, SETP, RUN, TUNE_P, TUNE_I, TUNE_D, AUTO, ZOOM_UP, STEP_UP }; operatingState opState = OFF; // ************************************************ // States for logging // ************************************************ enum loggingState { TIME = 0, TEMP }; loggingState logState = TIME; // how we scan the buttons bool buttonScan() { bool changed = false; // make sure we're ready to read the button state by doing this often changed |= SW_Left.update(); changed |= SW_Right.update(); changed |= SW_Up.update(); changed |= SW_Down.update(); changed |= SW_Shift.update(); if ((changed != 0)| SW_Left.read()| SW_Right.read()| SW_Up.read()| SW_Down.read() | SW_Shift.read()) // changed also true if a button is pressed. { lastInput = millis(); } return changed; } void SetLEDColour(int colour) { digitalWrite(LED_R, (colour & 1)); digitalWrite(LED_G, (colour & 2)); digitalWrite(LED_B, (colour & 4)); } // returns the number of pressed buttons int buttonsPressed() { buttonScan(); int pressed = 0; if (SW_Left.read()) pressed++; if (SW_Right.read()) pressed++; if (SW_Up.read()) pressed++; if (SW_Down.read()) pressed++; if (SW_Shift.read()) pressed++; return pressed; } void lcdHeader() { lcd.clear(); lcd.setCursor(0, 0); lcd.print(F("AdaCORE SousVide")); } void showActivity() { static byte i = 0; lcd.setCursor(15,1); i++; if (i == 1) lcd.print(' '); else if (i == 2) lcd.print('\2'); else if (i == 3) lcd.print('\3'); else if (i == 4) lcd.print('\4'); else if (i == 5) lcd.print('\3'); else { lcd.print('\2'); i = 0; } } // ************************************************ // Setup and diSplay initial screen // ************************************************ void setup() { Serial.begin(9600); Serial.println(); Serial.println("AdaCORE SousVide Startup"); // Initialize Relay Control: pinMode(RelayPin, OUTPUT); // Output mode to drive relay digitalWrite(RelayPin, LOW); // make sure it is off to start // initialize LED pinMode(LED_R, OUTPUT); digitalWrite(LED_R, LOW); // make sure it is off to start pinMode(LED_G, OUTPUT); digitalWrite(LED_G, LOW); // make sure it is off to start pinMode(LED_B, OUTPUT); digitalWrite(LED_B, LOW); // make sure it is off to start // set up the LCD's number of columns and rows: lcd.begin(num_cols, num_rows); // scan the buttons buttonScan(); // Initialize LCD DiSplay lcd.clear(); lcd.createChar(1, degree); // create degree symbol from the binary lcd.createChar(2, progress_0); // and now the progress characters lcd.createChar(3, progress_1); lcd.createChar(4, progress_2); SetLEDColour(VIOLET); lcdHeader(); // Start up the DS18B20 One Wire Temperature Sensor sensors.begin(); if (!sensors.getAddress(tempSensor, 0)) { ErrorFail('A'); } sensors.setResolution(tempSensor, 12); // max resolution sensors.setWaitForConversion(false); // don't wait for a temperature sensors.requestTemperatures(); // request a read while (!ReadTemp()){}; // wait for a read, we might get a reset value while (!ReadTemp()){}; // wait for a read, this one will be a real temp InputLast = Input0 = Input1 = Input = Raw; // initial values MaxTemp = MinTemp = Input; MaxTime = MinTime = millis(); // Splash screen delay, but keep reading temperatures for (long i = millis() + SPLASH_TIME; i > millis(); ReadTemp()); // Initialize the PID and related variables LoadParameters(); myPID.SetTunings(Kp,Ki,Kd); myPID.SetSampleTime(1000); myPID.SetOutputLimits(0, WindowSize); // Run timer2 interrupt every 15 ms TCCR2A = 0; TCCR2B = 1<<CS22 | 1<<CS21 | 1<<CS20; //Timer2 Overflow Interrupt Enable TIMSK2 |= 1<<TOIE2; } // ************************************************ // Timer Interrupt Handler // ************************************************ SIGNAL(TIMER2_OVF_vect) { if (opState == OFF) { digitalWrite(RelayPin, LOW); // make sure relay is off } else { DriveOutput(); } } // ************************************************ // Main Control Loop // // All state changes pass through here // ************************************************ void loop() { // wait for button release before changing state while (buttonsPressed() != 0) {} lcd.clear(); switch (opState) { case OFF: Off(); break; case SETP: Tune_Sp(); break; case RUN: Run(); break; case TUNE_P: TuneP(); break; case TUNE_I: TuneI(); break; case TUNE_D: TuneD(); break; case ZOOM_UP: ZoomUp(); break; case STEP_UP: StepUp(); break; } } void PrintStatus() { lcd.setCursor(5, 1); lcd.print(Input, 3); lcd.write(1); lcd.print("C "); lcd.print(TestSequence ? "s" : " "); } // ************************************************ // Initial State - press RIGHT to enter setpoint // ************************************************ void Off() { myPID.SetMode(MANUAL); SetLEDColour(BLACK); digitalWrite(RelayPin, LOW); // make sure it is off lcdHeader(); uint8_t buttons = 0; TestSequence = false; // if we get back here, the sequence stops. lcdHeader(); lcd.setCursor(0, 1); lcd.print("Off"); while(!(SW_Right.read()||SW_Up.read()||SW_Down.read())) { buttonScan(); if (ReadTemp()) { PrintStatus(); showActivity(); LogTemps(); } } if (SW_Up.read()) { TestSequence = true; opState = ZOOM_UP; // Do a zoom test } else if (SW_Down.read()) { opState = STEP_UP; // Do a step test } else { // Prepare to transition to the RUN state sensors.requestTemperatures(); // Start an asynchronous temperature reading //turn the PID on myPID.SetMode(AUTOMATIC); windowStartTime = millis(); opState = RUN; // start control } } bool WaitForTemp(float Temp, int Power, bool DirUp, bool & LedOn, int LedCol) { bool AtTemp = false; Setpoint = Temp; onTime = Output = Power; digitalWrite(RelayPin, Power > 0 ); while(!(SW_Right.read()||SW_Left.read()||SW_Up.read()||SW_Down.read()||AtTemp)) { buttonScan(); if (ReadTemp()) { if (LedOn) { SetLEDColour(BLACK); } else { SetLEDColour(LedCol); } LedOn = !LedOn; lcd.setCursor(2, 1); lcd.print(Setpoint, 0); lcd.print(" "); PrintStatus(); showActivity(); LogTemps(); if (DirUp) { AtTemp = Input >= Setpoint; } else { AtTemp = Input <= Setpoint; } } } Setpoint = 0; onTime = Output = 0; digitalWrite(RelayPin, FALSE); return AtTemp; } // ************************************************ // Zoom up. left or right to stop // zooms to 85C from current temp then goes to OFF // any button will terminate // ************************************************ void ZoomUp() { myPID.SetMode(MANUAL); bool LedOn = true; bool abort = false; lcdHeader(); lcd.setCursor(0, 1); lcd.print("ZU"); logState = TEMP; while (buttonsPressed() != 0) {} // wait for button release // drop temperature to 35C or less if (!abort) abort = !WaitForTemp(LOW_TEMP, 0, false, LedOn, BLUE); // Now zoom up to 85C if (!abort) abort = !WaitForTemp(HIGH_TEMP, 10000, true, LedOn, RED); // allow temperature to drop again to 35C if (!abort) abort = !WaitForTemp(LOW_TEMP, 0, false, LedOn, BLUE); if (abort) { if (SW_Up.read()) // up from zoom to step { opState = STEP_UP; } else if (SW_Down.read()) // down to off { opState = OFF; } else { opState = OFF; } } else // run to end { if (TestSequence) { opState = STEP_UP; // if we're running a sequence then go to the next step } else { opState = OFF; // otherwise just end } } logState = TIME; } // ************************************************ // Step up. left or right to stop // steps up to next 10+5 degrees, then allows the // temp to fall 0.5 degrees, then repeat until 85C // ************************************************ void StepUp() { myPID.SetMode(MANUAL); bool LedOn = true; bool AtTemp = false; // we are not at temperature yet int abort = false; // we have not aborted float EndTemp = LOW_TEMP - 1; // initial target is 34C boolean goingUp = false; // initial direction is DOWN lcdHeader(); lcd.setCursor(0, 1); lcd.print("SU"); logState = TEMP; while (buttonsPressed() != 0) {} // wait for button release while (!(abort||AtTemp)) { if (!(abort = !WaitForTemp(EndTemp, goingUp ? 10000 : 0, goingUp, LedOn, goingUp ? RED : BLUE))) { if (goingUp) { EndTemp = EndTemp - 1; } else { Setpoint = EndTemp = ceil(Input/10)*10 + 5; } goingUp = !goingUp; } AtTemp = Setpoint > (HIGH_TEMP + 1); // final end point } if (!abort) abort = !WaitForTemp(LOW_TEMP, 0, false, LedOn, BLUE); // ramp back down if (abort) { if (SW_Up.read()) // up from step to off { opState = OFF; } else if (SW_Down.read()) // down to zoom { opState = ZOOM_UP; } else { opState = OFF; } } else // run to end { if (TestSequence) { opState = OFF; // if we're running a sequence then go to the next step (which is off) } else { opState = OFF; // otherwise just end } } logState = TIME; } // ************************************************ // Setpoint Entry State // UP/DOWN to change setpoint // RIGHT for tuning parameters // LEFT for OFF // SHIFT for 10x tuning // ************************************************ void Tune_Sp() { SetLEDColour(TEAL); lcd.print(F("Set Temperature:")); uint8_t buttons = 0; while(true) { buttonScan(); float resolution = 0.1; float increment = resolution; if (SW_Shift.read()) { increment *= 10; } if (SW_Left.read()) { opState = RUN; return; } if (SW_Right.read()) { opState = TUNE_P; return; } if (SW_Up.read()) { Setpoint += increment; Setpoint = floor((Setpoint / resolution) + 0.5) * resolution; delay(200); } if (SW_Down.read()) { Setpoint -= increment; Setpoint = floor((Setpoint / resolution) + 0.5) * resolution; delay(200); } if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle { opState = RUN; return; } lcd.setCursor(0,1); lcd.print(Setpoint, 1); lcd.print(" "); LogTemps(); DoControl(); } } // ************************************************ // Proportional Tuning State // UP/DOWN to change Kp // RIGHT for Ki // LEFT for setpoint // SHIFT for 10x tuning // ************************************************ void TuneP() { SetLEDColour(TEAL); lcd.print(F("Set Kp")); uint8_t buttons = 0; while(true) { buttonScan(); float increment = 1.0; if (SW_Shift.read()) { increment *= 10; } if (SW_Left.read()) { opState = SETP; return; } if (SW_Right.read()) { opState = TUNE_I; return; } if (SW_Up.read()) { Kp += increment; Kp = floor(Kp + 0.5); delay(200); } if (SW_Down.read()) { Kp -= increment; Kp = floor(Kp + 0.5); delay(200); } if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle { opState = RUN; return; } lcd.setCursor(0,1); lcd.print(Kp, 0); lcd.print(" "); LogTemps(); DoControl(); } } // ************************************************ // Integral Tuning State // UP/DOWN to change Ki // RIGHT for Kd // LEFT for Kp // SHIFT for 10x tuning // ************************************************ void TuneI() { SetLEDColour(TEAL); lcd.print(F("Set Ki")); uint8_t buttons = 0; while(true) { buttonScan(); float resolution = 0.01; float increment = resolution; if (SW_Shift.read()) { increment *= 10; } if (SW_Left.read()) { opState = TUNE_P; return; } if (SW_Right.read()) { opState = TUNE_D; return; } if (SW_Up.read()) { Ki += increment; Ki = floor((Ki / resolution) + 0.5) * resolution; delay(200); } if (SW_Down.read()) { Ki -= increment; Ki = floor((Ki / resolution) + 0.5) * resolution; delay(200); } if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle { opState = RUN; return; } lcd.setCursor(0,1); lcd.print(Ki); lcd.print(" "); LogTemps(); DoControl(); } } // ************************************************ // Derivative Tuning State // UP/DOWN to change Kd // RIGHT for setpoint // LEFT for Ki // SHIFT for 10x tuning // ************************************************ void TuneD() { SetLEDColour(TEAL); lcd.print(F("Set Kd")); uint8_t buttons = 0; while(true) { buttonScan(); float resolution = 0.01; float increment = resolution; if (SW_Shift.read()) { increment *= 10; } if (SW_Left.read()) { opState = TUNE_I; return; } if (SW_Right.read()) { opState = RUN; return; } if (SW_Up.read()) { Kd += increment; Kd = floor((Kd / resolution) + 0.5) * resolution; delay(200); } if (SW_Down.read()) { Kd -= increment; Kd = floor((Kd / resolution) + 0.5) * resolution; delay(200); } if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle { opState = RUN; return; } lcd.setCursor(0,1); lcd.print(Kd); lcd.print(" "); LogTemps(); DoControl(); } } void LogTemps() { // No previous log entry static long lastLogTime = 0; static float lastLogTemp = 0; if (logState == TIME) { // periodically log to serial port in csv format if (millis() - lastLogTime > LOG_INTERVAL_MS) { LogTempLine(); while(millis() - lastLogTime > LOG_INTERVAL_MS) { lastLogTime += LOG_INTERVAL_MS; } } } else { // periodically log to serial port in csv format if (abs(Input - lastLogTemp) >= LOG_INTERVAL_TEMP) { LogTempLine(); lastLogTemp = floor((Input / LOG_INTERVAL_TEMP) + (LOG_INTERVAL_TEMP / 2)) * LOG_INTERVAL_TEMP; } } } void LogTempLine() { unsigned long m = millis(); int th = m/3600000; // hours m -= (long)th*3600000; int tm = m/60000; // minutes m -= (long)tm*60000; int ts = (m + 50) / 100; // tenths of seconds) if (th < 10) Serial.print('0'); // Time since start Serial.print(th); Serial.print(':'); if (tm < 10) Serial.print('0'); Serial.print(tm); Serial.print(':'); if (ts < 100) Serial.print('0'); Serial.print((float)ts/10, 1); Serial.print(","); Serial.print(opState); Serial.print(","); Serial.print(Raw,3); // Raw temperature reading Serial.print(","); Serial.print(Input,3); // Smoothed temperature reading Serial.print(","); Serial.print((float)Output/100,2); // output power % Serial.print(","); Serial.print(Setpoint,1); // target temperature Serial.print(","); Serial.print(myPID.GetKp(),0); // PID Parameters Serial.print(","); Serial.print(myPID.GetKi()); Serial.print(","); Serial.print(myPID.GetKd()); Serial.print(","); Serial.print(Input0,3); // Delayed temperature Serial.print(","); Serial.print(Input1,3); // predicted temperature Serial.print(","); Serial.print(DELAY_TEMP); // constant used to delay temperature Serial.print(","); Serial.print(Input - InputLast,3); // delta T for last measurement Serial.print(","); Serial.print(millis()); // current time Serial.print(","); Serial.print(MaxTemp,3); // highest temp since last reading Serial.print(","); Serial.print(MaxTime); // time of highest temp Serial.print(","); Serial.print(MinTemp,3); // lowest temp since last reading Serial.print(","); Serial.print(MinTime); // time of lowest temp Serial.println(); MaxTemp = MinTemp = Input; MaxTime = MinTime = millis(); InputLast = Input; } // ************************************************ // PID COntrol State // SHIFT and RIGHT for autotune // RIGHT - Setpoint // LEFT - OFF // ************************************************ void Run() { bool LedOn = false; double OldSetpoint = Setpoint; while (buttonsPressed() != 0) {} // wait for button release // first slew to close to the required temperature. lcdHeader(); lcd.setCursor(0, 1); lcd.print("Slew"); if (Input > Setpoint + 0.25) { myPID.SetMode(MANUAL); WaitForTemp(Setpoint + 0.25, 0, false, LedOn, BLUE); Setpoint = OldSetpoint; myPID.SetMode(AUTOMATIC); } else if (Input < Setpoint - 5) { myPID.SetMode(MANUAL); WaitForTemp(Setpoint - 5, 10000, true, LedOn, RED); Setpoint = OldSetpoint; myPID.SetMode(AUTOMATIC); } // set up the LCD's number of rows and columns: lcd.clear(); lcd.print(F("Sp: ")); lcd.print(Setpoint, 1); lcd.write(1); lcd.print(F("C ")); SaveParameters(); myPID.SetTunings(Kp,Ki,Kd); uint8_t buttons = 0; while(true) { setBacklight(); // set backlight based on state buttonScan(); if ((SW_Shift.read()) && (SW_Right.read()) && (abs(Input - Setpoint) < 0.5)) // Should be at steady-state { StartAutoTune(); } else if (SW_Right.read()) { opState = SETP; return; } else if (SW_Left.read()) { opState = OFF; return; } DoControl(); lcd.setCursor(0,1); lcd.print(Input, 3); lcd.write(1); lcd.print(F("C : ")); float pct = map(Output, 0, WindowSize, 0, 1000); lcd.setCursor(10,1); lcd.print(F(" ")); lcd.setCursor(10,1); lcd.print(pct/10, 0); lcd.print("%"); showActivity(); lcd.setCursor(15,0); if (tuning) { lcd.print("T"); } else { lcd.print(" "); } LogTemps(); delay(100); } } boolean ReadTemp() { static double lastRead = 0; static unsigned long lastTime = 0; if (sensors.isConversionAvailable(0)) { Raw = sensors.getTempC(tempSensor); if ((abs(Raw - lastRead) > 0.001) || (lastTime+750 <= millis())) { lastRead = Raw; lastTime = millis(); Input = ((Input * (SMOOTH_TEMP - 1)) + Raw)/SMOOTH_TEMP; // provide a slower response, but smoother Input0 = ((Input0 * (DELAY_TEMP - 1)) + Raw)/DELAY_TEMP; // an even slower response to allow prediction of future temp Input1 = 3*Input - 2*Input0; // prediction of future temp if (Input > MaxTemp) { MaxTemp = Input; MaxTime = millis(); } if (Input < MinTemp) { MinTemp = Input; MinTime = millis(); } sensors.requestTemperatures(); // prime the pump for the next one - but don't wait return true; } } else { ErrorFail('R'); delay(1000); } return false; } void ErrorFail(char E) { // try to shut down first myPID.SetMode(MANUAL); Setpoint = 0; Output = onTime = 0; digitalWrite(RelayPin, LOW); // make sure it is off // Display the failure lcdHeader(); lcd.setCursor(0, 1); lcd.print(F("SENSOR FAILURE=")); lcd.print(E); // Then log the faiure Serial.print("SENSOR FAILURE="); Serial.println(E); // Make a unique display and loop forever so nothing else happens while (true) { digitalWrite(RelayPin, LOW); // make sure it is off (belt and braces) SetLEDColour(RED); delay(250); SetLEDColour(GREEN); delay(250); SetLEDColour(BLUE); delay(250); } } // ************************************************ // Execute the control loop // ************************************************ void DoControl() { // Read the input: ReadTemp(); if (tuning) // run the auto-tuner { if (aTune.Runtime()) // returns 'true' when done { FinishAutoTune(); } } else // Execute control algorithm { myPID.Compute(); } // Time Proportional relay state is updated regularly via timer interrupt. onTime = Output; } // ************************************************ // Called by ISR every 15ms to drive the output // ************************************************ void DriveOutput() { unsigned long now = millis(); // Set the output // "on time" is proportional to the PID output if(now - windowStartTime > WindowSize) { //time to shift the Relay Window windowStartTime += WindowSize; } if((onTime > 100) && (onTime > (now - windowStartTime))) { digitalWrite(RelayPin,HIGH); } else { digitalWrite(RelayPin,LOW); } } // ************************************************ // Set Backlight based on the state of control // ************************************************ void setBacklight() { if (tuning) { SetLEDColour(VIOLET); // Tuning Mode } else if (abs(Input - Setpoint) > 1.0) { SetLEDColour(RED); // High Alarm - off by more than 1 degree } else if (abs(Input - Setpoint) > 0.2) { SetLEDColour(YELLOW); // Low Alarm - off by more than 0.2 degrees } else { SetLEDColour(WHITE); // We're on target! } } // ************************************************ // Start the Auto-Tuning cycle // ************************************************ void StartAutoTune() { // REmember the mode we were in ATuneModeRemember = myPID.GetMode(); // set up the auto-tune parameters aTune.SetNoiseBand(aTuneNoise); aTune.SetOutputStep(aTuneStep); aTune.SetLookbackSec((int)aTuneLookBack); tuning = true; } // ************************************************ // Return to normal control // ************************************************ void FinishAutoTune() { tuning = false; // Extract the auto-tune calculated parameters Kp = aTune.GetKp(); Ki = aTune.GetKi(); Kd = aTune.GetKd(); // Re-tune the PID and revert to normal control mode myPID.SetTunings(Kp,Ki,Kd); myPID.SetMode(ATuneModeRemember); // Persist any changed parameters to EEPROM SaveParameters(); } // ************************************************ // Save any parameter changes to EEPROM // ************************************************ void SaveParameters() { if (Setpoint != EEPROM_readDouble(SpAddress)) { EEPROM_writeDouble(SpAddress, Setpoint); } if (Kp != EEPROM_readDouble(KpAddress)) { EEPROM_writeDouble(KpAddress, Kp); } if (Ki != EEPROM_readDouble(KiAddress)) { EEPROM_writeDouble(KiAddress, Ki); } if (Kd != EEPROM_readDouble(KdAddress)) { EEPROM_writeDouble(KdAddress, Kd); } } // ************************************************ // Load parameters from EEPROM // ************************************************ void LoadParameters() { // Load from EEPROM Setpoint = EEPROM_readDouble(SpAddress); Kp = EEPROM_readDouble(KpAddress); Ki = EEPROM_readDouble(KiAddress); Kd = EEPROM_readDouble(KdAddress); // Use defaults if EEPROM values are invalid if (isnan(Setpoint)) { Setpoint = 44.0; } if (isnan(Kp)) { Kp = 1850; // this is for me (850 was default) } if (isnan(Ki)) { Ki = 1.0; // so far for me, 0.5 is default } if (isnan(Kd)) { Kd = 0.0; // so far 0.1 was default } } // ************************************************ // Write floating point values to EEPROM // ************************************************ void EEPROM_writeDouble(int address, double value) { byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { EEPROM.write(address++, *p++); } } // ************************************************ // Read floating point values from EEPROM // ************************************************ double EEPROM_readDouble(int address) { double value = 0.0; byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { *p++ = EEPROM.read(address++); } return value; }
The output drives a SSR which turns the slow cooker on and off.
A display tells you what's going on:
The logging looks like this:
But you can't read that, so here is a small sample:
The columns are:Code:AdaCORE SousVide Startup 00:00:30.0,0,30.125,30.172,0.00,61.0,1850,1.00,0.00,30.183,30.150,128,-0.015,30028,30.187,918,30.172,30008 00:01:00.3,0,30.125,30.138,0.00,61.0,1850,1.00,0.00,30.168,30.080,128,-0.034,60353,30.172,30060,30.138,60333 00:01:30.5,0,30.125,30.122,0.00,61.0,1850,1.00,0.00,30.154,30.058,128,-0.016,90506,30.138,60387,30.122,89883 00:02:00.6,2,30.062,30.091,100.00,54.0,1850,1.00,0.00,30.134,30.004,128,-0.031,120622,30.122,94273,30.091,120598 00:02:30.3,2,30.062,30.071,100.00,54.0,1850,1.00,0.00,30.115,29.981,128,-0.020,150273,30.091,120658,30.071,150249 00:03:00.0,2,30.062,30.062,100.00,54.0,1850,1.00,0.00,30.100,29.987,128,-0.008,180063,30.071,150310,30.062,172592 00:03:30.6,2,30.062,30.056,100.00,54.0,1850,1.00,0.00,30.087,29.996,128,-0.006,210579,30.062,180101,30.054,201588 00:04:00.4,2,30.062,30.060,100.00,54.0,1850,1.00,0.00,30.079,30.020,128,0.003,240368,30.060,240347,30.054,216172 00:04:30.7,2,30.125,30.077,100.00,54.0,1850,1.00,0.00,30.079,30.073,128,0.017,270763,30.077,270740,30.060,240406
1) Time since startup hh:mm:ss.s
2) Operating mode
3) Raw temperature sensor reading
4) Smoothed temperature reading
5) Output power (%)
6) Target temperature
7) P gain
8) I gain
9) D gain
10) Lagging temperature reading
11) Leading temperature estimate
12) Temperature delay constant
13) Change in temperature since last reading (from (4))
14) Current time (ms)
15) Highest temp (4) since last reading
16) Time of highest temp
17) Lowest temp (4) since last reading
18) Time of last reading
Here is the temperature profile for the slow cooker at 100% power.
As you can see, it takes *hours* to get up to its practical peak temperature of 85C. Fortunately sousvide of meat is done at lower temperatures (55C to 70C is the main range)
4) Steps in the cooking of the meat (from raw to meal in about 50 hours!)
Meat as purchased:
And then seasoned:
The seasoning is about 2 teaspoons of Shan Meat Masala spice. Very simple but very nice.
The two pieces of meat are stacked together to make a thicker "chunk" that's easier t measure the elasticity of:
Then it was placed in a sous vide bag:
It's actually cut from a roll of plastic "tube" and one end sealed.
At this point I measured the elasticity and the weight. The bag weighs about 10g and the total weight is 418g. That means the shop weighed the meat *AND* the packaging!
Then it was vacuum sealed:
Again, the elasticity was measured. This was to determine how the vacuum sealing affected the elasticity.
Then the meat was placed in the fridge overnight as the sous vide cooker was given plenty of time to come up to temperature.
After being taken out of the fridge:
And another elasticity measure to see what cooling did...
Then it was placed in the sous vide bath for 3 hours. At the end of 3 hours:
Then after 6.75 hours:
After 36 hours:
Contents of the bag, about 80ml of juices. Actually, only 60ml of juices and 20ml of water that entered the bag via osmosis(?).
The meat out of the bag. The colour of this meat is due to the seasoning, not the cooking.
Cut in half, the meat is edge to edge medium-rare.
See! Yum...
Served on a column of rice and with a large scoop of sauce on top.
And it tasted *really* good. Super tender and tasty.
Recipe:
1) Season 400g of gravy beef with 2tsp of Shan meat masala powder.
2) Seal in a bag and sous vide for 33 hours at 56.0C
3) Thinly slice 2 medium onions and fry with 1/2 tsp of salt and some oil until browned.
4) Add 1 heaped tsp chopped ginger and 2 tsp finely chopped garlic. Fry for a few minutes.
5) Add one tin of diced tomatoes and deglaze the pan.
6) Simmer on a low heat adding water as needed to keep the mix the consistency of a tomato paste.
7) Cook 2 cups of rice.
8) Remove meat from sous vide. Empty the juices into the sauce.
9) Prepare a column of rice by packing it into a biscuit cutter, egg ring, etc
10) Cut meat in half and place a piece on the rice column.
11) take a scoop of sauce and pile on the meat trying not to hide the cut edge of the meat.
Serve and eat immediately.
5) Raw data (data logging, weight, stress, strain, time, etc.)
Here is a dump of various data files used. If you're interested in them in more detail, please ask.
Rise.zip contains the raw data file and a spreadsheet containing a graph of the time vs temperature characteristics of my slow cooker at 100% power.
calib.zip contains the logging of the calibration run of my controller. It takes a little over 28 hours to complete. The steps are:
1) Full power to 85C
2) Zero power to 35C
3) Step rise from 35C to 85C
4) Zero power to 35C
The step rise steps the temperature up in 10C steps. Each step proceeds as follows:
1) Zero power until the temperature falls 1C below the start temp
2) 100% power until the temp rises to the next step.
The step rise test looks at overshoot and undershoot at various temperatures.
I intend to perform analysis of this data to determine appropriate P and I values for the controller. The ideal temperatures actually vary with temperature so a final implementation of the controller will probably trim the PID settings to suit the target temperature.
Compress test.zip is a spreadsheet listing the change in height of the plunger with 1/2 turns of the wing nut.
6) Results
The quick results:
1) Elasticity rises
2) Ignoring bad measurements it looks like it increases at a decreasing rate.
3) The mass increased! (osmosis of water through the bag?)
In more detail (see the attached spreadsheet for the raw data):
The change in elasticity with time can be described by the equation
Y = 670 X + 67700
Where Y is the elasticity in Pa and X represents the cooking time at 56C in hours.
Approximately 70% of the variation in elasticity is explained by this equation. If we ignore the three readings flagged earlier the equation is very similar, but over 95% of the variation is explained.
There is no reliable indication that the elasticity of the meat starts to decrease, and perhaps a longer cooking time would be required to show this (if indeed it happens).
The results are unsurprising because at 56C the enzymes cease activity (due to denaturing) and cannot be responsible for collagen breakdown. The breakdown of collagen to gelatin requires a significantly higher temperature.
There are lots of things which could be done to either improve this experiment or to investigate further.
I've been asked for the changed libraries (wow, somebody read this!)
I started with the Arduino PID_v1.01 library (see here). This has now need superseded, but I will attach it below in case you need it. I recommend you use the newer version and apply the changes that I will list below:
PID_v1.h
And PID_v1.cppCode:CWD 2012 cat 1-2 3.15 vs 3.17 Produced: 15/10/2014 7:10:31 AM Mode: Differences, With Context, Ignoring Unimportant Left file: D:\Downloads\PID_v1\PID_v1\PID_v1.h Right file: C:\Users\Steve\Dropbox\Arduino\Arduino\libraries\PID_v1\PID_v1.h 17 17 //commonly used functions ************************************************************************** 18 18 PID(double*, double*, double*, // * constructor. links the PID to the Input, Output, and 19 19 double, double, double, int); // Setpoint. Initial tuning parameters are also set here 20 20 21 21 void SetMode(int Mode); // * sets PID to either Manual (0) or Auto (non-0) 22 22 ------------------------------------------------------------------------ 23 void Compute(); // * performs the PID calculation. it should be 23 bool Compute(); // * performs the PID calculation. it should be ------------------------------------------------------------------------ 24 24 // called every time loop() cycles. ON/OFF and 25 25 // calculation frequency can be set using SetMode 26 26 // SetSampleTime respectively 27 27 28 28 void SetOutputLimits(double, double); //clamps the output to a specific range. 0-255 by default, but 29 29 //it's likely the user will want to change this depending on ------------------------------------------------------------------------ ------------------------------------------------------------------------ 69 69 double *mySetpoint; // PID, freeing the user from having to constantly tell us 70 70 // what these values are. with pointers we'll just know. 71 71 72 72 unsigned long lastTime; 73 73 double ITerm, lastInput; 74 74 ------------------------------------------------------------------------ 75 int SampleTime; 75 unsigned long SampleTime; ------------------------------------------------------------------------ 76 76 double outMin, outMax; 77 77 bool inAuto; 78 78 }; 79 79 #endif 80 80 ------------------------------------------------------------------------
Clearly I am not comparing against the actual original version I used. (I didn't change the license)Code:CWD 2012 cat 1-2 3.15 vs 3.17 Produced: 15/10/2014 7:18:21 AM Mode: Differences, With Context Left file: D:\Downloads\PID_v1.0.1\PID_v1\PID_v1.cpp Right file: C:\Users\Steve\Dropbox\Arduino\Arduino\libraries\PID_v1\PID_v1.cpp 1 1 /********************************************************************************************** ------------------------------------------------------------------------ 2 * Arduino PID Library - Version 1 2 * Arduino PID Library - Version 1.0.1 ------------------------------------------------------------------------ 3 3 * by Brett Beauregard <[email protected]> brettbeauregard.com 4 4 * ------------------------------------------------------------------------ 5 * This Code is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. 5 * This Library is licensed under a GPLv3 License ------------------------------------------------------------------------ 6 6 **********************************************************************************************/ 7 7 ------------------------------------------------------------------------ 8 #include <WProgram.h> 8 #if ARDUINO >= 100 9 #include "Arduino.h" 10 #else 11 #include "WProgram.h" 12 #endif 13 ------------------------------------------------------------------------ 9 14 #include <PID_v1.h> 10 15 11 16 /*Constructor (...)********************************************************* 12 17 * The parameters specified here are those for for which we can't set up 13 18 * reliable defaults, so we need to have the user set them. 14 19 ***************************************************************************/ 15 20 PID::PID(double* Input, double* Output, double* Setpoint, 16 21 double Kp, double Ki, double Kd, int ControllerDirection) 17 22 { ------------------------------------------------------------------------ 23 24 myOutput = Output; 25 myInput = Input; 26 mySetpoint = Setpoint; 27 inAuto = false; 28 ------------------------------------------------------------------------ 18 29 PID::SetOutputLimits(0, 255); //default output limit corresponds to 19 30 //the arduino pwm limits 20 31 21 32 SampleTime = 100; //default Controller Sample Time is 0.1 seconds 22 33 23 34 PID::SetControllerDirection(ControllerDirection); 24 35 PID::SetTunings(Kp, Ki, Kd); 25 36 26 37 lastTime = millis()-SampleTime; ------------------------------------------------------------------------ 27 inAuto = false; 28 myOutput = Output; 29 myInput = Input; 30 mySetpoint = Setpoint; 31 ------------------------------------------------------------------------ 32 38 } 33 39 34 40 35 41 /* Compute() ********************************************************************** 36 42 * This, as they say, is where the magic happens. this function should be called 37 43 * every time "void loop()" executes. the function will decide for itself whether a new ------------------------------------------------------------------------ 38 * pid Output needs to be computed 44 * pid Output needs to be computed. returns true when the output is computed, 45 * false when nothing has been done. ------------------------------------------------------------------------ 39 46 **********************************************************************************/ ------------------------------------------------------------------------ 40 void PID::Compute() 47 bool PID::Compute() ------------------------------------------------------------------------ 41 48 { ------------------------------------------------------------------------ 42 if(!inAuto) return; 49 if(!inAuto) return false; ------------------------------------------------------------------------ 43 50 unsigned long now = millis(); ------------------------------------------------------------------------ 44 int timeChange = (now - lastTime); 51 unsigned long timeChange = (now - lastTime); ------------------------------------------------------------------------ 45 52 if(timeChange>=SampleTime) 46 53 { 47 54 /*Compute all the working error variables*/ 48 55 double input = *myInput; 49 56 double error = *mySetpoint - input; 50 57 ITerm+= (ki * error); ------------------------------------------------------------------------ ------------------------------------------------------------------------ 59 66 else if(output < outMin) output = outMin; 60 67 *myOutput = output; 61 68 62 69 /*Remember some variables for next time*/ 63 70 lastInput = input; 64 71 lastTime = now; ------------------------------------------------------------------------ 72 return true; ------------------------------------------------------------------------ 65 73 } ------------------------------------------------------------------------ 74 else return false; ------------------------------------------------------------------------ 66 75 } 67 76 68 77 69 78 /* SetTunings(...)************************************************************* 70 79 * This function allows the controller's dynamic performance to be adjusted. 71 80 * it's called automatically from the constructor, but tunings can also ------------------------------------------------------------------------
My changes allow the PID controller to maintain the state it was in, run for longer without the time wrapping, and return a state.
For those into sousvide:
Squid - 57C 3 hrs
Chicken Breast - 63C 3 to 4 hours
For fun, Lamb 48 hours 55C ends up with a consistency such that you can spread it with a knife. Horrible texture for meat, but possibly an alternative to pate.
It seems I used a very old version if the Bounce2 Library. I think I had to modify it in order to get the original code (the Adafruit code) to compile.
I have compared this code to the current version (here) but the differences are too many. It is also interesting to note that the code has changed from a GPL to an MIT license.
In any case, my entire library is attached - see discussion thread.
Here are all the non-arduino libraries I'm using (see discussion thread). They are probably old and possibly modified. They may help if you're having trouble compiling.
hevans1944 and Ian like this.