Some quick help with high level program design

Hi all. I’m building an Arduino controlled BBQ smoker as described here. Naturally it has to Internet enabled so I can monitor the progress while I’m at work. :sunglasses:

I’ve got the Ethernet shield setup and have played around with the sample web server code… so far so good. I’ve also got some sample PID-based code for controlling the heating element for the smoker.

I’m now working on the complete program, which I want it to do a few things:

  • check the smoker temp, adjust if necessary
  • log stuff to SD card
  • monitor and respond to HTTP requests

Any suggestions on how to best structure this? The most important task is maintaining the smoker temp so I want to make sure that’s not disrupted by any shenanigans with the web server. Should I just put all the tasks one after another in the loop()? Should I use something like TimedAction or Timer1 for the smoker temp and logging functions and just leave the web server stuff in the loop()? Does it matter?

Thanks!

im not sure how smokers work, im guessing gas? you can use a thermistor, although i am also not sure how much heat they can endure and how hot a smoker gets. if it is gas, there are special valves with metal balls that can be displaced by inducing current to a coil around the valve. The other parts of your questions i am not comfortable with yet. If you can post a graphic of your smoker maybe i can think of something :wink:

xxlbreed

How often do you intend to be connecting to your server to check on the BBQ?

How you structure your program will depend on often you really need to make adjustments to the temperature, how often you need to/plan to log data, and how often the program needs to respond to HTTP requests.

I imagine that most of the program's time is going to be occupied thumb twiddling.

I think a loop with those three things in it should work just fine.

The only one that will take any time is the webserver, so you shouldn't have a problem.

Just out of interest, why are you logging to SD?

Just out of interest, why are you logging to SD?

I had the same question, initially. Having a temperature profile that could be re-used, when a particular piece of meat comes out quite good, or not re-used if a particular piece of meat comes out bad, would be a good thing.

If you can’t reproduce results, you can’t get any better.

The smoker is basically a big insulated metal box, approximately 60" x 28" x 18"… it was constructed around a bun rack from a bakery. Heat will be provided via 1500w electric element at the bottom.

Here the approximate frequency for the different tasks…

  • monitor/adjust temp: every 30 seconds
  • log data: every 30 seconds
  • respond to HTTP requests: as needed, probably a few times each hour

The average BBQ cook time will be 8-12+ hours. Maintaining an average temp around the target is more important than precise adjustments and I figure the heating element probably doesn’t want/need to be cycled on and off many times per minute. Depending on what I’m cooking the program might be like 110o for the two hours, 120o for the next hour and so on… up to 170o where it will stay until the meat reaches a certain temp. I plan to increase/decrease the intervals and/or tweak the PID algorithm based on how well it holds temp and how often it cycles on/off.

The SD logging is so I can go back and see exactly how it all played out… make sure my algorithm is working correctly, see how fast the meat temp actually rose, etc. Depending on how fancy I want to get with the web interface I might also write most of the web server code on a PC hooked up to the network and just have that read the data files off the Arduino rather than hit it directly.

Initially I planned on doing it all in the main loop() but while browsing the Ethernet library I saw someplace that it some object might not get closed if a connection was hung open. My primary concern is monitoring/adjusting the temp and I want to be sure this goes smoothly regardless of what happens in the rest of the code.

Okay well I have everything up and running… you can see some pics of the project over here. The code is below… I had all the pieces working independently but ran into issues when I strung it all together… seems like I was running out of RAM due to excessive string creation, so I put some stuff into PROGMEM and it all seems good now.

Any suggestions for improvements are welcome and appreciated.

Also I’d like to add some functionality that will adjust the target pit temp over time… for example it would shoot for 100 deg the first two hours, then raise 10 degrees/hr until 160 and stay there. I’m thinking I can maybe have a two dimensional array, one with the time offset and the other with the desired temp. At each interval I can check how much time has elapsed and then pull the temp associated with that offset.

#include <SPI.h>
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <SdFatUtil.h>
#include <Ethernet.h>
#include <Time.h> 
#include <Udp.h>
#include <avr/pgmspace.h>

//#define USE_LCD
#define USE_LOG

