1 /*
2  * Monitor function.
3  *
4  * Greg Lehey, 18 May 2004
5  *
6  * Copyright (c) 2004 by Greg Lehey
7  *
8  *  This software is distributed under the so-called ``Berkeley
9  *  License'':
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions
13  * are met:
14  *
15  * 1.  Redistributions of source code must retain the above copyright
16  *    notice, this list of conditions and the following disclaimer.
17  * 2.  Redistributions in binary form must reproduce the above copyright
18  *    notice, this list of conditions and the following disclaimer in the
19  *    documentation and/or other materials provided with the distribution.
20  *
21  * This software is provided ``as is'', and any express or implied
22  * warranties, including, but not limited to, the implied warranties
23  * of merchantability and fitness for a particular purpose are
24  * disclaimed.  In no event shall Greg Lehey be liable for any direct,
25  * indirect, incidental, special, exemplary, or consequential damages
26  * (including, but not limited to, procurement of substitute goods or
27  * services; loss of use, data, or profits; or business interruption)
28  * however caused and on any theory of liability, whether in contract,
29  * strict liability, or tort (including negligence or otherwise)
30  * arising in any way out of the use of this software, even if advised
31  * of the possibility of such damage.
32  *
33  * $Id: monitor.c,v 1.24 2005/08/17 03:48:32 grog Exp grog $
34  */
35 
36 #include <syslog.h>
37 #include <stdarg.h>
38 #include <sys/types.h>
39 #include <sys/socket.h>
40 #include <netinet/in.h>
41 #ifdef __FreeBSD__
42 #include <libutil.h>					    /* for realhostname */
43 #include <dev/ppbus/ppi.h>
44 #include <dev/ppbus/ppbconf.h>
45 #endif
46 #ifdef __DragonFly__
47 #include <libutil.h>					    /* for realhostname */
48 #include <dev/misc/ppi/ppi.h>
49 #include <bus/ppbus/ppbconf.h>
50 #endif
51 #include "main.h"
52 #include <termios.h>
53 #include "statemachine.h"
54 
55 /*
56  * For tradition's sake, we make odd buffers the length of a punched
57  * card.
58  */
59 #define CARDSIZE 80
60 
61 /*
62  * xterm command sequences to clear the screen (termcap cl capability)
63  * and home the cursor (termcap ho capability).  We don't use termcap
64  * here because that requires some assumption about the kind of
65  * terminal, and there's no particular reason to assume that it
66  * relates to the TERM environment variable, but mainly because we're
67  * too lazy.  FIXME.  Anyway, this is pretty ubiquitous nowadays.
68  */
69 char *xterm_cls = "";                              /* clear screen sequence for xterm (cl) */
70 char *xterm_home = "";                                  /* home cursor sequence (ho) */
71 char *xterm_cleol = "";                                 /* clear to EOL sequence (ce) */
72 
73 int linefd;                                                 /* file descriptor of input serial line */
74 int relayfd;                                                /* file descriptor of relay output */
75 FILE *logfd;                                                /* for logging */
76 FILE *graphlogfd;                                           /* for writing graph info */
77 FILE *displayfd;                                            /* for writing direct status info */
78 FILE *debugfd;                                              /* for writing debug information */
79 
80 char firmwarerev [CARDSIZE];                                /* save temperature probe startup info here */
81 #define NOTEMP -500
82 
83 /*
84  * Temperatures read from serial line.  The sensors are numbered 1 to
85  * 4, so we use 0 for a null value.
86  */
87 float temps [5] = {0, NOTEMP, NOTEMP, NOTEMP, NOTEMP};      /* one temperature per probe */
88 time_t lastreading [5];					    /* and last reading time, to detect problems */
89 
90 /*
91  * start temperature at last configuration read in time, used for
92  * ramping calculations Normally this is the start temperature in the
93  * configuration file, but if we change the end temperature without
94  * changing the start temperature, we'll set it to the current
95  * temperature.
96  */
97 float realstarttemp;
98 
99 /*
100  * temperature to base our calculations on.  Somewhere between the two
101  * fermenter temperatures.  See comments in probe2factor in commands.c
102  * for more details.
103  */
104 float basetemp;
105 
106 /* The earliest time we can do certain things.  */
107 time_t nextheateron;                                        /* next time we can turn the heater on */
108 time_t nextcooleron;                                        /* next time we can turn the cooler on */
109 
110 time_t nextheateroff;                                       /* next time we can turn the heater off */
111 time_t nextcooleroff;                                       /* next time we can turn the cooler off */
112 
113 time_t probetimeout = 60;				    /* probe timeout in seconds */
114 
115 char idletext [80];					    /* a text to print when we go idle */
116 time_t idletexttime;					    /* and when we did it */
117 
118 /*
119  * And, for completely unrelated purposes, the last time we did the
120  * same certain things.  We use these values to calculate when to turn
121  * heater and cooler off without overshooting our envelope.
122  *
123  * The algorithm looks like this: when we heat or cool, note the
124  * temperatures when we turn the heat on and off.  Then note the
125  * maximum temperature overshoot before the temperature turns around.
126  * Use these three values next time to guess where to stop heating or
127  * cooling to just stay within the envelope.
128  *
129  * There's a margin for error here, of course, particularly when
130  * starting from way outside the envelope.  This will tend towards
131  * underestimating the overshoot (since it's relatively small compared
132  * to the start and end temperature difference).  That's OK: it's
133  * still better than no estimate.
134  */
135 time_t lastheateron;                                        /* last time we turned the heater on */
136 time_t lastcooleron;                                        /* last time we turned the cooler on */
137 
138 time_t lastheateroff;					    /* and times we turned them off */
139 time_t lastcooleroff;
140 
141 /*
142  * The fermenter temperatures corresponding to the last times above.
143  */
144 
145 float lastheaterontemp = NOTEMP;                            /* temp last time we turned the heater on */
146 float lastcoolerontemp = NOTEMP;                            /* temp last time we turned the cooler on */
147 
148 float lastheaterofftemp = NOTEMP;                           /* temp last time we turned the heater off */
149 float lastcoolerofftemp = NOTEMP;                           /* temp last time we turned the cooler off */
150 
151 /*
152  * The highest and lowest the temperature ascended or descended after
153  * turning off.  These values are initialized to make sense the first
154  * time through the loop.
155  */
156 float lastheaterhightemp = NOTEMP;                          /* highest temp after turning the heater off */
157 float lastcoolerlowtemp = -NOTEMP;                          /* lowest temp after turning the cooler off */
158 
159 float thiscoolerofftemp = NOTEMP;                           /* temperature to choose this time around */
160 float thisheaterofftemp = NOTEMP;                           /* temperature to choose this time around */
161 
162 float overshootratio;                                       /* ratio of temperature overshoot */
163 float thisgoal;                                             /* temperature we want to get to this time */
164 
165 /* What are we doing at the moment? */
166 enum coolstatus coolstatus;
167 enum machinestate state;
168 enum machinestate lastcoolstate = M_IDLE;                   /* last kind of temperature manipulation */
169 
170 time_t now;                                                 /* current time */
171 time_t nextlogtime;                                         /* and time next log entry is due */
172 time_t nextgraphlogtime;                                    /* and time next graph log entry is due */
173 time_t lastdisplay;                                         /* time of last display output */
174 
175 int monitoring;                                             /* set when we're in monitor () */
176 int doinfodump;                                             /* set to dump info if we're debugging */
177 
178 struct termios seriallineparms;                             /* parameters for the input line */
179 
180 /* Stuff for network reporting of temperatures. */
181 
182 int listenfd;
183 int connfd;
184 pid_t childpid;
185 socklen_t clilen;
186 struct sockaddr_in cliaddr;
187 struct sockaddr_in servaddr;
188 
189 /*
190  * Open and set parameters for serial line.
191  * Return 1 on failure.
192  */
openserialline()193 int openserialline ()
194 {
195   if (linefd)                                               /* stuff left behind? */
196     close (linefd);
197   linefd = open (serialline, O_RDONLY);
198   if (linefd < 0)
199     {
200     fprintf (stderr,
201              "Can't open serial line %s: %s (%d)\n",
202              serialline,
203              strerror (errno),
204              errno );
205     return 1;
206     }
207 
208   /* Set up the line the way we want it.  */
209   if (tcgetattr (linefd, &seriallineparms) < 0)             /* can't get line info */
210     {
211     fprintf (stderr,
212              "Can't get parameters for serial line %s: %s (%d)\n",
213              serialline,
214              strerror (errno),
215              errno );
216     return 1;
217     }
218 
219   seriallineparms.c_iflag = 0;                              /* input flags: none */
220   seriallineparms.c_oflag = 0;                              /* no output flags */
221   seriallineparms.c_cflag = CS8 | CREAD;                    /* control flags: 8 bits, read enable */
222   seriallineparms.c_lflag = ICANON;                         /* local flags: canonical input */
223   seriallineparms.c_ispeed = linespeed;                     /* set line speed */
224   /*
225    * We don't need to output, but if the speeds aren't the same, the
226    * tcsetattr fails with EINVAL.
227    */
228   seriallineparms.c_ospeed = linespeed;                     /* set line speed */
229 
230   if (tcsetattr (linefd, TCSANOW, &seriallineparms) < 0)    /* can't get line info */
231     {
232     fprintf (stderr,
233              "Can't get parameters for serial line %s: %s (%d)\n",
234              serialline,
235              strerror (errno),
236              errno );
237     return 1;
238     }
239   return 0;
240   }
241 
242 /*
243  * Open relay line.
244  * Return 1 on failure.
245  */
openrelayline()246 int openrelayline ()
247 {
248   if (relayfd)
249     close (relayfd);
250 
251   /* Now open the files.  */
252   relayfd = open (relayline, O_WRONLY);
253   if (relayfd < 0)
254     {
255     fprintf (stderr,
256              "Can't open relay controller %s: %s (%d)\n",
257              relayline,
258              strerror (errno),
259              errno );
260     return 1;
261     }
262   return 0;
263   }
264 
265 /* Open graph log file.  Fail silently.  */
opengraphlogfile()266 void opengraphlogfile ()
267 {
268   if (graphlogfd)
269     fclose (graphlogfd);
270   if (graphlogfile [0])
271     {
272     graphlogfd = fopen (graphlogfile, "a");
273     if (graphlogfd == NULL)                                 /* error, don't let this stop us */
274       fprintf (stderr,
275                "Can't open graph log file %s: %s (%d)\n",
276                graphlogfile,
277                strerror (errno),
278                errno );
279     }
280   }
281 
282 /* Open log file.  Fail silently.  */
openlogfile()283 void openlogfile ()
284 {
285   if (logfd)
286     fclose (logfd);
287   if (logfile [0])
288     {
289    logfd = fopen (logfile, "a");
290     if (logfd == NULL)                                      /* error, don't let this stop us */
291       fprintf (stderr,
292                "Can't open log file %s: %s (%d)\n",
293               logfile,
294                strerror (errno),
295                errno );
296     }
297   }
298 
299 /* Open display log file.  Fail silently.  */
opendisplayfile()300 void opendisplayfile ()
301 {
302   if (displayfd)
303     fclose (displayfd);
304   if (displayfile [0])
305     {
306     displayfd = fopen (displayfile, "a");
307     if (displayfd == NULL)                                  /* error, don't let this stop us */
308       fprintf (stderr,
309                "Can't open display log file %s: %s (%d)\n",
310                displayfile,
311                strerror (errno),
312                errno );
313     /*
314      * Clear the screen.  We don't use termcap here because that
315      * requires some assumption about the kind of terminal, and
316      * there's no particular reason to assume that it relates to the
317      * TERM environment variable.  Anyway, this is pretty ubiquitous
318      * nowadays.
319      */
320     else if (isatty (fileno (displayfd)))
321       fputs (xterm_cls, displayfd);                         /* clear screen */
322     }
323   }
324 
325 /*
326  * This is a wrapper around checkrcfile which checks for file name
327  * changes.
328  */
checkourrcfile()329 void checkourrcfile ()                                      /* see if we need to update things */
330 {
331   char oldserialline [MAXPATHLEN];
332   char oldlogfile [MAXPATHLEN];
333   char oldgraphlogfile [MAXPATHLEN];
334   char oldrelayline [MAXPATHLEN];
335   char olddisplayfile [MAXPATHLEN];
336   char olddebugfile [MAXPATHLEN];
337   float oldstarttemp;
338   float oldendtemp;
339   time_t oldtempchangeend;
340 
341   /* Keep track of what we had before */
342   strcpy (oldserialline, serialline);
343   strcpy (oldlogfile, logfile);
344   strcpy (oldgraphlogfile, graphlogfile);
345   strcpy (oldrelayline, relayline);
346   strcpy (olddisplayfile, displayfile);
347   oldstarttemp = starttemp;
348   oldendtemp = endtemp;
349   oldtempchangeend = tempchangeend;
350 
351   if (checkrcfile ())                                       /* see if anything has changed */
352     {
353     if (debugfd)
354       dump_state ("rcfile changed");
355     if (strcmp (oldserialline, serialline))
356       openserialline ();
357     if (strcmp (oldlogfile, logfile))
358       openlogfile ();
359     if (strcmp (oldgraphlogfile, graphlogfile))
360       opengraphlogfile ();
361     if (strcmp (oldrelayline, relayline))
362       openrelayline ();
363     if (strcmp (olddisplayfile, displayfile))
364       {
365       if (displayfd)					    /* currently displaying, */
366 	{
367 	if (strlen (displayfile) == 0)			    /* no display file any more */
368 	  fprintf (displayfd, "\n*** Terminating status display ***\n");
369 	else
370 	  fprintf (displayfd, "\n*** Moving status display to %s ***\n", displayfile);
371 	}
372       opendisplayfile ();
373       }
374     if (strcmp (olddebugfile, debugfile))
375       opendebugfile ();
376 
377     /*
378      * Yet another attempt at the start/end temperature stuff.
379      *
380      * 1: If we change the start temperature, it should take effect
381      * immediately.  If the end temperature hasn't changed, we
382      * approach it at the specified end time.  It's not clear how much
383      * use this is.
384      *
385      * 2: If we change the end temperature but not the start
386      * temperature, it should start from the current temperature and
387      * approach this temperature at the specified time.  If the time
388      * is in the past, it should take place immediately (yes, this is
389      * just another way to shoot yourself in the foot).
390      *
391      * 3: If both start and end temperature change, we just set them
392      * normally.
393      */
394     if (starttemp != oldstarttemp)			    /* start temp changed, */
395       {
396       setrelay (0);					    /* turn things off for the time being */
397       state = M_IDLE;					    /* and start again */
398       strcpy (idletext, "Due to setting new parameters");
399       idletexttime = now;
400       /* Forget our timeouts */
401       nextheateron = now;
402       nextcooleron = now;
403       nextheateroff = now;
404       nextcooleroff = now;
405       if (starttemp != oldstarttemp)			    /* start temp has changed, */
406 	{
407 	realstarttemp = starttemp;			    /* start at this temperature */
408 	tempchangestart = time (NULL);			    /* now */
409 	}
410       }
411     else if ((endtemp != oldendtemp) 			    /* or end temp changed */
412 	     || (tempchangeend != oldtempchangeend) )
413       {
414       realstarttemp = goaltemp;				    /* current goal temperature, */
415       tempchangestart = time (NULL);			    /* start measuring from now */
416       }
417     }
418   }
419 
420 /* Format and print a time */
timestring(time_t time,int local)421 char *timestring (time_t time, int local)
422 {
423   struct tm *bursttime;
424   static char string [20];
425 
426   /*
427    * You'd think we'd always want local time, but we use this function
428    * to represent differences between two times, and localtime adds
429    * the time zone offset :-(
430    */
431   if (local)
432     bursttime = localtime (&time);
433   else
434     bursttime = gmtime (&time);
435   sprintf (string,
436            "%02d:%02d:%02d ",
437            bursttime->tm_hour,
438            bursttime->tm_min,
439            bursttime->tm_sec );
440   return string;
441   }
442 
443 /* Format and print a time */
printtime(time_t time,FILE * fd)444 void printtime (time_t time, FILE *fd)
445 {
446   fputs (timestring (time, 1), fd);
447   }
448 
449 /*
450  * Create a current status log message and return to 'text'.  Include
451  * a header if 'header' is set, and assume it's an xterm if 'screen'
452  * is set.
453  */
statustext(int header,int screen,char * text,int textlen)454 void statustext (int header, int screen, char *text, int textlen)
455 {
456   char *et = text;					    /* point to where we put next text */
457   char *cleol = "";					    /* clear to EOL when required */
458 
459   if (header)
460     {
461     if (screen)                                             /* displaying to screen, */
462       {
463       cleol = xterm_cleol;				    /* set clear to EOL string */
464       strcpy (et, xterm_cls);
465       et = &et [strlen (et)];
466       }
467     sprintf (et,
468              "Time    %6s %7s  Base  Ambient Goal  Offset  Room\n"
469              "        %6s %7s%s\n",
470 	     fermenter1label1,
471 	     fermenter2label1,
472 	     fermenter1label2,
473 	     fermenter2label2,
474 	     cleol );
475     }
476   strlcat (et, timestring (now, 1), textlen);
477   et = &et [strlen (et)];
478   sprintf (et,
479            "%6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f%s\n",
480            temps [fermenterprobe],
481            temps [fermenter2probe],
482            basetemp,
483            temps [ambientprobe],
484            goaltemp,
485            basetemp - goaltemp,
486            temps [roomtempprobe],
487 	   cleol );
488   et = &et [strlen (et)];
489   sprintf (et, "Status: %s ", machinestatetext [state]);    /* display state */
490 
491   /* For the time wait states, show what we're waiting for */
492   switch (state)
493     {
494   case M_IDLE:
495     if (idletext [0])					    /* we have a comment to make */
496       {
497       strlcat (et, idletext, textlen);
498       et = &et [strlen (et)];
499       if (now > (idletexttime + idledisplaytime))	    /* outlived its usefulness, */
500 	idletext [0] = '\0';				    /* and never more */
501       }
502     break;
503 
504   case M_COOL_ON_WAIT:                                      /* waiting for cooler on time */
505     strlcat (et, timestring (nextcooleron, 1), textlen);
506     et = &et [strlen (et)];
507     break;
508 
509   case M_COOL_OFF_WAIT:                                     /* waiting for cooler off time */
510     strlcat (et, timestring (nextcooleroff, 1), textlen);
511     et = &et [strlen (et)];
512     break;
513 
514   case M_HEAT_ON_WAIT:                                      /* waiting for heater on time */
515     strlcat (et, timestring (nextheateron, 1), textlen);
516     et = &et [strlen (et)];
517     break;
518 
519   case M_HEAT_OFF_WAIT:                                     /* waiting for heater off time */
520     strlcat (et, timestring (nextheateroff, 1), textlen);
521     et = &et [strlen (et)];
522     break;
523 
524   default:
525     break;
526     }
527 
528   strlcat (et, xterm_cleol, textlen);			    /* clear to end of line */
529   et = &et [strlen (et)];
530   strlcat (et, "\n", textlen);				    /* only \n if not on screen */
531   }
532 
533 /*
534  * Log current status.  Print a header if 'header' is set,
535  * and assume it's an xterm if 'screen' is set.
536  */
logstatus(FILE * fd,int header,int screen)537 void logstatus (FILE *fd, int header, int screen)
538 {
539   char mystatus [1024];
540 
541   statustext (header, screen, mystatus, 1024);
542   fputs (mystatus, fd);
543   }
544 
545 /*
546  * Write current fermenter status for plotting purposes.  If force
547  * !=0, print even if our time is not yet up.  This is for events like
548  * relays on and off.
549  */
printstatus(int force)550 void printstatus (int force)
551 {
552   static int outlines = 0;                                  /* output lines on log file */
553 
554   if (logfd                                                 /* we're logging */
555       && ((now >= nextlogtime) || force ) )
556     {
557     logstatus (logfd, outlines == 0, 0);
558     if (++outlines > logpagesize)
559       outlines = 0;                                         /* force a new page every 50 lines */
560     if (! force)
561       nextlogtime = now + loginterval;                      /* next time for normal log */
562     }
563   if (displayfd && (now > lastdisplay))                     /* not more than once a second */
564     {
565     fseek (displayfd, 0, SEEK_SET);
566     logstatus (displayfd, 1, isatty (fileno (displayfd)));
567     lastdisplay = now;
568     }
569 
570   if (graphlogfd                                            /* got a graph log */
571       && (state != M_INIT)				    /* and we're not initializing */
572       && ((now >= nextgraphlogtime)			    /* and we're ready to write */
573 	   || force ) )					    /* or we have no choice */
574     {
575     fprintf (graphlogfd,
576              "%d %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %6.2f %d\n",
577              (int) now,
578              temps [fermenterprobe],
579              temps [fermenter2probe],
580              basetemp,
581              temps [ambientprobe],
582              goaltemp,
583              temps [fermenterprobe] - goaltemp,
584              temps [roomtempprobe],
585              coolstatus );
586     fflush (graphlogfd);
587     if (! force)
588       nextgraphlogtime = now + graphloginterval;            /* next time for normal log */
589     }
590   }
591 
592 /*
593  * Read temperature information from the serial line and save it
594  * somewhere safe.
595  */
gettemp()596 void gettemp ()
597 {
598   int probe;
599   float temp;
600   char linein [CARDSIZE];                                   /* read from serial line */
601   int fields;
602   int inlen;
603   time_t now;						    /* current time */
604 
605   inlen = read (linefd, linein, CARDSIZE);                  /* get a temperature */
606 
607   if (inlen != PROBERECORDLEN)				    /* invalid probe data record */
608     {
609     if (inlen < 0)
610       fprintf (stderr,
611 	       "Can't read serial line: %s (%d)\n",
612 	       strerror (errno),
613 	       errno );
614     else if (state != M_INIT)				    /* this can happen during initialization */
615       fprintf (stderr,
616 	       "Invalid temperature sensor input: \"%s\", length %d\n",
617 	       linein,
618 	       inlen );
619     return;
620     }
621 
622   time (&now);
623   fields = sscanf (linein, "%d %g\n", &probe, &temp);       /* get a temperature */
624   if (fields != 2)
625     /*
626      * We don't bother mentioning errors.  They can happen when we
627      * start the program and read a partial record.
628      */
629     return;
630 
631   if ((probe < 1) || (probe > 4))                           /* invalid probe number */
632     return;
633   temps [probe] = temp;
634   lastreading [probe] = now;
635 
636   if (state == M_INIT)					    /* no calculations yet */
637     return;
638 
639   if (lastreading [fermenterprobe] < now - probetimeout)    /* probe 1 has timed out */
640     {
641     if (lastreading [fermenter2probe] < now - probetimeout) /* and so has probe 2 */
642       {
643       if (dosyslog)
644 	syslog (LOG_USER | LOG_EMERG,
645 		"Both temperature sensors timed out\n");
646       fprintf (stderr,"Both temperature sensors timed out\n");
647       sleep (1);
648       }
649     else if (probe2factor == 0)				    /* probe 2 up, but we're ignoring it */
650       {
651       if (dosyslog)
652 	syslog (LOG_USER | LOG_EMERG,
653 		"Temperature sensor timed out\n");
654       fprintf (stderr,"Temperature sensor timed out\n");
655       sleep (1);
656       }
657     else						    /* only sensor 1 down */
658       {
659       if (dosyslog)
660 	syslog (LOG_USER | LOG_CRIT,
661 		"Temperature sensor 1 timed out\n");
662       fprintf (stderr,"Temperature sensor 1 timed out\n");
663       basetemp = temps [fermenter2probe];
664       }
665     }
666   else if (lastreading [fermenter2probe] < now - probetimeout) /* probe 2 timed out */
667     {
668     if (dosyslog)
669       syslog (LOG_USER | LOG_CRIT,
670 	      "Temperature sensor 2 timed out\n");
671     fprintf (stderr,"Temperature sensor 2 timed out\n");
672     basetemp = temps [fermenterprobe];
673     }
674   else							    /* all as it should be */
675     basetemp = temps [fermenterprobe] * (1 - probe2factor)  /* proportion of 1st fermenter */
676       + temps [fermenter2probe] * probe2factor;		    /* proportion of 2nd fermenter */
677   }
678 
679 /* Talk to the relay board */
setrelay(int bits)680 void setrelay (int bits)
681 {
682 #if defined(__FreeBSD__) || defined(__DragonFly__)
683   ioctl (relayfd, PPISDATA, &bits);                         /* just output the bits */
684 #else
685   /* This kludge to work around potential endianness problems */
686   char cbits [8];
687 
688   cbits [0] = bits;                                         /* only last byte */
689 
690   if (write (relayfd, cbits, 1) < 1)
691     fprintf (stderr,
692              "Can't write to relay board: %s (%d)\n",
693              strerror (errno),
694              errno );
695 #endif
696   printstatus (1);                                          /* force status output */
697   }
698 
turn_cooler_on()699 void turn_cooler_on ()
700 {
701   /* First update some times */
702   state = M_COOLING;
703   coolstatus = cooling;                                     /* we're cooling now */
704   nextheateroff = now;                                      /* in case we change the defaults */
705   nextcooleroff = now + cooleronmin;                        /* earliest time to turn the cooler off */
706   nextcooleron = now + cooleroffmin;                        /* don't turn cooler on until this long */
707 
708   /* The temperature we want to drop to */
709   thisgoal = goaltemp - coolerovershoot * coolerholdoff;
710 
711   /*
712    * Calculate when to turn off the cooler.  This can't be outside the
713    * envelope, but it could be before we hit the edge to compensate
714    * for overshoot.
715    *
716    * The rationale here is that when we turn off the cooler, the
717    * ambient temperature will be lower than the wort temperature, so
718    * the wort will continue to cool for some time.  There are a number
719    * of models we can apply:
720    *
721    * 1.  Assume that the wort temperature drops by a fixed value.  In
722    *     this case, we can use linear interpolation.
723    *
724    * 2.  Assume that the wort temperature drops by a fixed factor in
725    *     relationship to the on and off temperatures.  In this case,
726    *     we can also use linear interpolation.
727    *
728    * 3.  Accept the fact that the truth is more complex and attempt to
729    *     describe it as a polynomial.  In this case, we apply the
730    *     polynomial.
731    *
732    * Model 3 clearly is the most accurate, but it's almost impossible
733    * to derive a polynomial with so inaccurate data (temperature
734    * resolution of only 0.06�C), and where the normal circumstances
735    * are that the wort temperature cycles quickly from the upper limit
736    * to the lower limit, and then more slowly from the lower limit to
737    * the upper limit.  Since the limits are (almost) the same every
738    * time, it's difficult to find a way to get enough information.
739    *
740    * Under these circumstances, model 1 looks more than adequate.  It
741    * falls down, though, if we change the goal temperature during
742    * operation.  In this case, a combination of model 1 and model 2
743    * might be best, but we can't work out what it should be.
744    *
745    * The current compromise is: use model 1.  Based on prior
746    * experience, turn the cooler off at a point which should ensure
747    * that the cooler low temperature is at that point between the goal
748    * temperature and the lower bound that has been defined by the
749    * variable 'coolerovershoot' (which defaults to 1, meaning that we
750    * aim for a cooler low temperature equal to the lower bound).
751    *
752    * If the cooler off temperature or the cooler low temperature are
753    * obviously wrong, presumably due to a goal temperature change,
754    * drop the adjustment and assme that there's no overshoot
755    * (i.e.  assume that cooler off and cooler low temperatures are the
756    * same).
757    */
758 
759   if  ((lastcoolerontemp > lastcoolerofftemp)
760        && (lastcoolerofftemp > lastcoolerlowtemp) )
761     /* This is guaranteed to be between 0 and 1 */
762     overshootratio = (lastcoolerofftemp - lastcoolerlowtemp) / (lastcoolerontemp - lastcoolerlowtemp);
763   else
764     overshootratio = 0;
765 
766   /*
767    * Temperature at which to turn off the cooler.  This can't be lower
768    * than the lower bound, but it can be higher than the upper bound.
769    * That's possible, but unlikely.  The most likely cause for this
770    * would be a change of the goal temperature, so for the moment
771    * we'll assume that we want to cool at least to the upper bound.
772    */
773 
774   thiscoolerofftemp = thisgoal + (basetemp - goaltemp) * overshootratio;
775   if (thiscoolerofftemp > (goaltemp + heaterholdoff))       /* too warm, */
776     thiscoolerofftemp = goaltemp + heaterholdoff;           /* correct */
777 
778   lastcoolerontemp = basetemp;                              /* note current temperature */
779   lastcooleron = now;					    /* and time */
780 
781   /*
782    * Then set relays.  This prints status info, so we need to do it
783    * after the updates above.
784    */
785   setrelay (1 << coolrelay);                                /* turn the cooler on */
786   if (dosyslog)
787     syslog (syslog_prio,
788             "Cooler on, fermenter %2.2f, goal %2.2f\n",
789             basetemp,
790             goaltemp );
791   if (debugfd)
792     dumptempcalcs ("Cooler on");
793   }
794 
turn_heater_on()795 void turn_heater_on ()
796 {
797   /* First update some times */
798   state = M_HEATING;
799   coolstatus = heating;                                     /* we're heating now */
800   nextcooleroff = now;                                      /* in case we change the defaults */
801   nextheateroff = now + heateronmin;                        /* earliest time to turn the heater off */
802   nextheateron = now + heateroffmin;                        /* don't turn heater on until this long */
803 
804   /* The temperature we want to heat to */
805   thisgoal = goaltemp + heaterovershoot * heaterholdoff;
806 
807   /*
808    * Calculate the temperature at which to turn off the heater.  See
809    * the comments at turn_cooler_on () above for the details.
810    */
811   if  ((lastheaterofftemp > lastheaterontemp)
812        && (lastheaterhightemp > lastheaterofftemp) )
813     /* This is guaranteed to be between 0 and 1 */
814     overshootratio = (lastheaterofftemp - lastheaterhightemp) / (lastheaterontemp - lastheaterhightemp);
815   else
816     overshootratio = 0;
817 
818   thisheaterofftemp = thisgoal - (goaltemp - basetemp) * overshootratio;
819   if (thisheaterofftemp < (goaltemp - coolerholdoff))       /* too cool, */
820     thisheaterofftemp = goaltemp - coolerholdoff;           /* correct */
821 
822   lastheaterontemp = basetemp;                              /* note temperature */
823   lastheateron = now;					    /* and time */
824 
825   /*
826    * Then set relays.  This prints status info, so we need to do it
827    * after the updates above.
828    */
829   setrelay (1 << heatrelay);                                /* turn the heater on */
830   if (dosyslog)
831     syslog (syslog_prio,
832             "Heater on, fermenter %2.2f, goal %2.2f\n",
833             basetemp,
834             goaltemp );
835   if (debugfd)
836     dumptempcalcs ("Heater on");
837   }
838 
839 /*
840  * Stop cooling.  Set a machine state from what's passed to us.
841  */
turn_cooler_off(int newstate)842 void turn_cooler_off (int newstate)
843 {
844   coolstatus = idle;
845   state = newstate;                                         /* set the sta */
846   lastcoolstate = M_COOLING;                                /* last thing we did was to cool */
847   lastcoolerofftemp = thisgoal;                             /* note temperature we were aiming for */
848   lastcoolerlowtemp = basetemp;                             /* and the lowest we've been this cycle */
849   lastcooleroff = now;
850   setrelay (0);                                             /* turn both things off on */
851 
852   /*
853    * XXX do we want a separate minimum time between cooler off and
854    * heater on?  It seems reasonable; for now, use heateroffmin.
855    * Interestingly, this also means that we can use the same code
856    * for turning off both relays.
857    */
858   nextcooleron = now + cooleroffmin;                        /* don't turn cooler on until this long */
859   nextheateron = now + coolertoheaterdelay;
860   if (dosyslog)
861     syslog (syslog_prio,
862             "Cooler off, fermenter %2.2f, goal %2.2f\n",
863             basetemp,
864             goaltemp );
865   if (debugfd)
866     dumptempcalcs ("Cooler off ");
867   if (newstate == M_IDLE)				    /* which I think it must be */
868     {
869     sprintf (idletext, "Cooled for %s", timestring (lastcooleroff - lastcooleron, 0));
870     idletexttime = now;
871     }
872   }
873 
874 /*
875  * Stop heating.  Set a machine state from what's passed to us.
876  */
turn_heater_off(int newstate)877 void turn_heater_off (int newstate)
878 {
879   /* Changed state: stop heating */
880   coolstatus = idle;
881   state = newstate;                                         /* set the sta */
882   lastcoolstate = M_HEATING;                                /* last thing we did was to heat */
883   lastcoolerofftemp = thisgoal;                             /* note temperature we were aiming for */
884   lastheaterhightemp = basetemp;                            /* and the highest we've been this cycle */
885   lastheateroff = now;
886   setrelay (0);                                             /* turn both things off on */
887 
888   nextcooleron = now + heatertocoolerdelay;                 /* don't turn cooler on until this long */
889   nextheateron = now + heateroffmin;
890   if (dosyslog)
891     syslog (syslog_prio,
892             "Heater off, fermenter %2.2f, goal %2.2f\n",
893             basetemp,
894             goaltemp );
895   if (debugfd)
896     dumptempcalcs ("Heater off ");
897   if (newstate == M_IDLE)				    /* which I think it must be */
898     {
899     sprintf (idletext, "Heated for %s", timestring (lastheateroff - lastheateron, 0));
900     idletexttime = now;
901     }
902   }
903 
904 /* This is the main monitor function.  */
monitor_command(int argc,char * argv[],char * arg0[])905 void monitor_command (int argc, char *argv [], char *arg0 [])
906 {
907   int i;
908 
909   /*
910    * Decide whether we want to be here.  Do this before anything else.
911    *
912    * We re-read the .rc file when it changes.  It probably contains a
913    * 'monitor' command, which will cause us to reenter.  Catch that
914    * situation with the following:
915    */
916   if (monitoring)
917     return;
918   monitoring = 1;
919 
920   /*
921    * XXX We should take into account having been stopped in mid-ramp.
922    */
923   realstarttemp = starttemp;				    /* start at speficied temperature */
924   tempchangestart = time (NULL);			    /* in case we're ramping */
925 
926   /*
927    * Open our files.  We absolutely need the serial line and the relay
928    * line, so we fail if we can't open them.  If the others fail, we
929    * can limp on regardless.
930    */
931   if (openserialline ())
932     return;
933   if (openrelayline ())
934     return;
935   opengraphlogfile ();
936   openlogfile ();
937   opendisplayfile ();
938   opendebugfile ();
939 
940   /*
941    * Now we need to get at least one temperature for each sensor, so
942    * that things can work at all.
943    *
944    * It's possible that our first read will get an incomplete first
945    * line, so take a couple more tries than theoretically necessary.
946    *
947    * We do this in state M_INIT, which tells gettemp () that we don't
948    * yet have enough information to calculate temperature parameters.
949    * After we move on, in the main loop, we need to call gettemp ()
950    * again to calculate them.
951    */
952   for (i = 0; i < 6; i++)
953     gettemp ();
954 
955   if ((basetemp == NOTEMP)
956       || (temps [ambientprobe] == NOTEMP) )
957     {
958     fprintf (stderr,
959              "Couldn't get temperatures: Ambient %g, %s %g, %s %g\n",
960              temps [ambientprobe],
961 	     fermenter1label,
962              temps [fermenterprobe],
963 	     fermenter2label,
964              temps [fermenter2probe] );
965     return;
966     }
967 
968   /*
969    * Point of no return.  If we get this far, we stay here and loop.
970    */
971 
972   /*
973    * Set up a listen socket for information feedback.  We don't really
974    * need TCP, but this way anybody can get the information with
975    * telnet.
976    *
977    * This code grew from something in Stevens' UNIX Network
978    * Programming.  I don't think much is left except possibly some
979    * names.
980    */
981 
982   listenfd = socket (AF_INET, SOCK_STREAM, 0);
983   if (listenfd < 0)
984     perror ("Can't open TCP listen socket");
985 
986   if ((i = fcntl (listenfd, F_GETFL, 0)) < 0)
987     perror ("Can't get TCP listen socket flags");
988 
989   if ((i = fcntl (listenfd, F_SETFL, i | O_NONBLOCK)) < 0)
990     perror ("Can't set TCP listen socket flags");
991 
992   bzero (&servaddr, sizeof (servaddr));
993   servaddr.sin_family = AF_INET;
994   servaddr.sin_addr.s_addr = htonl (INADDR_ANY);
995   servaddr.sin_port = htons (tcpport);
996 
997   if (bind (listenfd, (struct sockaddr *) & servaddr, sizeof (servaddr)) < 0)
998     perror ("Can't bind to TCP listen socket");
999 
1000   if (listen (listenfd, SOMAXCONN) < 0)
1001     perror ("Can't listen on TCP listen socket");
1002 
1003   state = M_INIT;                                           /* initialize */
1004   coolstatus = idle;
1005   time (&now);                                              /* get the time */
1006   setrelay (0);                                             /* paranoia */
1007   dump_state ("start monitor");                             /* log to debug if wanted */
1008 
1009   /* Decide how we're going to change our temperatures. */
1010   goaltemp = realstarttemp;				    /* start with the start temperature */
1011 
1012   /*
1013    * Now set plausible defaults for the last event temperatures, so
1014    * that the algorithm will work correctly the first time (it will
1015    * not apply any correction to the temperatures).
1016    */
1017   lastcoolerontemp = goaltemp + coolerholdoff;              /* temp last time we turned the cooler on */
1018   lastcoolerofftemp = goaltemp - heaterholdoff;             /* temp last time we turned the cooler off */
1019   lastcoolerlowtemp = goaltemp - heaterholdoff;             /* lowest temp after turning the cooler off */
1020   lastheaterontemp = goaltemp - heaterholdoff;              /* temp last time we turned the heater on */
1021   lastheaterofftemp = goaltemp + coolerholdoff;             /* temp last time we turned the heater off */
1022   lastheaterhightemp = goaltemp + coolerholdoff;            /* highest temp after turning the heater off */
1023 
1024   /* OK, ready to go.  Run the state machine.  */
1025   while (1)
1026     {
1027     int connfd;
1028     FILE *fpout;
1029 
1030     if (tempchangeend != 0)				    /* we're changing temperature over time */
1031       {
1032       if (now < tempchangeend)				    /* still changing */
1033 	goaltemp = realstarttemp + (endtemp - realstarttemp)
1034 	  * (now - tempchangestart)
1035 	    / (tempchangeend - tempchangestart);	    /* interpolate */
1036       else						    /* got to the end */
1037 	{
1038 	tempchangeend = 0;				    /* done its dash */
1039 	realstarttemp = endtemp;			    /* we're staying here now */
1040 	goaltemp = endtemp;
1041 	}
1042       }
1043     else
1044       goaltemp = realstarttemp;
1045     gettemp ();
1046     if (doinfodump)					    /* we've had a signal telling us to dump state */
1047       {
1048       doinfodump = 0;
1049       dump_state ("User request");
1050       }
1051     checkourrcfile ();                                      /* see if we need to update things */
1052     time (&now);                                            /* get the time */
1053 
1054     /* See if any TCP connections are waiting for us */
1055     clilen = sizeof (cliaddr);
1056     if ((connfd = accept (listenfd,
1057 			  (struct sockaddr *) &cliaddr,
1058 			  &clilen ))
1059 	>= 0 )						    /* got a request */
1060       {
1061       /* XXX This is tacky.  Tidy up. */
1062       char mystatus [1024];
1063       unsigned char *addrbyte = (unsigned char *) &(cliaddr.sin_addr.s_addr);
1064 
1065       if (realhostname (mystatus, 1024, &cliaddr.sin_addr) == HOSTNAME_FOUND)
1066 	syslog (LOG_INFO | LOG_SECURITY,
1067 		"Query from %s (%u.%u.%u.%u)\n",
1068 		mystatus,
1069 		addrbyte [0],
1070 		addrbyte [1],
1071 		addrbyte [2],
1072 		addrbyte [3] );
1073       else
1074 	syslog (LOG_INFO | LOG_SECURITY,
1075 		"Query from unknown (%u.%u.%u.%u)\n",
1076 		addrbyte [0],
1077 		addrbyte [1],
1078 		addrbyte [2],
1079 		addrbyte [3] );
1080       fpout = fdopen (connfd, "w");
1081       if (fpout == NULL)
1082 	perror ("Can't reopen connection fd");
1083 
1084       statustext (1, 0, mystatus, 1024);
1085       fputs (mystatus, fpout);
1086       fclose (fpout);
1087       }
1088     else if ((errno != EWOULDBLOCK)			    /* real error, */
1089 	     && (errno != EINTR) )
1090       perror ("Can't listen to TCP listen socket");
1091 
1092     /* Update our extremes.  */
1093     if (lastcoolstate == M_COOLING)                         /* we were cooling */
1094       {
1095       if (basetemp < lastcoolerlowtemp)
1096         lastcoolerlowtemp = basetemp;                       /* this is the lowest we've been since */
1097       }
1098     else if (lastcoolstate == M_HEATING)                    /* we were heating */
1099       {
1100       if (basetemp > lastheaterhightemp)
1101         lastheaterhightemp = basetemp;                      /* this is the highest we've been since */
1102       }
1103 
1104     switch (state)
1105       {
1106     case M_INIT:                                            /* initializing */
1107       state = M_IDLE;                                       /* don't think we need to do anything */
1108       gettemp ();					    /* now do our first calculation */
1109       /* Fall through */
1110 
1111     case M_IDLE:                                            /* not doing anything */
1112       /*
1113        * We originally stopped heating or cooling when we come within
1114        * heaterholdoff or coolerholdoff degrees of the temperature.
1115        * It now seems better to stop at exactly the temperature,
1116        * otherwise we have to rely on overrun to get the goal
1117        * temperature.  There will be some overrun, though, so this is
1118        * another parameter to watch.
1119        */
1120 
1121       if (basetemp > (goaltemp + coolerholdoff))            /* too warm */
1122         {
1123         /* Want to cool.  Can we? */
1124         if (nextcooleron > now)                             /* can't start yet */
1125           {
1126           dump_state ("set cooler on wait");
1127           state = M_COOL_ON_WAIT;
1128           }
1129         else if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff))
1130                  || (temps [ambientprobe] <= minambienttemp) )
1131           state = M_COOL_AMBIENT_ABS;                       /* absolute difference too high */
1132         /* Passed all hurdles.  Turn the cooler on.  */
1133         else
1134           turn_cooler_on ();
1135         }
1136       else if (basetemp < (goaltemp - heaterholdoff))       /* too cool */
1137         {
1138         /* Want to heat.  Can we? */
1139         if (nextheateron > now)                             /* can't start yet */
1140           {
1141           dump_state ("set heater on wait");
1142           state = M_HEAT_ON_WAIT;
1143           }
1144         else if ((temps [ambientprobe] > (goaltemp + maxheattempdiff))
1145                  || (temps [ambientprobe] >= maxambienttemp) )
1146           state = M_HEAT_AMBIENT_ABS;                       /* absolute difference too high */
1147         /* Passed all hurdles.  Turn the heater on.  */
1148         else
1149           turn_heater_on ();
1150         }
1151       break;
1152 
1153       /* Cooling states */
1154 
1155     case M_COOLING:                                         /* actively cooling */
1156       if (now >= nextcooleroff)                             /* we could turn it off if we wanted to */
1157         {
1158         if (basetemp < thiscoolerofftemp)                   /* no longer too warm */
1159           turn_cooler_off (M_IDLE);
1160         else if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff))
1161                  || (temps [ambientprobe] <= minambienttemp))
1162           turn_cooler_off (M_COOL_AMBIENT_ABS);
1163         }
1164       /* We have a condition to turn off the heater, but it's too early yet.  */
1165       else if ((basetemp < (goaltemp - coolerovershoot * coolerholdoff)) /* no longer too warm */
1166                || (temps [ambientprobe] < (goaltemp - maxcooltempdiff)) )
1167           state = M_COOL_OFF_WAIT;
1168       break;
1169 
1170       break;
1171 
1172     case M_COOL_ON_WAIT:                                    /* waiting for cooler on time */
1173       /*
1174        * Do we still want to cool?  First check the ambient
1175        * temperature.
1176        */
1177       if ((temps [ambientprobe] < (goaltemp - maxcooltempdiff))
1178           || (temps [ambientprobe] <= minambienttemp))
1179         state = M_COOL_AMBIENT_ABS;
1180       /*
1181        * If we've just been heating, this may be due to an overshoot,
1182        * so we only cool if we're outside our range.  Otherwise we
1183        * should be at least as low as our goal temperature.
1184        */
1185       else if (((lastcoolstate != M_HEATING)		    /* temperature still low enough? */
1186                 && (basetemp <= goaltemp) )
1187                || ((lastcoolstate == M_HEATING)
1188                    && (basetemp <= (goaltemp + coolerholdoff)) ) )
1189         /*
1190          * We don't consider the case here that the temperature has
1191          * risen high enough to require cooling.  That's very
1192          * unlikely, and in any case it'll be caught on the next
1193          * iteration.
1194          */
1195         {
1196         /*
1197          * We don't consider the case here that the temperature has
1198          * dropped low enough to require heating.  That's very
1199          * unlikely, and in any case it'll be caught on the next
1200          * iteration.
1201          */
1202         state = M_IDLE;                                     /* yes, just go idle */
1203 	strcpy (idletext, "cancelled cooler on wait");
1204 	idletexttime = now;
1205         dump_state (idletext);
1206         }
1207       else if (now >= nextcooleron)                         /* we can turn it on now */
1208         turn_cooler_on ();
1209       break;
1210 
1211     case M_COOL_OFF_WAIT:                                   /* waiting for cooler off time */
1212       /* Do we still want to stop cooling? */
1213       if (now >= nextcooleroff)                             /* we can turn it off now */
1214         {
1215         if (basetemp > (goaltemp + coolerholdoff))          /* is the temperature still high enough? */
1216           state = M_COOLING;                                /* no, carry on cooling */
1217         else
1218           turn_cooler_off (M_IDLE);                         /* yes, do it */
1219         }
1220       break;
1221 
1222     case M_COOL_AMBIENT_ABS:                                /* waiting for absolute ambient temperature */
1223       /* Do we still want to cool? */
1224       if (basetemp < goaltemp)                              /* temperature low enough? */
1225 	{
1226         /*
1227          * We don't consider the case here that the temperature has
1228          * dropped low enough to require heating.  That's very
1229          * unlikely, and in any case it'll be caught on the next
1230          * iteration.
1231          */
1232         state = M_IDLE;                                     /* yes, just go idle */
1233 	strcpy (idletext, "Temperature in range");
1234 	idletexttime = now;
1235 	}
1236       else if ((temps [ambientprobe] > (goaltemp - maxcooltempdiff)) /* ambient difference OK now? */
1237                && (temps [ambientprobe] > minambienttemp)
1238                && (now >= nextcooleron) )                   /* and we can turn it on */
1239         turn_cooler_on ();                                  /* yes, turn cooler on */
1240       break;
1241 
1242       /* Heating states */
1243 
1244     case M_HEATING:                                         /* actively heating */
1245       if (now >= nextheateroff)                             /* we could turn it off if we wanted to */
1246         {
1247         if (basetemp > thisheaterofftemp)                   /* no longer too cool */
1248           turn_heater_off (M_IDLE);
1249         else if ((temps [ambientprobe] > (goaltemp + maxheattempdiff))
1250                  || (temps [ambientprobe] >= maxambienttemp) )
1251           turn_heater_off (M_HEAT_AMBIENT_ABS);
1252         }
1253       /* We have a condition to turn off the heater, but it's too early yet.  */
1254       else if ((basetemp > (goaltemp + heaterovershoot * heaterholdoff)) /* no longer too cool */
1255                || (temps [ambientprobe] > (goaltemp + maxheattempdiff)) )
1256         state = M_HEAT_OFF_WAIT;
1257       break;
1258 
1259     case M_HEAT_ON_WAIT:                                    /* waiting for heater on time */
1260 
1261           /*
1262            * Do we still want to heat?  First check the ambient
1263            * temperature.
1264            */
1265       if ((temps [ambientprobe] > (goaltemp + maxheattempdiff))
1266           || (temps [ambientprobe] >= maxambienttemp) )
1267         state = M_HEAT_AMBIENT_ABS;
1268       /*
1269        * If we've just been cooling, this may be due to an overshoot,
1270        * so we only heat if we're below our range.  Otherwise we
1271        * should be at least as high as our goal temperature.
1272        */
1273       else if (((lastcoolstate != M_COOLING)
1274            && (basetemp >= goaltemp) )
1275           || ((lastcoolstate == M_COOLING)
1276               && (basetemp >= (goaltemp - heaterholdoff)) ) )
1277         /*
1278          * We don't consider the case here that the temperature has
1279          * risen high enough to require cooling.  That's very
1280          * unlikely, and in any case it'll be caught on the next
1281          * iteration.
1282          */
1283         {
1284 	strcpy (idletext, "cancelled heater on wait");
1285 	idletexttime = now;
1286         dump_state (idletext);
1287         state = M_IDLE;                                     /* yes, just go idle */
1288         }
1289       else if (now >= nextheateron)                         /* we can turn it on now */
1290         turn_heater_on ();
1291       break;
1292 
1293     case M_HEAT_OFF_WAIT:                                   /* waiting for heater off time */
1294       /* Do we still want to stop heating? */
1295       if (now >= nextheateroff)                             /* we can turn it off now */
1296         {
1297         if (basetemp < (goaltemp - heaterholdoff))          /* is the temperature still high enough? */
1298           state = M_HEATING;                                /* no, carry on heating */
1299         else
1300           turn_heater_off (M_IDLE);
1301         }
1302       break;
1303 
1304     case M_HEAT_AMBIENT_ABS:                                /* waiting for absolute ambient temperature */
1305       /* Do we still want to heat? */
1306       if  (basetemp > goaltemp)                             /* temperature high enough? */
1307 	{
1308         /*
1309          * We don't consider the case here that the temperature has
1310          * risen high enough to require cooling.  That's very
1311          * unlikely, and in any case it'll be caught on the next
1312          * iteration.
1313          */
1314 	strcpy (idletext, "Temperature in range");
1315 	idletexttime = now;
1316         state = M_IDLE;                                     /* yes, just go idle */
1317 	}
1318       else if ((temps [ambientprobe] < (goaltemp + maxheattempdiff)) /* ambient difference OK now? */
1319                || (temps [ambientprobe] < maxambienttemp)
1320                && (now >= nextheateron) )                   /* and we can turn it on */
1321         turn_heater_on ();                                  /* yes, turn heater on */
1322 
1323       break;
1324       }
1325     printstatus (0);
1326     }
1327   }
1328 
1329 /*
1330  * Debug stuff.  This is here because it references static local
1331  * variables, and I can't be bothered to rearrange.
1332  */
1333 
1334 /* Open debug log file.  Fail silently.  */
opendebugfile()1335 void opendebugfile ()
1336 {
1337   if (debugfd)
1338     fclose (debugfd);
1339   if (debugfile [0])                                        /* we have a name */
1340     {
1341     debugfd = fopen (debugfile, "a");
1342     if (debugfd == NULL)                                    /* error, don't let this stop us */
1343       fprintf (stderr,
1344                "Can't open debug log file %s: %s (%d)\n",
1345                debugfile,
1346                strerror (errno),
1347                errno );
1348     }
1349   }
1350 
1351 
1352 /* Called only from other debug routines */
dumpcycleinfo()1353 void dumpcycleinfo ()
1354 {
1355   fprintf (debugfd,
1356            "Temperatures:\n"
1357            "Goal temperature:\t%6.2f\n"
1358            "Probe 2 factor:\t\t%6.2f\n"
1359            "Cooler holdoff:\t\t%6.2f\n"
1360            "Heater holdoff:\t\t%6.2f\n"
1361            "Cooler overshoot:\t%6.2f\n"
1362            "Heater overshoot:\t%6.2f\n"
1363            "Max ambient temp:\t%6.2f\n"
1364            "Min ambient temp:\t%6.2f\n"
1365            "Max ambient temp diff:\t%6.2f\n"
1366            "Min ambient temp diff:\t%6.2f\n\n",
1367            goaltemp,
1368            probe2factor,
1369            coolerholdoff,
1370            heaterholdoff,
1371            coolerovershoot,
1372            heaterovershoot,
1373            maxambienttemp,
1374            minambienttemp,
1375            maxcooltempdiff,
1376            maxheattempdiff );
1377 
1378   fprintf (debugfd,
1379            "Current temperatures:\n"
1380            "Fermenter 1 (%10s):\t%6.2f\n"
1381            "Fermenter 2 (%10s):\t%6.2f\n"
1382            "Base temp:\t\t%6.2f (could be out of date)\n"
1383            "Ambient:\t\t%6.2f\n"
1384            "Room:\t\t\t%6.2f\n\n",
1385 	   fermenter1label,
1386            temps [fermenterprobe],
1387 	   fermenter2label,
1388            temps [fermenter2probe],
1389            basetemp,
1390            temps [ambientprobe],
1391            temps [roomtempprobe] );
1392 
1393   fprintf (debugfd,
1394            "Last cycle temperatures\n"
1395            "lastheaterontemp:\t%6.2f\n"
1396            "lastheaterofftemp:\t%6.2f\n"
1397            "lastheaterhightemp:\t%6.2f\n"
1398            "thisheaterofftemp:\t%6.2f\n"
1399            "lastcoolerontemp:\t%6.2f\n"
1400            "lastcoolerofftemp:\t%6.2f\n"
1401            "lastcoolerlowtemp:\t%6.2f\n"
1402            "thiscoolerofftemp:\t%6.2f\n"
1403            "overshootratio:\t\t%6.2f\n"
1404            "thisgoal:\t\t%6.2f\n\n",
1405            lastheaterontemp,
1406            lastheaterofftemp,
1407            lastheaterhightemp,
1408            thisheaterofftemp,
1409            lastcoolerontemp,
1410            lastcoolerofftemp,
1411            lastcoolerlowtemp,
1412            thiscoolerofftemp,
1413            overshootratio,
1414            thisgoal );
1415   }
1416 
1417 /* Main dump routine */
dump_state(char * reason)1418 void dump_state (char *reason)
1419 {
1420   time_t now;
1421 
1422   if (debugfd)
1423     {
1424     fprintf (debugfd,
1425              "================================================================================\n"
1426              "Dumping state at ");
1427     time (&now);
1428     printtime (now, debugfd);
1429     fprintf (debugfd, " due to %s\n", reason);
1430 
1431     fprintf (debugfd,
1432              "Files:\n"
1433              "Log file:\t\t%s\n"
1434              "Graph log file:\t\t%s\n"
1435              "Display file:\t\t%s\n"
1436              "Debug file:\t\t%s\n\n",
1437              logfile,
1438              graphlogfile,
1439              displayfile,
1440              debugfile );
1441 
1442     fprintf (debugfd,
1443              "Probes and relays:\n"
1444              "Ambient:\t\t%d\n"
1445              "Room:\t\t\t%d\n"
1446              "Fermenter 1 (%10s):\t%d\n"
1447              "Fermenter 2 (%10s):\t%d\n"
1448              "Cooler relay:\t\t%d\n"
1449              "Heater relay:\t\t%d\n\n",
1450              roomtempprobe,
1451              ambientprobe,
1452 	     fermenter1label,
1453              fermenterprobe,
1454 	     fermenter2label,
1455              fermenter2probe,
1456              coolrelay,
1457              heatrelay );
1458 
1459     fprintf (debugfd,
1460              "Log info:\n"
1461              "Log interval:\t\t%d\n"
1462              "Log page size:\t\t%d\n"
1463              "Graph log interval:\t%d\n"
1464              "Do syslog:\t\t%d\n"
1465              "Syslog priority:\t%d\n\n",
1466              loginterval,
1467              logpagesize,
1468              graphloginterval,
1469              dosyslog,
1470              syslog_prio );
1471 
1472     fprintf (debugfd,
1473              "Timeouts:\n"
1474              "Heater on min:\t\t%d\n"
1475              "Heater off min:\t\t%d\n"
1476              "Cooler on min:\t\t%d\n"
1477              "Cooler off min:\t\t%d\n"
1478              "Cooler to heater delay:\t%d\n"
1479              "Heater to cooler delay:\t%d\n\n",
1480              heateronmin,
1481              heateroffmin,
1482              cooleronmin,
1483              cooleroffmin,
1484              coolertoheaterdelay,
1485              heatertocoolerdelay );
1486 
1487     fprintf (debugfd,
1488              "Seconds to timeouts:\n"
1489              "nextheateron:\t\t%d\n"
1490              "nextheateroff:\t\t%d\n"
1491              "lastheateron:\t\t%d\n"
1492              "lastheateroff:\t\t%d\n"
1493              "nextcooleron:\t\t%d\n"
1494              "nextcooleroff:\t\t%d\n"
1495              "lastcooleron:\t\t%d\n"
1496              "lastcooleroff:\t\t%d\n\n",
1497              (int) (nextheateron - now),
1498              (int) (nextheateroff - now),
1499              (int) (lastheateron - now),
1500              (int) (lastheateroff - now),
1501              (int) (nextcooleron - now),
1502              (int) (nextcooleroff - now),
1503              (int) (lastcooleron - now),
1504              (int) (lastcooleroff - now) );
1505 
1506     dumpcycleinfo ();                                       /* information about on/off calcs */
1507     fprintf (debugfd,
1508              "Currrent status\n"
1509              "coolstatus:\t\t%s\n"
1510              "state:\t\t\t%d\n"
1511              "lastcoolstate:\t\t%s\n\n",
1512              machinestatetext [coolstatus],
1513              state,
1514              machinestatetext [lastcoolstate] );
1515 
1516     fprintf (debugfd,
1517              "Relative times:\n"
1518              "Now:\t\t\t%d\n"
1519              "Next logtime:\t\t%d\n"
1520              "Next graphlogtime:\t%d\n"
1521              "Last display:\t\t%d\n",
1522              (int) (now),
1523              (int) (nextlogtime - now),
1524              (int) (nextgraphlogtime - now),
1525              (int) (lastdisplay - now) );
1526     fprintf (debugfd,
1527              "================================================================================\n");
1528     fflush (debugfd);
1529     }
1530   }
1531 
1532 /* Print info about state changes */
dumpstatechange(char * reason)1533 void dumpstatechange (char *reason)
1534 {
1535   if (debugfd)
1536     {
1537     fprintf (debugfd,
1538              "====================\n%s ",
1539              reason);
1540     printtime (now, debugfd);
1541     fprintf (debugfd,
1542              "\nTemperatures:\n"
1543              "Goal temperature:\t%6.2f\n"
1544              "Cooler holdoff:\t\t%6.2f\n"
1545              "Heater holdoff:\t\t%6.2f\n"
1546              "Cooler overshoot:\t%6.2f of holdoff\n"
1547              "Heater overshoot:\t%6.2f of holdoff\n"
1548              "Max ambient temp:\t%6.2f\n"
1549              "Min ambient temp:\t%6.2f\n"
1550              "Max ambient temp diff:\t%6.2f\n"
1551              "Min ambient temp diff:\t%6.2f\n\n",
1552              goaltemp,
1553              coolerholdoff,
1554              heaterholdoff,
1555              coolerovershoot,
1556              heaterovershoot,
1557              maxambienttemp,
1558              minambienttemp,
1559              maxcooltempdiff,
1560              maxheattempdiff );
1561     fflush (debugfd);
1562     }
1563   }
1564 
dumptempcalcs(char * reason)1565 void dumptempcalcs (char *reason)
1566 {
1567   if (debugfd)
1568     {
1569     dumpstatechange (reason);
1570     dumpcycleinfo ();
1571     fflush (debugfd);
1572     }
1573   }
1574