view tempctrl.c @ 61:9c5db7fee912

Make 'td log' work without printing an error.
author Daniel O'Connor <darius@dons.net.au>
date Mon, 08 Apr 2013 23:11:55 +0930
parents bbdf5642a7b7
children 0916bfe3f418
line wrap: on
line source

/*
 * Temperature control logic, copied from AVR version
 *
 * Copyright (c) 2012
 *      Daniel O'Connor <darius@dons.net.au>.  All rights reserved.
 *
 * 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 BY AUTHOR AND CONTRIBUTORS ``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 AUTHOR OR CONTRIBUTORS 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.
 */

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include "stm32f10x.h"

#include "1wire.h"
#include "ff.h"
#include "flash.h"
#include "tempctrl.h"

/* Holds all the settings needed */
typedef struct {
    uint8_t	fermenter_ROM[8];
    uint8_t	fridge_ROM[8];
    uint8_t	ambient_ROM[8];
    int16_t	target_temp;
    uint16_t	hysteresis;
    /* How much to under/overshoot on heating/cooling */
    int16_t	minheatovershoot;
    int16_t	mincoolovershoot;

    /* Minimum time the cooler can be on/off */
    int16_t	mincoolontime;
    int16_t	mincoolofftime;

    /* Minimum time the heater can be on/off */
    int16_t	minheatontime;
    int16_t	minheatofftime;

#define TC_MODE_AUTO	'a'	/* Automatic control */
#define TC_MODE_HEAT	'h'	/* Force heating */
#define TC_MODE_COOL	'c'	/* Force cooling */
#define TC_MODE_IDLE	'i'	/* Force idle */
#define TC_MODE_NOTHING	'n'	/* Do nothing (like idle but log nothing) */
    char	mode;
    
    /* GPIO port the heater & cooler are on */
    char	coolport;
    uint16_t	coolpin;
    uint8_t	coolinv;
    
    char 	heatport;
    uint16_t	heatpin;
    uint8_t	heatinv;

    /* Check/stale times */
    int16_t	check_interval;
    int16_t	stale_factor;

    char	logfilefmt[64];

    uint8_t	pad[1]; /* Pad to multiple of 4 bytes */
} settings_t;

/* Current settings in RAM */
static settings_t	settings;

/* Defaults that are shoved into SPI flash if it isn't inited */
const settings_t	default_settings = {
    .fermenter_ROM = { 0x10, 0x4c, 0x7d, 0x53, 0x01, 0x08, 0x00, 0xff },
    .fridge_ROM =  { 0x10, 0x6d, 0x40, 0x53, 0x01, 0x08, 0x00, 0x16 },
    .ambient_ROM = { 0x10, 0x76, 0x05, 0x53, 0x01, 0x08, 0x00, 0x8e },
    .target_temp = 1000,
    .hysteresis = 100,
    .minheatovershoot = 50,
    .mincoolovershoot = -50,
    .mincoolontime = 300,
    .mincoolofftime = 600,
    .minheatontime = 60,
    .minheatofftime = 60,
    .mode = TC_MODE_AUTO,
    .coolport = 'E',
    .coolpin = 4,
    .coolinv = 1,
    .heatport = 'E',
    .heatpin = 5,
    .heatinv = 1,
    .check_interval = 10,
    .stale_factor = 3,
    .logfilefmt = "/%Y%M%D.log"
};

/* Local variable declarations */

/* Local function prototypes */
static void		tempctrl_load_or_init_settings(void);
static void		tempctrl_default_settings(void);
static void		tempctrl_write_settings(void);
static void		setstate(char state);
static const char *	state2long(char s);
static int		fmttemp(char *buf, const char *name, int tmp, const char *trailer);
static GPIO_TypeDef	*char2port(char port);

/* 
 * tempctrl_init
 *
 */
void
tempctrl_init(void) {
    tempctrl_load_or_init_settings();
    setstate('i');
}

/* 
 * tempctrl_update
 *
 * Should be called in a normal context, could run things that take a long time.
 * (ie 1wire bus stuff)
 *
 */