// ETHERNET SETUP
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte ip[] = { 192, 168, 0, 101 };
Server server(80);

// NTP TIME SYNC SETUP
unsigned int localPort = 8888;      // local port to listen for UDP packets
byte timeServer[] = {192, 43, 244, 18}; // time.nist.gov NTP server
const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets 
const  long timeZoneOffset = 18000L; // 60 seconds * 60 minutes * 5 hours

// SD CARD SETUP
Sd2Card card;
SdVolume volume;
SdFile root;
SdFile file;

#ifdef USE_LCD  // LCD SETUP
LiquidCrystal lcd(3, 5, 6, 7, 8, 9);
#endif // USE_LCD

// setup variables to track interval for logging/temp adjustment
long previousMillis = 0;        // will store last time logging/temp adjustment was called
long interval = 5000;           // interval at which to log data/adjust temp
time_t startTime;

// setup variables to hold temp readings/targets
unsigned int pitTemp = 0;
unsigned int pitTarget = 225;  // hardcode for now
unsigned int meatTemp = 0;
unsigned int meatTarget = 200;   // hardcode for now

// heating element
int elementStatus = HIGH;
unsigned int elementPin =  A5;

// html
prog_char http_ok[] PROGMEM = "HTTP/1.1 200 OK";
prog_char content_type[] PROGMEM = "Content-Type: text/html";
prog_char html_open[] PROGMEM = "<html><head></head><body><form method=\"get\"><table border=\"1\">";
prog_char tr_time[] PROGMEM = "<tr><td colspan=99> %2.2d:%2.2d:%2.2d </td></tr>";
prog_char tr_header[] PROGMEM = "<tr><td>&nbsp;</td><td><b>Temp</b></td><td><b>Target</b></td></tr>";
prog_char tr_pit[] PROGMEM = "<tr><td><b>Pit</b></td><td> %2.2d </td> <td><input type=text size=3 name=pit value=\"%2.2d\"></td></tr>";
prog_char tr_meat[] PROGMEM = "<tr><td><b>Meat</b></td><td> %2.2d </td> <td><input type=text size=3 name=meat value=\"%2.2d\"></td></tr>";
prog_char tr_button[] PROGMEM = "<tr><td colspan=99 align=\"center\"><input type=submit value=Update></td></tr>";
prog_char html_close[] PROGMEM = "</table></form></body></html>";
PROGMEM const char *web_code[] = { http_ok, content_type, html_open, tr_time, tr_header, tr_pit, tr_meat, tr_button, html_close};

// store error strings in flash to save RAM
#define error(s) error_P(PSTR(s))

void error_P(const char* str) {
  PgmPrint("error: ");
  SerialPrintln_P(str);
  if (card.errorCode()) {
    PgmPrint("SD error: ");
    Serial.print(card.errorCode(), HEX);
    Serial.print(',');
    Serial.println(card.errorData(), HEX);
  }
  while(1);
}


void setup() {
  Serial.begin(9600); 
  Serial.println("Program started.");

  // lcd setup
#ifdef USE_LCD
  lcd.begin(16, 2);
  lcd.clear();
  lcd.home();
  lcd.print("welcome to");
  lcd.setCursor(0, 1);
  lcd.print("botulismaster9MM");
  delay(1000);
#endif // USE_LCD

  // initialize the SD card at SPI_HALF_SPEED to avoid bus errors with breadboards.
  pinMode(10, OUTPUT); // set the SS pin as an output (necessary!)
  digitalWrite(10, HIGH); // but turn off the W5100 chip!
  
  pinMode(elementPin, OUTPUT);

#ifdef USE_LOG
  initializeLog();
#endif // USE_LOG
  
  // start ethernet
  Ethernet.begin(mac, ip);
  Udp.begin(localPort);
  server.begin();

  // initialize time
  setupTime();
  startTime = now();

}

