/* * Monitor function. * * Greg Lehey, 18 May 2004 * * Copyright (c) 2004 by Greg Lehey * * This software is distributed under the so-called ``Berkeley * License'': * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * This software is provided ``as is'', and any express or implied * warranties, including, but not limited to, the implied warranties * of merchantability and fitness for a particular purpose are * disclaimed. In no event shall Greg Lehey be liable for any direct, * indirect, incidental, special, exemplary, or consequential damages * (including, but not limited to, procurement of substitute goods or * services; loss of use, data, or profits; or business interruption) * however caused and on any theory of liability, whether in contract, * strict liability, or tort (including negligence or otherwise) * arising in any way out of the use of this software, even if advised * of the possibility of such damage. * * $Id: monitor.c,v 1.24 2005/08/17 03:48:32 grog Exp grog $ */ #include #include #include #include #include #ifdef __FreeBSD__ #include /* for realhostname */ #include #include #endif #include "main.h" #include #include "statemachine.h" /* * For tradition's sake, we make odd buffers the length of a punched * card. */ #define CARDSIZE 80 /* * xterm command sequences to clear the screen (termcap cl capability) * and home the cursor (termcap ho capability). We don't use termcap * here because that requires some assumption about the kind of * terminal, and there's no particular reason to assume that it * relates to the TERM environment variable, but mainly because we're * too lazy. FIXME. Anyway, this is pretty ubiquitous nowadays. */ char *xterm_cls = ""; /* clear screen sequence for xterm (cl) */ char *xterm_home = ""; /* home cursor sequence (ho) */ char *xterm_cleol = ""; /* clear to EOL sequence (ce) */ int linefd; /* file descriptor of input serial line */ int relayfd; /* file descriptor of relay output */ FILE *logfd; /* for logging */ FILE *graphlogfd; /* for writing graph info */ FILE *displayfd; /* for writing direct status info */ FILE *debugfd; /* for writing debug information */ char firmwarerev [CARDSIZE]; /* save temperature probe startup info here */ #define NOTEMP -500 /* * Temperatures read from serial line. The sensors are numbered 1 to * 4, so we use 0 for a null value. */ float temps [5] = {0, NOTEMP, NOTEMP, NOTEMP, NOTEMP}; /* one temperature per probe */ time_t lastreading [5]; /* and last reading time, to detect problems */ /* * start temperature at last configuration read in time, used for * ramping calculations Normally this is the start temperature in the * configuration file, but if we change the end temperature without * changing the start temperature, we'll set it to the current * temperature. */ float realstarttemp; /* * temperature to base our calculations on. Somewhere between the two * fermenter temperatures. See comments in probe2factor in commands.c * for more details. */ float basetemp; /* The earliest time we can do certain things. */ time_t nextheateron; /* next time we can turn the heater on */ time_t nextcooleron; /* next time we can turn the cooler on */ time_t nextheateroff; /* next time we can turn the heater off */ time_t nextcooleroff; /* next time we can turn the cooler off */ time_t probetimeout = 60; /* probe timeout in seconds */ char idletext [80]; /* a text to print when we go idle */ time_t idletexttime; /* and when we did it */ /* * And, for completely unrelated purposes, the last time we did the * same certain things. We use these values to calculate when to turn * heater and cooler off without overshooting our envelope. * * The algorithm looks like this: when we heat or cool, note the * temperatures when we turn the heat on and off. Then note the * maximum temperature overshoot before the temperature turns around. * Use these three values next time to guess where to stop heating or * cooling to just stay within the envelope. * * There's a margin for error here, of course, particularly when * starting from way outside the envelope. This will tend towards * underestimating the overshoot (since it's relatively small compared * to the start and end temperature difference). That's OK: it's * still better than no estimate. */ time_t lastheateron; /* last time we turned the heater on */ time_t lastcooleron; /* last time we turned the cooler on */ time_t lastheateroff; /* and times we turned them off */ time_t lastcooleroff; /* * The fermenter temperatures corresponding to the last times above. */ float lastheaterontemp = NOTEMP; /* temp last time we turned the heater on */ float lastcoolerontemp = NOTEMP; /* temp last time we turned the cooler on */ float lastheaterofftemp = NOTEMP; /* temp last time we turned the heater off */ float lastcoolerofftemp = NOTEMP; /* temp last time we turned the cooler off */ /* * The highest and lowest the temperature ascended or descended after * turning off. These values are initialized to make sense the first * time through the loop. */ float lastheaterhightemp = NOTEMP; /* highest temp after turning the heater off */ float lastcoolerlowtemp = -NOTEMP; /* lowest temp after turning the cooler off */ float thiscoolerofftemp = NOTEMP; /* temperature to choose this time around */ float thisheaterofftemp = NOTEMP; /* temperature to choose this time around */ float overshootratio; /* ratio of temperature overshoot */ float thisgoal; /* temperature we want to get to this time */ /* What are we doing at the moment? */ enum coolstatus coolstatus; enum machinestate state; enum machinestate lastcoolstate = M_IDLE; /* last kind of temperature manipulation */ time_t now; /* current time */ time_t nextlogtime; /* and time next log entry is due */ time_t nextgraphlogtime; /* and time next graph log entry is due */ time_t lastdisplay; /* time of last display output */ int monitoring; /* set when we're in monitor () */ int doinfodump; /* set to dump info if we're debugging */ struct termios seriallineparms; /* parameters for the input line */ /* Stuff for network reporting of temperatures. */ int listenfd; int connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr; struct sockaddr_in servaddr; /* * Open and set parameters for serial line. * Return 1 on failure. */ int openserialline () { if (linefd) /* stuff left behind? */ close (linefd); linefd = open (serialline, O_RDONLY); if (linefd < 0) { fprintf (stderr, "Can't open serial line %s: %s (%d)\n", serialline, strerror (errno), errno ); return 1; } /* Set up the line the way we want it. */ if (tcgetattr (linefd, &seriallineparms) < 0) /* can't get line info */ { fprintf (stderr, "Can't get parameters for serial line %s: %s (%d)\n", serialline, strerror (errno), errno ); return 1; } seriallineparms.c_iflag = 0; /* input flags: none */ seriallineparms.c_oflag = 0; /* no output flags */ seriallineparms.c_cflag = CS8 | CREAD; /* control flags: 8 bits, read enable */ seriallineparms.c_lflag = ICANON; /* local flags: canonical input */ seriallineparms.c_ispeed = linespeed; /* set line speed */ /* * We don't need to output, but if the speeds aren't the same, the * tcsetattr fails with EINVAL. */ seriallineparms.c_ospeed = linespeed; /* set line speed */ if (tcsetattr (linefd, TCSANOW, &seriallineparms) < 0) /* can't get line info */ { fprintf (stderr, "Can't get parameters for serial line %s: %s (%d)\n", serialline, strerror (errno), errno ); return 1; } return 0; } /* * Open relay line. * Return 1 on failure. */ int openrelayline () { if (relayfd) close (relayfd); /* Now open the files. */ relayfd = open (relayline, O_WRONLY); if (relayfd < 0) { fprintf (stderr, "Can't open relay controller %s: %s (%d)\n", relayline, strerror (errno), errno ); return 1; } return 0; } /* Open graph log file. Fail silently. */ void opengraphlogfile () { if (graphlogfd) fclose (graphlogfd); if (graphlogfile [0]) { graphlogfd = fopen (graphlogfile, "a"); if (graphlogfd == NULL) /* error, don't let this stop us */ fprintf (stderr, "Can't open graph log file %s: %s (%d)\n", graphlogfile, strerror (errno), errno ); } } /* Open log file. Fail silently. */ void openlogfile () { if (logfd) fclose (logfd); if (logfile [0]) { logfd = fopen (logfile, "a"); if (logfd == NULL) /* error, don't let this stop us */ fprintf (stderr, "Can't open log file %s: %s (%d)\n", logfile, strerror (errno), errno ); } } /* Open display log file. Fail silently. */ void opendisplayfile () { if (displayfd) fclose (displayfd); if (displayfile [0]) { displayfd = fopen (displayfile, "a"); if (displayfd == NULL) /* error, don't let this stop us */ fprintf (stderr, "Can't open display log file %s: %s (%d)\n", displayfile, strerror (errno), errno ); /* * Clear the screen. We don't use termcap here because that * requires some assumption about the kind of terminal, and * there's no particular reason to assume that it relates to the * TERM environment variable. Anyway, this is pretty ubiquitous * nowadays. */ else if (isatty (fileno (displayfd))) fputs (xterm_cls, displayfd); /* clear screen */ } } /* * This is a wrapper around checkrcfile which checks for file name * changes. */ void checkourrcfile () /* see if we need to update things */ { char oldserialline [MAXPATHLEN]; char oldlogfile [MAXPATHLEN]; char oldgraphlogfile [MAXPATHLEN]; char oldrelayline [MAXPATHLEN]; char olddisplayfile [MAXPATHLEN]; char olddebugfile [MAXPATHLEN]; float oldstarttemp; float oldendtemp; time_t oldtempchangeend; /* Keep track of what we had before */ strcpy (oldserialline, serialline); strcpy (oldlogfile, logfile); strcpy (oldgraphlogfile, graphlogfile); strcpy (oldrelayline, relayline); strcpy (olddisplayfile, displayfile); oldstarttemp = starttemp; oldendtemp = endtemp; oldtempchangeend = tempchangeend; if (checkrcfile ()) /* see if anything has changed */ { if (debugfd) dump_state ("rcfile changed"); if (strcmp (oldserialline, serialline)) openserialline (); if (strcmp (oldlogfile, logfile)) openlogfile (); if (strcmp (oldgraphlogfile, graphlogfile)) opengraphlogfile (); if (strcmp (oldrelayline, relayline)) openrelayline (); if (strcmp (olddisplayfile, displayfile)) { if (displayfd) /* currently displaying, */ { if (strlen (displayfile) == 0) /* no display file any more */ fprintf (displayfd, "\n*** Terminating status display ***\n"); else fprintf (displayfd, "\n*** Moving status display to %s ***\n", displayfile); } opendisplayfile (); } if (strcmp (olddebugfile, debugfile)) opendebugfile (); /* * Yet another attempt at the start/end temperature stuff. * * 1: If we change the start temperature, it should take effect * immediately. If the end temperature hasn't changed, we * approach it at the specified end time. It's not clear how much * use this is. * * 2: If we change the end temperature but not the start * temperature, it should start from the current temperature and * approach this temperature at the specified time. If the time * is in the past, it should take place immediately (yes, this is * just another way to shoot yourself in the foot). * * 3: If both start and end temperature change, we just set them * normally. */ if (starttemp != oldstarttemp) /* start temp changed, */ { setrelay (0); /* turn things off for the time being */ state = M_IDLE; /* and start again */ strcpy (idletext, "Due to setting new parameters"); idletexttime = now; /* Forget our timeouts */ nextheateron = now; nextcooleron = now; nextheateroff = now; nextcooleroff = now; if (starttemp != oldstarttemp) /* start temp has changed, */ { realstarttemp = starttemp; /* start at this temperature */ tempchangestart = time (NULL); /* now */ } } else if ((endtemp != oldendtemp) /* or end temp changed */ || (tempchangeend != oldtempchangeend) ) { realstarttemp = goaltemp; /* current goal temperature, */ tempchangestart = time (NULL); /* start measuring from now */ } } } /* Format and print a time */ char *timestring (time_t time, int local) { struct tm *bursttime; static char string [20]; /* * You'd think we'd always want local time, but we use this function * to represent differences between two times, and localtime adds * the time zone offset :-( */ if (local) bursttime = localtime (&time); else bursttime = gmtime (&time); sprintf (string, "%02d:%02d:%02d ", bursttime->tm_hour, bursttime->tm_min, bursttime->tm_sec ); return string; } /* Format and print a time */ void printtime (time_t time, FILE *fd) { fputs (timestring (time, 1), fd); } /* * Create a current status log message and return to 'text'. Include * a header if 'header' is set, and assume it's an xterm if 'screen' * is set. */ void statustext (int header, int screen, char *text, int textlen) { char *et = text; /* point to where we put next text */ char *cleol = ""; /* clear to EOL when required */ if (header) { if (screen) /* displaying to screen, */ { cleol = xterm_cleol; /* set clear to EOL string */ strcpy (et, xterm_cls); et = &et [strlen (et)]; } sprintf (et, "Time %6s %7s Base Ambient Goal Offset Room\n" " %6s %7s%s\n", fermenter1label1, fermenter2label1, fermenter1label2, fermenter2label2, cleol ); } strlcat (et, timestring (now, 1), textlen); et = &et [strlen (et)]; sprintf (et, "%6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f%s\n", temps [fermenterprobe], temps [fermenter2probe], basetemp, temps [ambientprobe], goaltemp, basetemp - goaltemp, temps [roomtempprobe], cleol ); et = &et [strlen (et)]; sprintf (et, "Status: %s ", machinestatetext [state]); /* display state */ /* For the time wait states, show what we're waiting for */ switch (state) { case M_IDLE: if (idletext [0]) /* we have a comment to make */ { strlcat (et, idletext, textlen); et = &et [strlen (et)]; if (now > (idletexttime + idledisplaytime)) /* outlived its usefulness, */ idletext [0] = '\0'; /* and never more */ } break; case M_COOL_ON_WAIT: /* waiting for cooler on time */ strlcat (et, timestring (nextcooleron, 1), textlen); et = &et [strlen (et)]; break; case M_COOL_OFF_WAIT: /* waiting for cooler off time */ strlcat (et, timestring (nextcooleroff, 1), textlen); et = &et [strlen (et)]; break; case M_HEAT_ON_WAIT: /* waiting for heater on time */ strlcat (et, timestring (nextheateron, 1), textlen); et = &et [strlen (et)]; break; case M_HEAT_OFF_WAIT: /* waiting for heater off time */ strlcat (et, timestring (nextheateroff, 1), textlen); et = &et [strlen (et)]; break; default: break; } strlcat (et, xterm_cleol, textlen); /* clear to end of line */ et = &et [strlen (et)]; strlcat (et, "\n", textlen); /* only \n if not on screen */ } /* * Log current status. Print a header if 'header' is set, * and assume it's an xterm if 'screen' is set. */ void logstatus (FILE *fd, int header, int screen) { char mystatus [1024]; statustext (header, screen, mystatus, 1024); fputs (mystatus, fd); } /* * Write current fermenter status for plotting purposes. If force * !=0, print even if our time is not yet up. This is for events like * relays on and off. */ void printstatus (int force) { static int outlines = 0; /* output lines on log file */ if (logfd /* we're logging */ && ((now >= nextlogtime) || force ) ) { logstatus (logfd, outlines == 0, 0); if (++outlines > logpagesize) outlines = 0; /* force a new page every 50 lines */ if (! force) nextlogtime = now + loginterval; /* next time for normal log */ } if (displayfd && (now > lastdisplay)) /* not more than once a second */ { fseek (displayfd, 0, SEEK_SET); logstatus (displayfd, 1, isatty (fileno (displayfd))); lastdisplay = now; } if (graphlogfd /* got a graph log */ && (state != M_INIT) /* and we're not initializing */ && ((now >= nextgraphlogtime) /* and we're ready to write */ || force ) ) /* or we have no choice */ { fprintf (graphlogfd, "%d %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %d\n", (int) now, temps [fermenterprobe], temps [fermenter2probe], basetemp, temps [ambientprobe], goaltemp, temps [fermenterprobe] - goaltemp, temps [roomtempprobe], coolstatus ); fflush (graphlogfd); if (! force) nextgraphlogtime = now + graphloginterval; /* next time for normal log */ } } /* * Read temperature information from the serial line and save it * somewhere safe. */ void gettemp () { int probe; float temp; char linein [CARDSIZE]; /* read from serial line */ int fields; int inlen; time_t now; /* current time */ inlen = read (linefd, linein, CARDSIZE); /* get a temperature */ if (inlen != PROBERECORDLEN) /* invalid probe data record */ { if (inlen < 0) fprintf (stderr, "Can't read serial line: %s (%d)\n", strerror (errno), errno ); else if (state != M_INIT) /* this can happen during initialization */ fprintf (stderr, "Invalid temperature sensor input: \"%s\", length %d\n", linein, inlen ); return; } time (&now); fields = sscanf (linein, "%d %g\n", &probe, &temp); /* get a temperature */ if (fields != 2) /* * We don't bother mentioning errors. They can happen when we * start the program and read a partial record. */ return; if ((probe < 1) || (probe > 4)) /* invalid probe number */ return; temps [probe] = temp; lastreading [probe] = now; if (state == M_INIT) /* no calculations yet */ return; if (lastreading [fermenterprobe] < now - probetimeout) /* probe 1 has timed out */ { if (lastreading [fermenter2probe] < now - probetimeout) /* and so has probe 2 */ { if (dosyslog) syslog (LOG_USER | LOG_EMERG, "Both temperature sensors timed out\n"); fprintf (stderr,"Both temperature sensors timed out\n"); sleep (1); } else if (probe2factor == 0) /* probe 2 up, but we're ignoring it */ { if (dosyslog) syslog (LOG_USER | LOG_EMERG, "Temperature sensor timed out\n"); fprintf (stderr,"Temperature sensor timed out\n"); sleep (1); } else /* only sensor 1 down */ { if (dosyslog) syslog (LOG_USER | LOG_CRIT, "Temperature sensor 1 timed out\n"); fprintf (stderr,"Temperature sensor 1 timed out\n"); basetemp = temps [fermenter2probe]; } } else if (lastreading [fermenter2probe] < now - probetimeout) /* probe 2 timed out */ { if (dosyslog) syslog (LOG_USER | LOG_CRIT, "Temperature sensor 2 timed out\n"); fprintf (stderr,"Temperature sensor 2 timed out\n"); basetemp = temps [fermenterprobe]; } else /* all as it should be */ basetemp = temps [fermenterprobe] * (1 - probe2factor) /* proportion of 1st fermenter */ + temps [fermenter2probe] * probe2factor; /* proportion of 2nd fermenter */ } /* Talk to the relay board */ void setrelay (int bits) { #ifdef __FreeBSD__ ioctl (relayfd, PPISDATA, &bits); /* just output the bits */ #else /* This kludge to work around potential endianness problems */ char cbits [8]; cbits [0] = bits; /* only last byte */ if (write (relayfd, cbits, 1) < 1) fprintf (stderr, "Can't write to relay board: %s (%d)\n", strerror (errno), errno ); #endif printstatus (1); /* force status output */ } void turn_cooler_on () { /* First update some times */ state = M_COOLING; coolstatus = cooling; /* we're cooling now */ nextheateroff = now; /* in case we change the defaults */ nextcooleroff = now + cooleronmin; /* earliest time to turn the cooler off */ nextcooleron = now + cooleroffmin; /* don't turn cooler on until this long */ /* The temperature we want to drop to */ thisgoal = goaltemp - coolerovershoot * coolerholdoff; /* * Calculate when to turn off the cooler. This can't be outside the * envelope, but it could be before we hit the edge to compensate * for overshoot. * * The rationale here is that when we turn off the cooler, the * ambient temperature will be lower than the wort temperature, so * the wort will continue to cool for some time. There are a number * of models we can apply: * * 1. Assume that the wort temperature drops by a fixed value. In * this case, we can use linear interpolation. * * 2. Assume that the wort temperature drops by a fixed factor in * relationship to the on and off temperatures. In this case, * we can also use linear interpolation. * * 3. Accept the fact that the truth is more complex and attempt to * describe it as a polynomial. In this case, we apply the * polynomial. * * Model 3 clearly is the most accurate, but it's almost impossible * to derive a polynomial with so inaccurate data (temperature * resolution of only 0.06°C), and where the normal circumstances * are that the wort temperature cycles quickly from the upper limit * to the lower limit, and then more slowly from the lower limit to * the upper limit. Since the limits are (almost) the same every * time, it's difficult to find a way to get enough information. * * Under these circumstances, model 1 looks more than adequate. It * falls down, though, if we change the goal temperature during * operation. In this case, a combination of model 1 and model 2 * might be best, but we can't work out what it should be. * * The current compromise is: use model 1. Based on prior * experience, turn the cooler off at a point which should ensure * that the cooler low temperature is at that point between the goal * temperature and the lower bound that has been defined by the * variable 'coolerovershoot' (which defaults to 1, meaning that we * aim for a cooler low temperature equal to the lower bound). * * If the cooler off temperature or the cooler low temperature are * obviously wrong, presumably due to a goal temperature change, * drop the adjustment and assme that there's no overshoot * (i.e. assume that cooler off and cooler low temperatures are the * same). */ if ((lastcoolerontemp > lastcoolerofftemp) && (lastcoolerofftemp > lastcoolerlowtemp) ) /* This is guaranteed to be between 0 and 1 */ overshootratio = (lastcoolerofftemp - lastcoolerlowtemp) / (lastcoolerontemp - lastcoolerlowtemp); else overshootratio = 0; /* * Temperature at which to turn off the cooler. This can't be lower * than the lower bound, but it can be higher than the upper bound. * That's possible, but unlikely. The most likely cause for this * would be a change of the goal temperature, so for the moment * we'll assume that we want to cool at least to the upper bound. */ thiscoolerofftemp = thisgoal + (basetemp - goaltemp) * overshootratio; if (thiscoolerofftemp > (goaltemp + heaterholdoff)) /* too warm, */ thiscoolerofftemp = goaltemp + heaterholdoff; /* correct */ lastcoolerontemp = basetemp; /* note current temperature */ lastcooleron = now; /* and time */ /* * Then set relays. This prints status info, so we need to do it * after the updates above. */ setrelay (1 << coolrelay); /* turn the cooler on */ if (dosyslog) syslog (syslog_prio, "Cooler on, fermenter %2.2f, goal %2.2f\n", basetemp, goaltemp ); if (debugfd) dumptempcalcs ("Cooler on"); } void turn_heater_on () { /* First update some times */ state = M_HEATING; coolstatus = heating; /* we're heating now */ nextcooleroff = now; /* in case we change the defaults */ nextheateroff = now + heateronmin; /* earliest time to turn the heater off */ nextheateron = now + heateroffmin; /* don't turn heater on until this long */ /* The temperature we want to heat to */ thisgoal = goaltemp + heaterovershoot * heaterholdoff; /* * Calculate the temperature at which to turn off the heater. See * the comments at turn_cooler_on () above for the details. */ if ((lastheaterofftemp > lastheaterontemp) && (lastheaterhightemp > lastheaterofftemp) ) /* This is guaranteed to be between 0 and 1 */ overshootratio = (lastheaterofftemp - lastheaterhightemp) / (lastheaterontemp - lastheaterhightemp); else overshootratio = 0; thisheaterofftemp = thisgoal - (goaltemp - basetemp) * overshootratio; if (thisheaterofftemp < (goaltemp - coolerholdoff)) /* too cool, */ thisheaterofftemp = goaltemp - coolerholdoff; /* correct */ lastheaterontemp = basetemp; /* note temperature */ lastheateron = now; /* and time */ /* * Then set relays. This prints status info, so we need to do it * after the updates above. */ setrelay (1 << heatrelay); /* turn the heater on */ if (dosyslog) syslog (syslog_prio, "Heater on, fermenter %2.2f, goal %2.2f\n", basetemp, goaltemp ); if (debugfd) dumptempcalcs ("Heater on"); } /* * Stop cooling. Set a machine state from what's passed to us. */ void turn_cooler_off (int newstate) { coolstatus = idle; state = newstate; /* set the sta */ lastcoolstate = M_COOLING; /* last thing we did was to cool */ lastcoolerofftemp = thisgoal; /* note temperature we were aiming for */ lastcoolerlowtemp = basetemp; /* and the lowest we've been this cycle */ lastcooleroff = now; setrelay (0); /* turn both things off on */ /* * XXX do we want a separate minimum time between cooler off and * heater on? It seems reasonable; for now, use heateroffmin. * Interestingly, this also means that we can use the same code * for turning off both relays. */ nextcooleron = now + cooleroffmin; /* don't turn cooler on until this long */ nextheateron = now + coolertoheaterdelay; if (dosyslog) syslog (syslog_prio, "Cooler off, fermenter %2.2f, goal %2.2f\n", basetemp, goaltemp ); if (debugfd) dumptempcalcs ("Cooler off "); if (newstate == M_IDLE) /* which I think it must be */ { sprintf (idletext, "Cooled for %s", timestring (lastcooleroff - lastcooleron, 0)); idletexttime = now; } } /* * Stop heating. Set a machine state from what's passed to us. */ void turn_heater_off (int newstate) { /* Changed state: stop heating */ coolstatus = idle; state = newstate; /* set the sta */ lastcoolstate = M_HEATING; /* last thing we did was to heat */ lastcoolerofftemp = thisgoal; /* note temperature we were aiming for */ lastheaterhightemp = basetemp; /* and the highest we've been this cycle */ lastheateroff = now; setrelay (0); /* turn both things off on */ nextcooleron = now + heatertocoolerdelay; /* don't turn cooler on until this long */ nextheateron = now + heateroffmin; if (dosyslog) syslog (syslog_prio, "Heater off, fermenter %2.2f, goal %2.2f\n", basetemp, goaltemp ); if (debugfd) dumptempcalcs ("Heater off "); if (newstate == M_IDLE) /* which I think it must be */ { sprintf (idletext, "Heated for %s", timestring (lastheateroff - lastheateron, 0)); idletexttime = now; } } /* This is the main monitor function. */ void monitor_command (int argc, char *argv [], char *arg0 []) { int i; /* * Decide whether we want to be here. Do this before anything else. * * We re-read the .rc file when it changes. It probably contains a * 'monitor' command, which will cause us to reenter. Catch that * situation with the following: */ if (monitoring) return; monitoring = 1; /* * XXX We should take into account having been stopped in mid-ramp. */ realstarttemp = starttemp; /* start at speficied temperature */ tempchangestart = time (NULL); /* in case we're ramping */ /* * Open our files. We absolutely need the serial line and the relay * line, so we fail if we can't open them. If the others fail, we * can limp on regardless. */ if (openserialline ()) return; if (openrelayline ()) return; opengraphlogfile (); openlogfile (); opendisplayfile (); opendebugfile (); /* * Now we need to get at least one temperature for each sensor, so * that things can work at all. * * It's possible that our first read will get an incomplete first * line, so take a couple more tries than theoretically necessary. * * We do this in state M_INIT, which tells gettemp () that we don't * yet have enough information to calculate temperature parameters. * After we move on, in the main loop, we need to call gettemp () * again to calculate them. */ for (i = 0; i < 6; i++) gettemp (); if ((basetemp == NOTEMP) || (temps [ambientprobe] == NOTEMP) ) { fprintf (stderr, "Couldn't get temperatures: Ambient %g, %s %g, %s %g\n", temps [ambientprobe], fermenter1label, temps [fermenterprobe], fermenter2label, temps [fermenter2probe] ); return; } /* * Point of no return. If we get this far, we stay here and loop. */ /* * Set up a listen socket for information feedback. We don't really * need TCP, but this way anybody can get the information with * telnet. * * This code grew from something in Stevens' UNIX Network * Programming. I don't think much is left except possibly some * names. */ listenfd = socket (AF_INET, SOCK_STREAM, 0); if (listenfd < 0) perror ("Can't open TCP listen socket"); if ((i = fcntl (listenfd, F_GETFL, 0)) < 0) perror ("Can't get TCP listen socket flags"); if ((i = fcntl (listenfd, F_SETFL, i | O_NONBLOCK)) < 0) perror ("Can't set TCP listen socket flags"); bzero (&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (tcpport); if (bind (listenfd, (struct sockaddr *) & servaddr, sizeof (servaddr)) < 0) perror ("Can't bind to TCP listen socket"); if (listen (listenfd, SOMAXCONN) < 0) perror ("Can't listen on TCP listen socket"); state = M_INIT; /* initialize */ coolstatus = idle; time (&now); /* get the time */ setrelay (0); /* paranoia */ dump_state ("start monitor"); /* log to debug if wanted */ /* Decide how we're going to change our temperatures. */ goaltemp = realstarttemp; /* start with the start temperature */ /* * Now set plausible defaults for the last event temperatures, so * that the algorithm will work correctly the first time (it will * not apply any correction to the temperatures). */ lastcoolerontemp = goaltemp + coolerholdoff; /* temp last time we turned the cooler on */ lastcoolerofftemp = goaltemp - heaterholdoff; /* temp last time we turned the cooler off */ lastcoolerlowtemp = goaltemp - heaterholdoff; /* lowest temp after turning the cooler off */ lastheaterontemp = goaltemp - heaterholdoff; /* temp last time we turned the heater on */ lastheaterofftemp = goaltemp + coolerholdoff; /* temp last time we turned the heater off */ lastheaterhightemp = goaltemp + coolerholdoff; /* highest temp after turning the heater off */ /* OK, ready to go. Run the state machine. */ while (1) { int connfd; FILE *fpout; if (tempchangeend != 0) /* we're changing temperature over time */ { if (now < tempchangeend) /* still changing */ goaltemp = realstarttemp + (endtemp - realstarttemp) * (now - tempchangestart) / (tempchangeend - tempchangestart); /* interpolate */ else /* got to the end */ { tempchangeend = 0; /* done its dash */ realstarttemp = endtemp; /* we're staying here now */ goaltemp = endtemp; } } else goaltemp = realstarttemp; gettemp (); if (doinfodump) /* we've had a signal telling us to dump state */ { doinfodump = 0; dump_state ("User request"); } checkourrcfile (); /* see if we need to update things */ time (&now); /* get the time */ /* See if any TCP connections are waiting for us */ clilen = sizeof (cliaddr); if ((connfd = accept (listenfd, (struct sockaddr *) &cliaddr, &clilen )) >= 0 ) /* got a request */ { /* XXX This is tacky. Tidy up. */ char mystatus [1024]; unsigned char *addrbyte = (unsigned char *) &(cliaddr.sin_addr.s_addr); if (realhostname (mystatus, 1024, &cliaddr.sin_addr) == HOSTNAME_FOUND) syslog (LOG_INFO | LOG_SECURITY, "Query from %s (%u.%u.%u.%u)\n", mystatus, addrbyte [0], addrbyte [1], addrbyte [2], addrbyte [3] ); else syslog (LOG_INFO | LOG_SECURITY, "Query from unknown (%u.%u.%u.%u)\n", addrbyte [0], addrbyte [1], addrbyte [2], addrbyte [3] ); fpout = fdopen (connfd, "w"); if (fpout == NULL) perror ("Can't reopen connection fd"); statustext (1, 0, mystatus, 1024); fputs (mystatus, fpout); fclose (fpout); } else if ((errno != EWOULDBLOCK) /* real error, */ && (errno != EINTR) ) perror ("Can't listen to TCP listen socket"); /* Update our extremes. */ if (lastcoolstate == M_COOLING) /* we were cooling */ { if (basetemp < lastcoolerlowtemp) lastcoolerlowtemp = basetemp; /* this is the lowest we've been since */ } else if (lastcoolstate == M_HEATING) /* we were heating */ { if (basetemp > lastheaterhightemp) lastheaterhightemp = basetemp; /* this is the highest we've been since */ } switch (state) { case M_INIT: /* initializing */ state = M_IDLE; /* don't think we need to do anything */ gettemp (); /* now do our first calculation */ /* Fall through */ case M_IDLE: /* not doing anything */ /* * We originally stopped heating or cooling when we come within * heaterholdoff or coolerholdoff degrees of the temperature. * It now seems better to stop at exactly the temperature, * otherwise we have to rely on overrun to get the goal * temperature. There will be some overrun, though, so this is * another parameter to watch. */ if (basetemp > (goaltemp + coolerholdoff)) /* too warm */ { /* Want to cool. Can we? */ if (nextcooleron > now) /* can't start yet */ { dump_state ("set cooler on wait"); state = M_COOL_ON_WAIT; } else if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff)) || (temps [ambientprobe] <= minambienttemp) ) state = M_COOL_AMBIENT_ABS; /* absolute difference too high */ /* Passed all hurdles. Turn the cooler on. */ else turn_cooler_on (); } else if (basetemp < (goaltemp - heaterholdoff)) /* too cool */ { /* Want to heat. Can we? */ if (nextheateron > now) /* can't start yet */ { dump_state ("set heater on wait"); state = M_HEAT_ON_WAIT; } else if ((temps [ambientprobe] > (goaltemp + maxheattempdiff)) || (temps [ambientprobe] >= maxambienttemp) ) state = M_HEAT_AMBIENT_ABS; /* absolute difference too high */ /* Passed all hurdles. Turn the heater on. */ else turn_heater_on (); } break; /* Cooling states */ case M_COOLING: /* actively cooling */ if (now >= nextcooleroff) /* we could turn it off if we wanted to */ { if (basetemp < thiscoolerofftemp) /* no longer too warm */ turn_cooler_off (M_IDLE); else if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff)) || (temps [ambientprobe] <= minambienttemp)) turn_cooler_off (M_COOL_AMBIENT_ABS); } /* We have a condition to turn off the heater, but it's too early yet. */ else if ((basetemp < (goaltemp - coolerovershoot * coolerholdoff)) /* no longer too warm */ || (temps [ambientprobe] < (goaltemp - maxcooltempdiff)) ) state = M_COOL_OFF_WAIT; break; break; case M_COOL_ON_WAIT: /* waiting for cooler on time */ /* * Do we still want to cool? First check the ambient * temperature. */ if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff)) || (temps [ambientprobe] <= minambienttemp)) state = M_COOL_AMBIENT_ABS; /* * If we've just been heating, this may be due to an overshoot, * so we only cool if we're outside our range. Otherwise we * should be at least as low as our goal temperature. */ else if (((lastcoolstate != M_HEATING) /* temperature still low enough? */ && (basetemp <= goaltemp) ) || ((lastcoolstate == M_HEATING) && (basetemp <= (goaltemp + coolerholdoff)) ) ) /* * We don't consider the case here that the temperature has * risen high enough to require cooling. That's very * unlikely, and in any case it'll be caught on the next * iteration. */ { /* * We don't consider the case here that the temperature has * dropped low enough to require heating. That's very * unlikely, and in any case it'll be caught on the next * iteration. */ state = M_IDLE; /* yes, just go idle */ strcpy (idletext, "cancelled cooler on wait"); idletexttime = now; dump_state (idletext); } else if (now >= nextcooleron) /* we can turn it on now */ turn_cooler_on (); break; case M_COOL_OFF_WAIT: /* waiting for cooler off time */ /* Do we still want to stop cooling? */ if (now >= nextcooleroff) /* we can turn it off now */ { if (basetemp > (goaltemp + coolerholdoff)) /* is the temperature still high enough? */ state = M_COOLING; /* no, carry on cooling */ else turn_cooler_off (M_IDLE); /* yes, do it */ } break; case M_COOL_AMBIENT_ABS: /* waiting for absolute ambient temperature */ /* Do we still want to cool? */ if (basetemp < goaltemp) /* temperature low enough? */ { /* * We don't consider the case here that the temperature has * dropped low enough to require heating. That's very * unlikely, and in any case it'll be caught on the next * iteration. */ state = M_IDLE; /* yes, just go idle */ strcpy (idletext, "Temperature in range"); idletexttime = now; } else if ((temps [ambientprobe] > (goaltemp - maxcooltempdiff)) /* ambient difference OK now? */ && (temps [ambientprobe] > minambienttemp) && (now >= nextcooleron) ) /* and we can turn it on */ turn_cooler_on (); /* yes, turn cooler on */ break; /* Heating states */ case M_HEATING: /* actively heating */ if (now >= nextheateroff) /* we could turn it off if we wanted to */ { if (basetemp > thisheaterofftemp) /* no longer too cool */ turn_heater_off (M_IDLE); else if ((temps [ambientprobe] > (goaltemp + maxheattempdiff)) || (temps [ambientprobe] >= maxambienttemp) ) turn_heater_off (M_HEAT_AMBIENT_ABS); } /* We have a condition to turn off the heater, but it's too early yet. */ else if ((basetemp > (goaltemp + heaterovershoot * heaterholdoff)) /* no longer too cool */ || (temps [ambientprobe] > (goaltemp + maxheattempdiff)) ) state = M_HEAT_OFF_WAIT; break; case M_HEAT_ON_WAIT: /* waiting for heater on time */ /* * Do we still want to heat? First check the ambient * temperature. */ if ((temps [ambientprobe] > (goaltemp + maxheattempdiff)) || (temps [ambientprobe] >= maxambienttemp) ) state = M_HEAT_AMBIENT_ABS; /* * If we've just been cooling, this may be due to an overshoot, * so we only heat if we're below our range. Otherwise we * should be at least as high as our goal temperature. */ else if (((lastcoolstate != M_COOLING) && (basetemp >= goaltemp) ) || ((lastcoolstate == M_COOLING) && (basetemp >= (goaltemp - heaterholdoff)) ) ) /* * We don't consider the case here that the temperature has * risen high enough to require cooling. That's very * unlikely, and in any case it'll be caught on the next * iteration. */ { strcpy (idletext, "cancelled heater on wait"); idletexttime = now; dump_state (idletext); state = M_IDLE; /* yes, just go idle */ } else if (now >= nextheateron) /* we can turn it on now */ turn_heater_on (); break; case M_HEAT_OFF_WAIT: /* waiting for heater off time */ /* Do we still want to stop heating? */ if (now >= nextheateroff) /* we can turn it off now */ { if (basetemp < (goaltemp - heaterholdoff)) /* is the temperature still high enough? */ state = M_HEATING; /* no, carry on heating */ else turn_heater_off (M_IDLE); } break; case M_HEAT_AMBIENT_ABS: /* waiting for absolute ambient temperature */ /* Do we still want to heat? */ if (basetemp > goaltemp) /* temperature high enough? */ { /* * We don't consider the case here that the temperature has * risen high enough to require cooling. That's very * unlikely, and in any case it'll be caught on the next * iteration. */ strcpy (idletext, "Temperature in range"); idletexttime = now; state = M_IDLE; /* yes, just go idle */ } else if ((temps [ambientprobe] < (goaltemp + maxheattempdiff)) /* ambient difference OK now? */ || (temps [ambientprobe] < maxambienttemp) && (now >= nextheateron) ) /* and we can turn it on */ turn_heater_on (); /* yes, turn heater on */ break; } printstatus (0); } } /* * Debug stuff. This is here because it references static local * variables, and I can't be bothered to rearrange. */ /* Open debug log file. Fail silently. */ void opendebugfile () { if (debugfd) fclose (debugfd); if (debugfile [0]) /* we have a name */ { debugfd = fopen (debugfile, "a"); if (debugfd == NULL) /* error, don't let this stop us */ fprintf (stderr, "Can't open debug log file %s: %s (%d)\n", debugfile, strerror (errno), errno ); } } /* Called only from other debug routines */ void dumpcycleinfo () { fprintf (debugfd, "Temperatures:\n" "Goal temperature:\t%6.2f\n" "Probe 2 factor:\t\t%6.2f\n" "Cooler holdoff:\t\t%6.2f\n" "Heater holdoff:\t\t%6.2f\n" "Cooler overshoot:\t%6.2f\n" "Heater overshoot:\t%6.2f\n" "Max ambient temp:\t%6.2f\n" "Min ambient temp:\t%6.2f\n" "Max ambient temp diff:\t%6.2f\n" "Min ambient temp diff:\t%6.2f\n\n", goaltemp, probe2factor, coolerholdoff, heaterholdoff, coolerovershoot, heaterovershoot, maxambienttemp, minambienttemp, maxcooltempdiff, maxheattempdiff ); fprintf (debugfd, "Current temperatures:\n" "Fermenter 1 (%10s):\t%6.2f\n" "Fermenter 2 (%10s):\t%6.2f\n" "Base temp:\t\t%6.2f (could be out of date)\n" "Ambient:\t\t%6.2f\n" "Room:\t\t\t%6.2f\n\n", fermenter1label, temps [fermenterprobe], fermenter2label, temps [fermenter2probe], basetemp, temps [ambientprobe], temps [roomtempprobe] ); fprintf (debugfd, "Last cycle temperatures\n" "lastheaterontemp:\t%6.2f\n" "lastheaterofftemp:\t%6.2f\n" "lastheaterhightemp:\t%6.2f\n" "thisheaterofftemp:\t%6.2f\n" "lastcoolerontemp:\t%6.2f\n" "lastcoolerofftemp:\t%6.2f\n" "lastcoolerlowtemp:\t%6.2f\n" "thiscoolerofftemp:\t%6.2f\n" "overshootratio:\t\t%6.2f\n" "thisgoal:\t\t%6.2f\n\n", lastheaterontemp, lastheaterofftemp, lastheaterhightemp, thisheaterofftemp, lastcoolerontemp, lastcoolerofftemp, lastcoolerlowtemp, thiscoolerofftemp, overshootratio, thisgoal ); } /* Main dump routine */ void dump_state (char *reason) { time_t now; if (debugfd) { fprintf (debugfd, "================================================================================\n" "Dumping state at "); time (&now); printtime (now, debugfd); fprintf (debugfd, " due to %s\n", reason); fprintf (debugfd, "Files:\n" "Log file:\t\t%s\n" "Graph log file:\t\t%s\n" "Display file:\t\t%s\n" "Debug file:\t\t%s\n\n", logfile, graphlogfile, displayfile, debugfile ); fprintf (debugfd, "Probes and relays:\n" "Ambient:\t\t%d\n" "Room:\t\t\t%d\n" "Fermenter 1 (%10s):\t%d\n" "Fermenter 2 (%10s):\t%d\n" "Cooler relay:\t\t%d\n" "Heater relay:\t\t%d\n\n", roomtempprobe, ambientprobe, fermenter1label, fermenterprobe, fermenter2label, fermenter2probe, coolrelay, heatrelay ); fprintf (debugfd, "Log info:\n" "Log interval:\t\t%d\n" "Log page size:\t\t%d\n" "Graph log interval:\t%d\n" "Do syslog:\t\t%d\n" "Syslog priority:\t%d\n\n", loginterval, logpagesize, graphloginterval, dosyslog, syslog_prio ); fprintf (debugfd, "Timeouts:\n" "Heater on min:\t\t%d\n" "Heater off min:\t\t%d\n" "Cooler on min:\t\t%d\n" "Cooler off min:\t\t%d\n" "Cooler to heater delay:\t%d\n" "Heater to cooler delay:\t%d\n\n", heateronmin, heateroffmin, cooleronmin, cooleroffmin, coolertoheaterdelay, heatertocoolerdelay ); fprintf (debugfd, "Seconds to timeouts:\n" "nextheateron:\t\t%d\n" "nextheateroff:\t\t%d\n" "lastheateron:\t\t%d\n" "lastheateroff:\t\t%d\n" "nextcooleron:\t\t%d\n" "nextcooleroff:\t\t%d\n" "lastcooleron:\t\t%d\n" "lastcooleroff:\t\t%d\n\n", (int) (nextheateron - now), (int) (nextheateroff - now), (int) (lastheateron - now), (int) (lastheateroff - now), (int) (nextcooleron - now), (int) (nextcooleroff - now), (int) (lastcooleron - now), (int) (lastcooleroff - now) ); dumpcycleinfo (); /* information about on/off calcs */ fprintf (debugfd, "Currrent status\n" "coolstatus:\t\t%s\n" "state:\t\t\t%d\n" "lastcoolstate:\t\t%s\n\n", machinestatetext [coolstatus], state, machinestatetext [lastcoolstate] ); fprintf (debugfd, "Relative times:\n" "Now:\t\t\t%d\n" "Next logtime:\t\t%d\n" "Next graphlogtime:\t%d\n" "Last display:\t\t%d\n", (int) (now), (int) (nextlogtime - now), (int) (nextgraphlogtime - now), (int) (lastdisplay - now) ); fprintf (debugfd, "================================================================================\n"); fflush (debugfd); } } /* Print info about state changes */ void dumpstatechange (char *reason) { if (debugfd) { fprintf (debugfd, "====================\n%s ", reason); printtime (now, debugfd); fprintf (debugfd, "\nTemperatures:\n" "Goal temperature:\t%6.2f\n" "Cooler holdoff:\t\t%6.2f\n" "Heater holdoff:\t\t%6.2f\n" "Cooler overshoot:\t%6.2f of holdoff\n" "Heater overshoot:\t%6.2f of holdoff\n" "Max ambient temp:\t%6.2f\n" "Min ambient temp:\t%6.2f\n" "Max ambient temp diff:\t%6.2f\n" "Min ambient temp diff:\t%6.2f\n\n", goaltemp, coolerholdoff, heaterholdoff, coolerovershoot, heaterovershoot, maxambienttemp, minambienttemp, maxcooltempdiff, maxheattempdiff ); fflush (debugfd); } } void dumptempcalcs (char *reason) { if (debugfd) { dumpstatechange (reason); dumpcycleinfo (); fflush (debugfd); } }