void
tempctrl_update(void) {
    /* State variables */
    static int32_t	checktime = 0;		// Time of next check
    static int32_t	lastdata = INT32_MIN;	// Last time we got data

    static int16_t	fermenter_temp = 0;	// Fermenter temperature
    static int16_t	fridge_temp = 0;	// Fridge temperature
    static int16_t	ambient_temp = 0;	// Ambient temperature
    static int32_t	lastheaton = INT32_MIN;	// Last time the heater was on
    static int32_t	lastheatoff = INT32_MIN;// Last time the heater was off
    static int32_t	lastcoolon = INT32_MIN;	// Last time the cooler was on
    static int32_t	lastcooloff = INT32_MIN;// Last time the cooler was off
    static char		currstate = 'i';	// Current state
    /* We init to times to INT32_MIN so that things function properly when
     * now < settings.minheat/cool/on/offtime */
    
    /* Temporary variables */
    int32_t		tempt;
    int16_t		diff;
    char		nextstate;
    int			forced;
    int			stale;
    time_t		t;
    struct tm		tm;
    char		linebuf[90], *p;
    FRESULT		fserr;
    FIL			f;
    char		fbuf[20];
    
    t = time(NULL);
    stale = 0;
    tempt = 0;
    
    /* Time to check temperatures? */
    if (t < checktime)
	return;
    
    checktime = t + settings.check_interval;

    /* Don't do any logging, just force idle and leave */
    if (settings.mode == TC_MODE_NOTHING) {
	nextstate = 'i';
	goto skip;
    }
    
    /* Update our temperatures
     * Can take a while (800ms each!)
     */
    tempt = OWGetTemp(settings.fermenter_ROM);
    fridge_temp = OWGetTemp(settings.fridge_ROM);
    ambient_temp = OWGetTemp(settings.ambient_ROM);
    
    /* We only care about this one, only update the value we decide on
     * only if it is valid
     */
    if (tempt > OW_TEMP_BADVAL) {
	fermenter_temp = tempt;
	lastdata = t;
    }
    
    /* Check for stale data */
    if (lastdata + (settings.check_interval * settings.stale_factor) < t)
	stale = 1;

    /* Default to remaining as we are */
    nextstate = '-';

    /* Temperature diff, -ve => too cold, +ve => too warm */
    diff = fermenter_temp - settings.target_temp;

    switch (currstate) {
	case 'i':
	    /* If we're idle then only heat or cool if the temperate difference is out of the
	     * hysteresis band and the heater/cooler have been off long enough
	     */
	    if (abs(diff) > settings.hysteresis) {
		if (diff < 0 && settings.minheatofftime + lastheatoff < t)
		    nextstate = 'h';
		else if (diff > 0 && settings.mincoolofftime + lastcooloff < t)
		    nextstate = 'c';
	    }
	    break;
	    
	case 'c':
	    /* Work out if we should go idle (based on min on time & overshoot) */
	    if (diff + settings.mincoolovershoot < 0 &&
		settings.mincoolontime + lastcoolon < t)
		nextstate = 'i';
	    break;

	case 'h':
	    if (diff - settings.minheatovershoot > 0 &&
		settings.minheatontime + lastheaton < t)
		nextstate = 'i';
	    break;

	default:
	    printf("\nUnknown state %c, going to idle\n", currstate);
	    nextstate = 'i';
	    break;
    }

    /* Override if we have stale data */
    if (stale)
	nextstate = 'i';
    
    /* Handle state forcing */
    if (settings.mode != TC_MODE_AUTO)
	forced = 1;
    else
	forced = 0;
    
    if (settings.mode == TC_MODE_IDLE)
	nextstate = 'i';
    else if (settings.mode == TC_MODE_HEAT)
	nextstate = 'h';
    else if (settings.mode == TC_MODE_COOL)
	nextstate = 'c';

    /* Keep track of when we last turned things on or off */
  skip:
    switch  (nextstate) {
	case 'c':
	    if (currstate == 'h')
		lastheatoff = t;
	    lastcoolon = t;
	    break;

	case 'h':
	    if (currstate == 'c')
		lastcooloff = t;
	    lastheaton = t;
	    break;

	default:
	    if (currstate == 'c')
		lastcooloff = t;
	    if (currstate == 'h')
		lastheatoff = t;
    }
    
    if (nextstate != '-')
	currstate = nextstate;

    
    if (settings.mode != TC_MODE_NOTHING) {
	localtime_r(&t, &tm);
	p = linebuf;
	p += strftime(p, sizeof(linebuf) - 1, "%Y/%m/%d %H:%M:%S: ", &tm);
	p += fmttemp(p, "Tr", settings.target_temp, ", ");
	p += fmttemp(p, "Fm", tempt, ", ");
	p += fmttemp(p, "Fr", fridge_temp, ", ");
	p += fmttemp(p, "Am", ambient_temp, ", ");
	sprintf(p, "St: %s, Fl: %s%s\n", state2long(currstate), 
		forced ? "F" : "", 
		stale ? "S" : "");
	fputs(linebuf, stdout);
	if (settings.logfilefmt[0] != '\0') {
	    strftime(fbuf, sizeof(fbuf) - 1, settings.logfilefmt, &tm);
	    
	    if ((fserr = f_open(&f, fbuf, FA_WRITE | FA_OPEN_ALWAYS)) != FR_OK) {
		printf("Failed to open file: %d\n", fserr);
		goto openerr;
	    }

	    if ((fserr = f_lseek(&f, f_size(&f))) != FR_OK) {
		printf("Failed to seek to end of file: %d\n", fserr);
		goto openerr;
	    }
	
	    f_puts(linebuf, &f);
	    f_close(&f);
	}
    }

  openerr:
    setstate(currstate);
}