void loop() {
  unsigned long currentMillis = millis();
  
  if(currentMillis - previousMillis > interval) {
  
    // save the last time you log/adjust temp
    previousMillis = currentMillis;
 
    // read temp data
    pitTemp = thermister_temp(analogRead(4));
    meatTemp = thermister_temp(analogRead(3));
   
#ifdef USE_LOG
    logData();
#endif // USE_LOG
//    adjustTargetTemp();
    adjustHeat();
#ifdef USE_LCD
    updateLCD();
#endif // USE_LCD
  }
  
  listenForClients();
}

void listenForClients() {
  Client client = server.available();
  if (client) {
    Serial.println("responding to request");
    //pit_temp = thermister_temp(analogRead(4));
    // an http request ends with a blank line
    boolean current_line_is_blank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.print(c);
        // if we've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so we can send a reply
        if (c == '\n' && current_line_is_blank) {
          char buffer[200];
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[0])));
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[1])));
          client.println(buffer);
          client.println("");
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[2])));
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[3])));
          sprintf(buffer, buffer, hour(), minute(), second());
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[4])));
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[5])));
          sprintf(buffer, buffer, pitTemp, pitTarget);
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[6])));
          sprintf(buffer, buffer, meatTemp, meatTarget);
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[7])));
          client.println(buffer);
          strcpy_P(buffer, (char*)pgm_read_word(&(web_code[8])));
          client.println(buffer);
          break;
        }
        if (c == '\n') {
          // we're starting a new line
          current_line_is_blank = true;
        } else if (c != '\r') {
          // we've gotten a character on the current line
          current_line_is_blank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    client.stop();
  }
}

int thermister_temp(int aval) {
      double R, T;

      // These were calculated from the thermister data sheet
      //      A = 2.3067434E-4;
      //      B = 2.3696596E-4;
      //      C = 1.2636414E-7;
      //
      // This is the value of the other half of the voltage divider
      //      Rknown = 22200;
      // Do the log once so as not to do it 4 times in the equation
      //      R = log(((1024/(double)aval)-1)*(double)22200);
      R = log((1 / ((1024 / (double) aval) - 1)) * (double) 22200);
      // Compute degrees C
      T = (1 / ((2.3067434E-4) + (2.3696596E-4) * R + (1.2636414E-7) * R * R * R)) - 273.25;
      // return degrees F
      return ((int) ((T * 9.0) / 5.0 + 32.0));
}

void initializeLog() {
  if (!card.init(SPI_HALF_SPEED, 4)) error("card.init failed!");
  
  // initialize a FAT volume
  if (!volume.init(&card)) error("volume.init failed");
  
  // open root directory
  if (!root.openRoot(&volume)) error("openRoot failed");
  
  // create a new file
  char name[] = "LOOOER00.CSV";
  for (uint8_t i = 0; i < 100; i++) {
    name[6] = i/10 + '0';
    name[7] = i%10 + '0';
    if (file.open(&root, name, O_CREAT | O_EXCL | O_WRITE)) break;
  }
  if (!file.isOpen()) error ("file.create");
  Serial.print("Logging to: ");
  Serial.println(name);

  // write header
  file.writeError = 0;
  file.print("time,pitTemp,pitTarget,meatTemp,meatTarget,elementStatus");
  file.println();
  if (file.writeError || !file.sync()) {
    error("write header failed");
  }
}

// log data to the SD card
void logData() {
  char line[60];
  file.writeError = 0;
  sprintf(line, "%2.2d:%2.2d:%2.2d,%3d,%3d,%3d,%3d,%3d", hour(), minute(), second(),pitTemp, pitTarget, meatTemp, meatTarget, elementStatus);
  Serial.println(line);
  file.print(line);
  file.println();  
  if (!file.sync()) error("sync failed");
}

void updateLCD() {
#ifdef USE_LCD
  char line[60];
  lcd.clear();
  lcd.home();
  sprintf(line, "%2.2d:%2.2d:%2.2d", hour(), minute(), second());
  lcd.print(line);
  lcd.setCursor(0, 1);
  if(second() % 6 < 3) {
    sprintf(line, "pit %3d%c/%3d%c", pitTemp, 0xDF, pitTarget, 0xDF);
  } else {
    sprintf(line, "meat %3d%c/%3d%c", meatTemp, 0xDF, meatTarget, 0xDF);
  }
  lcd.print(line);
#endif // USE_LCD
}