/*
 * Format a temperature (or short error code) with specified trailer
 */
static int
fmttemp(char *buf, const char *name, int tmp, const char *trailer) {
    if (tmp > OW_TEMP_BADVAL)
	return sprintf(buf, "%s: %d.%02d%s", name, GETWHOLE(tmp), GETFRAC(tmp), trailer);
    else
	return sprintf(buf, "%s: %s%s", name, OWTempStatusStr(tmp, 1), trailer);
}

/* Read the settings from SPI flash
 * If the CRC fails then reload from onboard flash
 */
static void
tempctrl_load_or_init_settings(void) {
    /* XXX: todo */
    if (!flashreadblock(0, sizeof(settings), &settings)) {
	fputs("CRC fails, loading defaults\n", stdout);
	tempctrl_default_settings();
	tempctrl_write_settings();
    }
}

/* Load in the defaults from flash */
static void
tempctrl_default_settings(void) {
    memcpy(&settings, &default_settings, sizeof(settings_t));
}

/* Write the current settings out to SPI flash */
static void
tempctrl_write_settings(void) {
    flashwriteblock(0, sizeof(settings), &settings);
}

/* Set the relays to match the desired state */
static void
setstate(char state) {
    uint8_t cool, heat;
    
    switch (state) {
	case 'c':
	    cool = 1;
	    heat = 0;
	    break;

	case 'h':
	    cool = 0;
	    heat = 1;
	    break;

	default:
	    printf("Unknown state %c, setting idle\n", state);
	    /* fallthrough */

	case 'i':
	    cool = 0;
	    heat = 0;
	    break;
    }
    
    if (cool ^ settings.coolinv)
	GPIO_SetBits(char2port(settings.coolport), 1 << settings.coolpin);
    else
	GPIO_ResetBits(char2port(settings.coolport), 1 << settings.coolpin);

    if (heat ^ settings.heatinv)
	GPIO_SetBits(char2port(settings.heatport), 1 << settings.heatpin);
    else
	GPIO_ResetBits(char2port(settings.heatport), 1 << settings.heatpin);
}

/* Handle user command
 *
 */