continued below…

// adjustHeat
void adjustHeat() {
    if ((pitTarget - 5) > pitTemp) {
        elementStatus = HIGH;
    } else {
        elementStatus = LOW;
    }
    if(pitTemp < 0) {
      elementStatus = LOW; // if something is wrong turn off the element
    }
    digitalWrite(elementPin, elementStatus);
}


// adjustHeat
void adjustTarget() {
  
}

void setupTime() {
  char line[60];

  Serial.println("Attempting to sync time..."); 
#ifdef USE_LCD
  lcd.clear();
  lcd.home();
  lcd.print("syncing time");
#endif // USE_LCD
  
  for(int i=1; i <=5; i++) {
    sprintf(line, "attempt %2d", i);
    Serial.println(line);
#ifdef USE_LCD
    lcd.setCursor(0, 1);
    lcd.print("                ");
    lcd.setCursor(0, 1);
    lcd.print(line);
#endif // USE_LCD

    Serial.println("sending packet");
    sendNTPpacket(timeServer); // send an NTP packet to a time server
  
      // wait to see if a reply is available
    delay(1000);  
    if ( Udp.available() ) {  
      Serial.println("udp available - processing packet");
      Udp.readPacket(packetBuffer,NTP_PACKET_SIZE);  // read the packet into the buffer
      Serial.println("packet read");
  
      //the timestamp starts at byte 40 of the received packet and is four bytes,
      // or two words, long. First, extract the two words:
      unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
      unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);  
      // combine the four bytes (two words) into a long integer
      // this is NTP time (seconds since Jan 1 1900):
      unsigned long secsSince1900 = highWord << 16 | lowWord;
      Serial.print("Seconds since Jan 1 1900 = " );
      Serial.println(secsSince1900); 
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;     
      // subtract seventy years:
      unsigned long epoch = secsSince1900 - seventyYears - timeZoneOffset;  
      setTime(epoch);
 
    // print Unix time:
    Serial.println(epoch);                              


    // print the hour, minute and second:
    Serial.print("The time is ");
    Serial.print((epoch  % 86400L) / 3600); // print the hour (86400 equals secs per day)
    Serial.print(':');  
    Serial.print((epoch  % 3600) / 60); // print the minute (3600 equals secs per minute)
    Serial.print(':');
    Serial.println(epoch %60); // print the second
      break;
      
    } else {
      Serial.println("UDP unavailable");
#ifdef USE_LCD
      lcd.setCursor(0, 1);
      lcd.print("UDP unavailable");
#endif // USE_LCD
      delay(2000); 
    }
  }
  if (timeStatus()== timeNotSet) {
    Serial.println("time not set... defaulting");
#ifdef USE_LCD
    lcd.setCursor(0, 1);
    lcd.print("fail. defaulting");
#endif // USE_LCD
    setTime(1262347900);
  }
}

// send an NTP request to the time server at the given address 
unsigned long sendNTPpacket(byte *address)
{

  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE); 
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49; 
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:                
  Udp.sendPacket( packetBuffer,NTP_PACKET_SIZE,  address, 123); //NTP requests are to port 123
}

void printDigits(int digits){
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

Question, are you able to use the logger the same time with the LCD ? If so, how is everything connected ? I'm running into problems with my lcd on pin8 and shield also on pin8.

Yes both work simultaneously... below is the pin layout I'm using. I may be wrong but I don't think the ethernet/logger shield uses pin 8.

Digital
1 - heating element / LED
2                        
3 - LCD RS               
4 - ethernet/sd          
5 - LCD EN               
6 - LCD DB4              
7 - LCD DB5              
8 - LCD DB6              
9 - LCD DB7
10 - ethernet/sd
11 - ethernet/sd
12 - ethernet/sd
13 - ethernet/sd

Analog
1
2
3 - meat thermometer
4 - pit thermometer
5

Ah, I was under the impression that you where using the sparkfun microsd shield (which I'm using). Thank you for the answer tho. The ethernet/sd shield might be an alternative to my microsd shield.