void
tempctrl_cmd(int argc, char **argv) {
    int16_t	data;
    uint8_t 	ROM[8];
    
    if (argc < 1) {
	printf("Unable to parse tc subcommand\n");
	return;
    }
	
    if (!strcasecmp(argv[0], "help")) {
	printf("tc help         This help\n"
	       "tc save         Save settings to EEPROM\n"
	       "tc load         Load or default settings from EEPROM\n"
	       "tc dflt         Load defaults from flash\n"
	       "tc list         List current settings\n"
	       "tc mode [achin] Change control mode, must be one of\n"
	       "                 a    Auto\n"
	       "                 c    Always cool\n"
	       "                 h    Always heat\n"
	       "                 i    Always idle\n"
	       "                 n    Like idle but don't log anything\n"
	       "tc X Y          Set X to Y where X is one of\n"
	       "                 targ Target temperature\n"
	       "                 hys  Hysteresis range\n"
	       "                 mhov Minimum heat overshoot\n"
	       "                 mcov Minimum cool overshoot\n"
	       "                 mcon Minimum cool on time\n"
	       "                 mcoff        Minimum cool off time\n"
	       "                 mhin Minimum heat on time\n"
	       "                 mhoff        Minimum heat off time\n"
	       "tc A B           Set temperature sensor ID\n"
	       "                 Where A is ferm, frg or amb\n"
	       "                 and B is of the form xx:xx:xx:xx:xx:xx:xx:xx\n"
	       "\n"
	       "                Times are in seconds\n"
	       "                Temperatures are in hundredths of degrees Celcius\n"
	    );
	return;
    }
	
    if (!strcasecmp(argv[0], "save")) {
	tempctrl_write_settings();
	return;
    }
    if (!strcasecmp(argv[0], "load")) {
	tempctrl_load_or_init_settings();
	return;
    }
    if (!strcasecmp(argv[0], "dflt")) {
	tempctrl_default_settings();
	return;
    }
    if (!strcasecmp(argv[0], "list")) {
	printf("Fermenter ROM ID %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\n"
	       "Fridge ROM ID    %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\n"
	       "Ambient ROM ID   %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x\n"
	       "Mode - %c, Target - %d, Hystersis - %d\n"
	       "Min heat overshoot - %d, Min cool overshoot - %d\n"
	       "Min cool on time - %d, Min cool off time - %d\n"
	       "Min heat on time - %d, Min heat off time - %d\n"
	       "Cool: Port %c Pin %d Inv %d, Heat: Port %c Pin %d Inv %d\n"
	       "Log format: %s\n",
	       settings.fermenter_ROM[0], settings.fermenter_ROM[1], settings.fermenter_ROM[2], settings.fermenter_ROM[3], 	
	       settings.fermenter_ROM[4], settings.fermenter_ROM[5], settings.fermenter_ROM[6], settings.fermenter_ROM[7], 	
	       settings.fridge_ROM[0], settings.fridge_ROM[1], settings.fridge_ROM[2], settings.fridge_ROM[3], 
	       settings.fridge_ROM[4], settings.fridge_ROM[5], settings.fridge_ROM[6], settings.fridge_ROM[7], 
	       settings.ambient_ROM[0], settings.ambient_ROM[1], settings.ambient_ROM[2], settings.ambient_ROM[3], 
	       settings.ambient_ROM[4], settings.ambient_ROM[5], settings.ambient_ROM[6], settings.ambient_ROM[7], 
	       settings.mode, settings.target_temp, settings.hysteresis,
	       settings.minheatovershoot, settings.mincoolovershoot,
	       settings.mincoolontime, settings.minheatontime,
	       settings.minheatontime, settings.minheatofftime,
	       settings.coolport, settings.coolpin, settings.coolinv,
	       settings.heatport, settings.heatpin, settings.heatinv,
	       settings.logfilefmt[0] == '\0' ? "none" : settings.logfilefmt
	    );
	return;
    }
    if (!strcasecmp(argv[0], "mode")) {
	if (argc < 2) {
	    fputs("Incorrect number of arguments\n", stdout);
	    return;
	}
	
	switch (argv[1][0]) {
	    case TC_MODE_AUTO:
	    case TC_MODE_HEAT:
	    case TC_MODE_COOL:
	    case TC_MODE_IDLE:
	    case TC_MODE_NOTHING:
		settings.mode = argv[1][0];
		break;
		    
	    default:
		printf("Unknown mode character '%c'\n", argv[1][0]);
		break;
	}
	return;
    }
    if (!strcasecmp(argv[0], "ferm") ||
	!strcasecmp(argv[0], "frg") ||
	!strcasecmp(argv[0], "amb")) {
	if (argc < 2) {
	    fputs("Incorrect number of arguments\n", stdout);
	    return;
	}
	
	if (sscanf(argv[1], "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
		   &ROM[0], &ROM[1], &ROM[2], &ROM[3],
		   &ROM[4], &ROM[5], &ROM[6], &ROM[7]) != 8) {
	    printf("Unable to parse ROM ID\n");
	} else {
	    if (!strcasecmp(argv[0], "ferm"))
		memcpy(&settings.fermenter_ROM, ROM, sizeof(ROM));
	    if (!strcasecmp(argv[0], "frg"))
		memcpy(&settings.fridge_ROM, ROM, sizeof(ROM));
	    if (!strcasecmp(argv[0], "amb"))
		memcpy(&settings.ambient_ROM, ROM, sizeof(ROM));
	}
	return;
    }
    if (!strcasecmp(argv[0], "log")) {
	if (argc == 1)
	    bzero(settings.logfilefmt, sizeof(settings.logfilefmt));
	else {
	    if (strlen(argv[1]) > sizeof(settings.logfilefmt) - 1)
		printf("New path too log (%d > %d)\n", strlen(argv[1]), sizeof(settings.logfilefmt));
	    else
		strcpy(settings.logfilefmt, argv[1]);
	}
	return;
    }
    
    /* Handle setting the multitude of variables
     * It's last to simplify things */
    if (argc < 3) {
	fputs("Incorrect number of arguments for variable/value\n", stdout);
	return;
    }
    
    if (sscanf(argv[2], "%hd", &data) != 1) {
	printf("Unable to parse value for variable\n");
	return;
    }

    if (!strcasecmp(argv[1], "targ")) {
	settings.target_temp = data;
    } else if (!strcasecmp(argv[1], "hys")) {
	settings.hysteresis = data;
    } else if (!strcasecmp(argv[1], "mhov")) {
	settings.minheatovershoot = data;
    } else if (!strcasecmp(argv[1], "mcov")) {
	settings.mincoolovershoot = data;
    } else if (!strcasecmp(argv[1], "mcon")) {
	settings.mincoolontime = data;
    } else if (!strcasecmp(argv[1], "mcoff")) {
	settings.mincoolofftime = data;
    } else if (!strcasecmp(argv[1], "mhon")) {
	settings.minheatontime = data;
    } else if (!strcasecmp(argv[1], "mhoff")) {
	settings.minheatofftime = data;
    } else {
	printf("Unknown setting\n");
    }
}

static const char*
state2long(char s) {
    switch (s) {
	case 'i':
	    return "idle";
	    break;
	    
	case 'c':
	    return "cool";
	    break;
	    
	case 'h':
	    return "heat";
	    break;
	    
	case '-':
	    return "-";
	    break;
	    
	default:
	    return "unknown";
	    break;
    }
}

/* Convert a port name into a number */
static GPIO_TypeDef *
char2port(char port) {
    char p;
    
    p = toupper(port);
    assert(p >= 'A' && p <= 'E');
    
    switch (p) {
	case 'A':
	    return GPIOA;
	case 'B':
	    return GPIOB;
	case 'C':
	    return GPIOC;
	case 'D':
	    return GPIOD;
	case 'E':
	    return GPIOE;
	default:
	    assert(1 == 0); /* Silence GCC warning */
    }
}