/* Hot water manager * ----------------- * This is a feature rich smart hot water management system * * More details and explanation of this project can be found at https://www.thefloatinglab.world/en/boilers.html * This project is developed and maintained at https://git.thefloatinglab.world/thefloatinglab/hotwatermanager * * License * ------- * HotWaterManager, a feature rich smart hot water management system. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * Objective * --------- * Objective was to create a hot water management system based on an Arduino architecture. * * More details and a list of features can be found at https://www.thefloatinglab.world/en/boilers.html * * Requirements * ------------ * The microcontroller should be a ATMEGA328. * The microcontroller clock speed needs to be at least 4 MHz * The power supply should be 5V, with lower voltages the voltage measurement and range degrades and a standard 5V LCD will not work. */ // Libraries #include "HotWaterManager.h" #include <avr/eeprom.h> #include <avr/wdt.h> #include <avr/sleep.h> #include <avr/boot.h> #ifdef LCDADDR #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(LCDADDR,16,2); // set the LCD address to LCDADDR for a 16 chars and 2 line display #else #include <LiquidCrystal.h> // initialize the library by associating any needed LCD interface pin // with the arduino pin number it is connected to LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7); #endif #include <OneWire.h> OneWire ds(TEMP); // Connect 1-wire devices rom_t rom = {EPROMCHK,BRIGHTNESS,DISPTIMEOUT,VREQ,VFLOAT,VOFF,PAUSE,false,TMIN,TTARGET,TSANITY,TMAX,TSANDAYS,TNOSAN, 1000, PUMPDELTA, PUMPAFTERRUN, HWVTIMER, false, HWVPWM, 0, LEDM_TOUCH, false, HOLDON, 0, NIGHTSENSOR, RELAYPWM, false}; sensors_t sensors = {EPROMCHK,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, THERMISTOR, THERMBETA, true}; // Reserve space for two DS18B20 sensors tempdata_t tempdata[] = {{0,0,1000},{0,0,-1000}}; button_t button = {BUTTON,0,false}; // Special characters for LCD uint8_t hwvchar[8] = {0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b00000}; uint8_t Anchor[8] = {0b00100,0b01010,0b00100,0b00100,0b00100,0b10101,0b01110,0b00000}; uint8_t Sailing[8] = {0b00010,0b00110,0b01111,0b01111,0b11111,0b00010,0b11111,0b01110}; uint8_t engine[8] = {0b00000,0b01111,0b00100,0b01111,0b11111,0b01111,0b00000,0b00000}; uint8_t solar[8] = {0b11111,0b11111,0b01110,0b00000,0b10101,0b10101,0b10101,0b00000}; uint8_t legion[8] = {0b01110,0b11111,0b10101,0b11111,0b11011,0b11011,0b01110,0b00000}; uint8_t down[8] = {0b00100,0b00100,0b00100,0b00100,0b10101,0b01110,0b00100,0b00000}; uint8_t up[8] = {0b00100,0b01110,0b10101,0b00100,0b00100,0b00100,0b00100,0b00000}; uint8_t lightning[8]={0b00011,0b00110,0b01100,0b11110,0b00110,0b01100,0b01000,0b10000}; uint8_t Tmr[8] = {0b00000,0b01110,0b10101,0b10111,0b10001,0b01110,0b00000,0b00000}; // Variables uint8_t mode=0; uint8_t menusel=0; boolean menuedit=false; boolean swapTsens=false; int16_t lastTemperature=0; boolean lcdon=true; boolean romdirty=false; boolean freset=false; boolean sreset=false; boolean reboot=false; boolean paused=true; boolean restartTmrReq=true; boolean hwv=false; uint32_t secs=0; uint32_t disptimer=0; uint32_t menuseltimer=0; uint32_t pauseTmr=0; uint32_t restartTmr=0; uint32_t pumpTimer=0; uint32_t HWVtmr=0; uint32_t lastlum=0; uint16_t hwv_timer=0; uint16_t rawVoltages[5]; uint16_t thermists[5]; uint8_t dummy; uint8_t bright; mma_t lum; // menu code, 8 bits #define MENU_8 B00001000 #define MENU_16 B00010000 #define MENU_SIGNED B10000000 #define MENU_FLOAT B00000011 #define MENU_BOOLEAN B01000000 #define MENU_TEMP B00000100 #define MENU_VOLTS B00100000 #define MENU_SHOW B11111111 // SFX extension code, 4 bits #define NOALT B01000000 #define FNALT B00100000 // Suffixes for some menu options const char sfx_seconds[] PROGMEM = " seconds"; const char sfx_minutes[] PROGMEM = " minutes"; const char sfx_days[] PROGMEM = " days"; const char sfx_percent[] PROGMEM = " percent"; const char sfx_volts[] PROGMEM = "V"; const char sfx_deg[] PROGMEM = "\xDF""K"; const char sfx_ohms[] PROGMEM = "\xF4"; const char* const suffixes[] = {sfx_seconds,sfx_minutes,sfx_days,sfx_percent,sfx_volts,sfx_deg,sfx_ohms}; // Build the menu table structure typedef void (*funcptr_t)(); struct menu_t { const char txt[17]; const uint8_t sfx; const uint8_t code; const int16_t min; const int16_t max; const int16_t step; funcptr_t funcptr; const void *varptr; }; const menu_t PROGMEM menu[]= { // Display, suffix, Menu flags min, max, step, function call, address of variable {"System Volts", 0, MENU_8, 0, 1, 1, &fptr_vmul, &rom.vmul}, {"Temp Units", 0, MENU_8, 0, 1, 1, &fptr_units, &rom.fahrenheit}, {"Min Temperature", 0, MENU_16+MENU_SIGNED+MENU_TEMP+1, 0, 100, 10, &fptr_dummy, &rom.tmin}, {"Target Temp", 0, MENU_16+MENU_SIGNED+MENU_TEMP+1, 340, 520, 20, &fptr_dummy, &rom.ttarget}, {"Max Temperature", 0, MENU_16+MENU_SIGNED+MENU_TEMP+1, 600, 900, 50, &fptr_dummy, &rom.tmax}, {"Sanity Temp", 0, MENU_16+MENU_SIGNED+MENU_TEMP+1, 560, 680, 20, &fptr_dummy, &rom.tsanity}, {"Sanity interval", 3, MENU_16+MENU_SIGNED, 2, 14, 1, &fptr_dummy, &rom.tsandays}, {"No Sanity below", 0, MENU_16+MENU_SIGNED+MENU_TEMP+1, 200, 300, 10, &fptr_dummy, &rom.tnosan}, {"Start Voltage", 5, MENU_16+MENU_VOLTS+1, 138, 148, 1, &fptr_dummy, &rom.vreq}, {"Float Voltage", 5, MENU_16+MENU_VOLTS+1, 125, 140, 1, &fptr_dummy, &rom.vfloat}, {"Abort Voltage", 5, MENU_16+MENU_VOLTS+1, 120, 135, 1, &fptr_dummy, &rom.voff}, {"Pause duration", 2, MENU_16+MENU_SIGNED, 5, 45, 5, &fptr_dummy, &rom.pause}, {"Hold On time", 1, MENU_8, 0, 10, 1, &fptr_dummy, &rom.holdon}, #if HWMODEL != HWV_1_0 {"LCD Brightness", 0, MENU_8, 1, 7, 1, &fptr_brightness, &rom.brightness}, #if HWMODEL > HWV_1_1 {"LCD NightBright", 0, MENU_8, 0, 7, 1, &fptr_dummy, &rom.nightbright}, {"LCD NightSensor", 0, MENU_8, 6, 16, 1, &fptr_lumsens, &rom.nightsensor}, #endif {"LCD Glow", 0, MENU_BOOLEAN, 0, 1, 1, &fptr_dummy, &rom.lcdglow}, #endif {"LCD TimeOut", 1, MENU_8, 0, 10, 1, &fptr_dummy, &rom.disptimeout}, {"LED Mode", 0, MENU_8, 0, 4, 1, &fptr_ledmode, &rom.ledmode}, {"Vlt Calibration", 0, MENU_16, 920, 1080, 1, &fptr_showvoltage, &rom.calibration}, {"Pump Source", 0, MENU_8, 0, 2, 1, &fptr_pumpsource, &rom.pumpMode}, {"Pump delta", 6, MENU_16+MENU_SIGNED+1, 5, 100, 5, &fptr_dummy, &rom.pumpdelta}, {"Pump afterrun", 1, MENU_16, 0, 300, 30, &fptr_dummy, &rom.pumpafterrun}, {"Hot Water timer", 2, MENU_16, 0, 180, 15, &fptr_dummy, &rom.hwv_timer}, {"Hot Water pwr", 4, MENU_8, 50, 100, 10, &fptr_dummy, &rom.hwv_pwm}, #if HWMODEL != HWV_1_0 {"Relay hold pwr", 4, MENU_8, 50, 100, 10, &fptr_dummy, &rom.relaypwm}, #endif {"Thermistor R", 7, MENU_16, 5000, 10000,5000, &fptr_showTemp, &sensors.thermistor}, {"Thermistor Beta", 0, MENU_16, 3000, 4200, 50, &fptr_showTemp, &sensors.thermbeta}, {"Swap temp sens", 0, MENU_BOOLEAN, 0, 1, 1, &fptr_showtemps, &swapTsens}, {"Reboot", 0, MENU_BOOLEAN, 0, 1, 1, &fptr_dummy, &reboot}, {"Factory reset", 0, MENU_BOOLEAN, 0, 1, 1, &fptr_dummy, &freset}, {"Sensors reset", 0, MENU_BOOLEAN, 0, 1, 1, &fptr_dummy, &sreset}, {"Versions", 0, MENU_SHOW, 0, 1, 1, &fptr_versions, &dummy}, #if HWMODEL != ARDUINO_ {"Serial number", 0, MENU_8, 0, 1, 1, &fptr_serialnr, &dummy}, #endif {"",0,0,0,0,0,NULL,NULL} }; // Interrupt routines EMPTY_INTERRUPT(INT0_vect); // external interrupt 0 (button press) wakes the MCU ISR(WDT_vect) { // Watchdog interrupt for the sleep timer wdt_disable(); // disable watchdog } void(* resetFunc) (void) = 0; // declare reset function at address 0 // If no bootloader is used, this code is necessary to prevent a watchdog reset repeating itself after the reboot void wdt_init(void) __attribute__((naked)) __attribute__((section(".init3"))); void wdt_init(void) { MCUSR = 0; wdt_disable(); return; } void setup() { MCUSR = 0; // Just to make sure the watchdog from the previous run... wdt_disable(); // ... does not continue to run pinMode(BUTTON,INPUT_PULLUP); pinMode(ENGINE,INPUT); pinMode(LED,OUTPUT); pinMode(HEATER,OUTPUT); pinMode(AUX,OUTPUT); pinMode(HWV,OUTPUT); #if HWMODEL != ARDUINO_ pinMode(PIN_PB6,INPUT_PULLUP); // We use the internal oscillator, prevent this pin from floating pinMode(PIN_PB7,INPUT_PULLUP); // We use the internal oscillator, prevent this pin from floating #endif #if HWMODEL > HWV_1_1 pinMode(LUMSENS,OUTPUT); // We use this ADC input to use the LED as a light sensor #endif brightness(BRIGHTNESS); // The brightness routine will define output ports as required initLCD(); // Now the brightness has been set, initialize the LCD boolean resetpressed=false; uint32_t tmr=millis(); while(millis()-tmr<3000) { // If in these 3 seconds... if(!digitalRead(BUTTON)) // the button is pressed... resetpressed=true; // record the reset request } lcd.clear(); // Read the sensor addresses eeprom_read_block((void*)&sensors, (void*)128, sizeof(sensors)); // If the reset button is pressed and we already have sensor addresses, offer the user to reset the sensor addresses as well if(resetpressed && sensors.epromchk==EPROMCHK) { lcd.print(F("Hold button to")); lcd.setCursor(0,1); lcd.print(F("reset sensors")); delay(5000); } lcd.clear(); // Button still pressed or sensor addresses never initialized? if(!digitalRead(BUTTON) || sensors.epromchk!=EPROMCHK || (!sensors.temphwa[0][0] && sensors.useDS)) { lcd.print(F("Sensors reset")); delay(2000); lcd.clear(); sensors.epromchk=EPROMCHK; sensors.useDS=true; sensors.thermistor=THERMISTOR; sensors.thermbeta=THERMBETA; uint8_t x=1; if(analogRead(TEMP)<980 && analogRead(TEMP)>200) { sensors.useDS=false; lcd.print(F("1 analog")); } else { #if HWMODEL > HWV_1_0 pinMode(OWPULLUP,OUTPUT); digitalWrite(OWPULLUP,HIGH); // Switch on the extra pullup resistor for the digital sensors #endif x=searchSensors(); lcd.print(x); lcd.print(F(" digital")); } lcd.setCursor(0,1); lcd.print(F("sensors found")); #ifndef SIMTEMP if(!x) // If no temperature sensors have been found... while(true); // continuation is pointless #endif eeprom_write_block((void*)&sensors, (void*)128, sizeof(sensors)); } if(sensors.useDS) { // Do we use Digital Sensors? #if HWMODEL > HWV_1_0 pinMode(OWPULLUP,OUTPUT); digitalWrite(OWPULLUP,HIGH); // Switch on the extra pullup resistor for the digital sensors #endif ds.reset(); // Reset 'em ds.select(sensors.temphwa[0]); // Request first temperature conversion ds.write(0x44,1); // Temperature conversion } // Prime the Voltage and thermistor median arrays for (int i = 0; i < 5; i++) { rawVoltages[i] = readvoltage(10); thermists[i] = analogRead(TEMP); delay(15 + i * 10); // Use a non constant delay to avoid any pulsing loads to sync with the sampling frequency } eeprom_read_block((void*)&rom, (void*)0, 2); // Read the ROM header if(resetpressed && rom.epromchk==EPROMCHK) { // Reset pressed, and we have an already configured EPROM? lcd.print(F("Hold button to")); // Offer user to reset the ROM lcd.setCursor(0,1); lcd.print(F("Factory Reset")); delay(5000); lcd.clear(); } if(resetpressed && !digitalRead(BUTTON) || rom.epromchk!=EPROMCHK) {// button is pressed or ROM not yet initialized rom.epromchk=EPROMCHK; if(getvoltage()>180) { // If the voltage is above 18 volts rom.vreq*=2; // Multiply all default voltages by 2 rom.vfloat*=2; rom.voff*=2; rom.vmul=true; // Set the system up for 24 Volts } eeprom_write_block((void*)&rom, (void*)0, sizeof(rom)); lcd.clear(); lcd.print(F("Reset OK")); } #ifndef USEDEFAULT eeprom_read_block((void*)&rom, (void*)0, sizeof(rom)); // These are the configuration settings. #endif brightness(rom.brightness); // Prime the "history" of the temperature. Make sure we start with a downward trend delay(1500); lastTemperature=readtemp(0)+TRENDDELTA; lcd.clear(); wdt_enable(WDTO_4S); // Enable watchdog, Use a 4 second interval } void initLCD() { digitalWrite(LED,HIGH); #ifdef LCDADDR lcd.begin(); #else lcd.begin(16, 2); #endif lcd.createChar(1, Anchor); lcd.createChar(2, Sailing); delay(100); for(int offset=0;offset<6;offset++) { lcd.setCursor(offset,0); lcd.print(" "); lcd.write((uint8_t)2); delay(400); } delay(800); lcd.setCursor(6,1); lcd.write((uint8_t)1); delay(1000); lcd.clear(); lcd.print(F(" Hot Water")); lcd.setCursor(2,1); lcd.print(F("Manager V" VERSION)); lcd.createChar(1, engine); lcd.createChar(2, solar); lcd.createChar(3, lightning); lcd.createChar(4, legion); lcd.createChar(5, down); lcd.createChar(6, up); lcd.createChar(7, Tmr); digitalWrite(LED,LOW); } // Main menu string constants const char mode0[] PROGMEM = "Automatic"; const char mode1[] PROGMEM = "Pause"; const char mode2[] PROGMEM = "WarmUp"; const char mode3[] PROGMEM = "Sanitize"; const char mode4[] PROGMEM = "Thermostat"; const char mode5[] PROGMEM = "AntiFreeze"; const char mode6[] PROGMEM = "Heater On"; const char mode7[] PROGMEM = "Pump on"; const char mode8[] PROGMEM = "Off"; const char mode9[] PROGMEM = "Setup"; const char* const modes[] = {mode0,mode1,mode2,mode3,mode4,mode5,mode6,mode7,mode8,mode9,NULL}; void modemenu() { // This function is used to traverse through the main menu if(modes[menusel]==NULL) // Reached the end? menusel=0; // Go to the first one lcd.print("> "); pgmLcdPrint(modes[menusel]); // Print the menu option from PROGMEM lcd.setCursor(0,1); // On the next line... lcd.print(F("Hold to select")); // Print this menusel++; // Advance to the next menu option } void loop() { wdt_reset(); // pat the watchdog, at least every 4 seconds, otherwise the microcontroller will reset #if HWMODEL > HWV_1_1 // Use the LED to sense ambient light, so we can dim the LCD in the dark if(rom.nightbright && rom.nightbright<rom.brightness) { // Valid configuration of rom.nightbright? if(seconds()-lastlum>1) { // Only once per 2 seconds lastlum=seconds(); boolean ledstate=digitalRead(LED); // Remember the current state of the LED analogRead(LUMSENS); // Prime the ADC (we arrive here after sleep mode, so extra long read) analogRead(LUMSENS); // Throw away the next read as well (just to be sure) pinMode(LUMSENS,OUTPUT); // Set LUMSENS to an output again digitalWrite(LUMSENS,HIGH); // Switch everything to high digitalWrite(LED,HIGH); // Switch the LED to high // See comments below why we manipulate the register manually DDRE &= ~4; // Switch the LUMSENS pin to an INPUT, and since the output was HIGH, it is now an INPUT_PULLUP uint16_t sens=analogRead(LUMSENS); // Read full voltage (should be close to 1023) pinMode(LUMSENS,OUTPUT); // Switch back to output again digitalWrite(LUMSENS,HIGH); // Reverse bias the LED digitalWrite(LED,LOW); // Reverse bias the LED // What we want to do: // pinMode(LUMSENS,INPUT_PULLUP); // Switch it to input pullup // But if pinMode first switches the output to LOW, and then switches to an input, we lost some charge from the LED. // So we do it manually, FIRST swich to an INPUT, THEN get rid of the pullup resistor. DDRE &= ~4; // Switch the LUMSENS pin to an INPUT, and since the output was HIGH, it is now an INPUT_PULLUP PORTE &= ~4; // Switch off the PULLUP resistor. delay(5); // Wait 5 milliseconds sens-=analogRead(LUMSENS); // Read the difference pinMode(LUMSENS,OUTPUT); // Switch LUMSENS back to an output again digitalWrite(LUMSENS,LOW); // Make sure it is low again digitalWrite(LED,ledstate); // Reset the led to whatever state it had if(sens>1024) // Full voltage lower than what we measured on the LED? sens=0; // Makes no sens... int16_t br=rom.nightbright+(modMovAvg(&lum,sens,LUMSAMPLES)-rom.nightsensor); if(br>rom.brightness) // If brightness is higher than what we allow br=rom.brightness; // Limit it if(br<rom.nightbright) // If brightness is lower than what we allow br=rom.nightbright; // Limit it bright=br; // convert int16 to uint8, it now falls in range if(lcdon) // Only if the LCD is on: brightness(bright); // Set the actual new brightness } else { bright=rom.brightness; // If nightbright is switched off, fall back to normal brightness } } #endif int16_t butt=chkbutton(&button); // Capture button presses switch(rom.ledmode) { // Process the LED modes case LEDM_WARMWATER: { // Do we have hot water? digitalWrite(LED,(readtemp(0) >= rom.ttarget)); } break; case LEDM_HEATING: { // Are we currently heating? digitalWrite(LED,(digitalRead(HEATER) || digitalRead(AUX))); } break; case LEDM_HWVALVE: { // Is the hot water timer running? digitalWrite(LED,hwv && hwv_timer); } break; case LEDM_LEGIONELLA: { // Is the water unsafe? digitalWrite(LED,sanitycheck()); } break; default: { // Is the button pressed? digitalWrite(LED,!digitalRead(button.button)); } } // Deal with the LCD backlight if(butt) { // Did we have a button press? disptimer=seconds(); // Restart the display timeout timer if(!lcdon) { // If the backlight was not already on, turn it on now brightness(bright); // Set the configured brightness lcdon=true; // Remember that the LCD backlight is on butt=0; // This keypress is "used up" to switch the LCD backlight on. } } if(butt) { // We still have the button press? (Could be "eaten up" by the LCD backlight timeout) menuseltimer=seconds(); // Reset the menu timeout timer if(butt==2) { // Long button press if(menusel) { // Are we already in the menu? if(menusel>=100) { // Are we in the setup menu? menuedit=!menuedit; // Long press is either exit or entry of a setup function lcd.clear(); // Show the associated menu option printMenu(menusel-100,menuedit); if(!menuedit && romdirty) { // We are not in edit mode but made the rom "dirty"? if(freset) rom.epromchk=0; if(sreset) sensors.epromchk=0; #ifndef SIMTEMP if(swapTsens) { // If we need to swap the sensors, do it now... for(uint8_t i=0;i<8;i++) { uint8_t x=sensors.temphwa[0][i]; sensors.temphwa[0][i]=sensors.temphwa[1][i]; sensors.temphwa[1][i]=x; } swapTsens=false; } #endif if(reboot) // Only a reboot? resetFunc(); // Don't write EEPROM, just reboot. // Save the changed entries in the ROM eeprom_write_block((void*)&rom, (void*)0, sizeof(rom)); eeprom_write_block((void*)&sensors, (void*)128, sizeof(sensors)); if(freset || sreset) // Reset rom or sensors? resetFunc(); // Follow it with a reset } } else { // We are not in the setup menu if(modes[menusel]==NULL) { // We reached the setup menu menusel=99; // Select it butt=1; // Simulate short press to enter it } else { // It is a long press on a menu option paused=false; mode=menusel-1; // so we selected this one menusel=0; // Exit the menu lcd.clear(); } } } else { // Not yet in the menu lcd.clear(); modemenu(); // So start it now } } if(butt==1) { // Short press lcd.clear(); // Are we editing a setup menu item? if(menuedit) { // Are we in edit mode? editItem(menusel-100); // Edit the item printMenu(menusel-100,menuedit); } else { // Not in edit mode if(menusel>=99) { // Are we in the setup menu? nextMenuItem(); // Advance to the next menu item } else { // Not in the setup menu if(!menusel && rom.hwv_timer) { // Have we configured a hot water valve timer? // Toggle the hot water valve timer setHWV(!(hwv && hwv_timer>HWVBLEED),rom.hwv_timer*60); } else { // No HWV... modemenu(); // ...just go into the main menu } } } } } else { // No button was pressed if(menusel>=100 && menuedit && getPgmVal(&menu[menusel-100].code)==MENU_SHOW) { lcd.setCursor(0,0); printMenu(menusel-100,menuedit); // This menu option has no timeout } else { if(menusel && seconds()-menuseltimer>(MENUTIMEOUT)) { menusel=0; // Menu timed out, get out of it menuedit=false; // Exit edit mode romdirty=false; // If we were in edit mode, discard the change lcd.clear(); } } if(!menusel) { // If we are not in a menu... processmodes(); // ... Process the mode } } if(!menusel && !butt && !button.pressed && !hwv && !digitalRead(HEATER)) { // We are not in a menu, no button is pressed, and we have no hot water valve timer going... // Let's get some sleep! // We are going to use the Watchdog timer for this. // We will shut down everything else. // The sleep will end when either the watchdog timer or an interrupt (button press) fires. uint8_t wdt_prescale = 6; // Sleep for 1 second uint8_t wdtcsr = bit(WDIE) | (wdt_prescale&7) | ((wdt_prescale&8)<<2); wdt_disable(); // Disable the watchdog timer for now. uint8_t adcsra=ADCSRA; // Store current state of ADCSRA ADCSRA = 0; // disable the ADC MCUSR = 0; // clear various "reset" flags WDTCSR = bit (WDCE) | bit (WDE); // allow changes, disable reset WDTCSR = wdtcsr; // set WDIE, and prescaler wdt_reset(); // pat the dog EICRA = _BV(ISC01); // configure INT0 to trigger on falling edge EIMSK = _BV(INT0); // enable INT0, so we wake up immediately if the button is pressed set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Power down everything sleep_enable(); sei(); // ensure interrupts enabled so we can wake up again sleep_cpu(); // go to sleep. Here we sleep // Zzzzzzz sleep_disable(); // We woke up, don't fall asleep again, there is work to do ADCSRA=adcsra; // Restore ADCSRA wdt_enable(WDTO_4S); // Re-enable the watch dog secs++; // The timer stopped during sleep, make good for it with the one second we slept // _millis+=(16<<wdt_prescale); // Add the time spent sleeping to our timer (use this for sleep periods other than 1 second) } } void processmodes() { // This is the function that controls the hot water management uint16_t voltage=getvoltage(); static uint16_t LCDvoltage; static uint32_t vfreq; // Did the display backlight timeout? if(lcdon && rom.disptimeout && seconds()-disptimer>rom.disptimeout) { lcdon=false; brightness(0); // Switch off the back light } // Change the value displayed on the LCD only once per second if(!LCDvoltage || seconds()-vfreq>1) { LCDvoltage=voltage; vfreq=seconds(); } lcd.setCursor(0,0); // If the alternate temp sensor has a higher value than the tank sensor, show both of them alternately. uint8_t src=0; if(rom.pumpMode && readtemp(1)-readtemp(0)>rom.pumpdelta && (seconds() & 2)) { lcd.print(F("Source")); src=1; } else { lcd.print(F("Tank ")); } printTemperature(readtemp(src),1); // Display the temperature lcd.setCursor(0,1); pgmLcdPrint(modes[mode]); // Print the current operation mode lcd.setCursor(11,1); lcd.print(((float)LCDvoltage)/10.0,1); // Display the voltage lcd.print("V "); // Update the temperature trend arrow lcd.setCursor(13,0); if(tempdata[src].failures) // If we failed to receive valid data from the sensor... lcd.write('?'); // ... indicate this with a question mark... else { #ifdef SIMTEMP lcd.write(6); // Simulate upward trend #else if(readtemp(0)-lastTemperature>=TRENDDELTA) { lcd.write(6); // Trend is going up lastTemperature=readtemp(0); } else if(lastTemperature-readtemp(0)>=TRENDDELTA) { lcd.write(5); // Trend is going down lastTemperature=readtemp(0); } } #endif lcd.setCursor(14,0); if(hwv && hwv_timer) { // If the hot water valve is selected... for(uint8_t i=0; i < 7 ;i++) { // build the character if(i<6 && ((seconds()-HWVtmr)*7)/hwv_timer > i) hwvchar[i] = 0b10001; else hwvchar[i] = 0b11111; } lcd.createChar(0, hwvchar); // create the character lcd.setCursor(14,0); // needed after createChar lcd.write((uint8_t)0); // Display the character } else if(sanitycheck()) lcd.write(4); // Display the legionella warning else // Or... lcd.write(' '); // ... clear it lcd.setCursor(15,0); if(!mode && digitalRead(ENGINE)) // AUtomatic mode and engine contact is on? lcd.write(1); // Show the engine symbol else if(digitalRead(AUX)) // Is the auxilliary pump running? lcd.write(3 - (int8_t) rom.pumpMode); // Show the respective symbol else if(paused) // If we are pausing... lcd.write(7); // ... show it on the screen else if(digitalRead(HEATER)) // If the heating element is energized lcd.write(3); // Show the symbol on the display else // Or... lcd.write(' '); // ... clear it when the heating element is de-energized. if(hwv && seconds()-HWVtmr > hwv_timer) // If the HWV is enabled and expired... setHWV(false,0); // ... switch it off if(voltage<rom.vfloat) // If the voltage has become below the float voltage... restartTmrReq=true; // ... we need to restart the timer if(readtemp(0)>rom.tmax) { // If Temp is higher than max... heaterState(false); // Switch off the heating element pumpState(LOW); // Switch off the auxilliary pump return; // Nothing else needs to be done, we're finished } if(voltage<rom.voff) { // If voltage below the lower threshold, pause whatever program is running heaterState(false); pumpState(LOW); // The pump also consumes power, so let's keep it off until the voltage goes up. paused=true; pauseTmr=0; } if(paused || mode==1) { // If Paused, keep boiler off until the timer expires if(!paused) { // Was the mode set manually (mode==1) to pause? pauseTmr=0; // Then we actually need to start the pause timer. paused=true; } if(voltage>=rom.vreq) { // Voltage reached the required value if(!pauseTmr) { // Did we already start the countdown? pauseTmr=seconds(); // If not, start it now } else { if(seconds()-pauseTmr>rom.pause*60L) { // Has the timer already expired? paused=false; // Cancel pause mode if(mode==1) // If the pause mode was manually selected... mode=0; // ... reset it to automatic mode } else { // Nope, we still need to pause heaterState(false); // Make sure the water heater is off // Do not return, maybe solar can still input some heat } } } } // We've handled all hard limits, now look at the modes if(mode==8) { // If mode is OFF... heaterState(false); // - Shut down the water heater pumpState(LOW); // - Shut down also the auxilliary pump return; // - No further actions required. } if(mode==7) { // Is mode is PUMP pumpState(HIGH); // Run the pump return; // Do nothing else } // If we have an auxilliary pump, if(rom.pumpMode) { // Do we have an auxilliary pump? if(readtemp(1)-readtemp(0)>rom.pumpdelta) { // Is there a worthwile temperature difference between the source and hot water tank? pumpState(HIGH); // Then start the pump heaterState(false); // And disable the heating element pumpTimer=seconds(); // Record the time if(mode!=6) // Unless the mode is "heater on"... return; // We are warming the boiler, as long as we can do that no electricity is needed, skip the rest. } else { // Not enough temperature difference if(seconds()-pumpTimer>rom.pumpafterrun) // Unless the "aterrun" timer is still running pumpState(LOW); // Switch off the pump } } if(mode==6) { // If program=ON... heaterState(true); // ... just keep the heater on. Overtemp and pause is already dealt with above. return; // Nothing else needs to be done } if(mode==2) { // If the program is in WarmUp mode... if(readtemp(0)<rom.ttarget) { // and temperature below the target heaterState(true); // Keep the heater on return; // Finished here } else { // Temperature reached the target? mode=0; // Then revert to automatic mode } } if(mode==3) { // If the program is in Sanitize mode... if(readtemp(0)<rom.tsanity) { // and temperature below the target heaterState(true); // Keep the heater on return; // Finished here } else { // Temperature reached the target? mode=0; // Then revert to automatic mode } } if(mode==4) { // If the mode is "thermostat" if(readtemp(0)<rom.ttarget) { // If the temperature is below the target heaterState(true); // Keep the heater on } else { // Otherwise heaterState(sanitycheck()); // Use the heater when sanitize is required, otherwise switch it off } return; // We're finished } // If we arrive here, we are either running in auto or antifreeze mode. if(paused) { // In pause mode heaterState(false); // Keep the heater off return; } if(readtemp(0)<rom.tmin) { // Hot water tank about to freeze? heaterState(true); // Turn the heating element on return; // Finished } if(mode==5) { // In antifreeze mode, if we made it until here, the water is warm enough heaterState(false); // So switch off the heater return; // We're finished } // If we make it until here, we are in automatic mode if(digitalRead(ENGINE)) { // Engine contact is on? heaterState(false); // Don't use the heater return; } // Is the boiler off? Maybe we can switch it on? if(!digitalRead(HEATER)) { if(voltage<rom.vreq) // If voltage too low... return; // ... Forget about it // The voltage is high enough, so check to see if we need to reset the timer if(restartTmrReq) { restartTmrReq=false; restartTmr=seconds(); } uint32_t holdoff=1; // Did we already reach the target temperature, then we can be a bit more conservative with energy consumption if(readtemp(0)>rom.ttarget) { if(sanitycheck()) { holdoff=2; // We are due to run a sanity cycle, so let's try to reach it with moderate conservatism } else { holdoff=5; // Sanity cycle is not due, so let's not try too hard to get the temperature up to the sanity level } } if(readtemp(0)>rom.tsanity) // Did we reach the sanity temperature? holdoff=10; // We just heat the water further but only if we are otherwise waisting available energy. if(seconds()-restartTmr>holdoff*60L) // Now we need to see how long ago we reached (again) VREQ. Can we already start? heaterState(true); return; } // If we arrive here, the boiler is on. Do we want to keep it that way? if(voltage<rom.vfloat) { // If boiler is on but we get under the float voltage, switch it off heaterState(false); return; } } void heaterState(boolean state) { static uint32_t lastChange=0; // Take care of the relay PWM if it has been configured if(state && rom.relaypwm<100 && digitalRead(HEATER) && seconds()-lastChange>2) analogWrite12V(HEATER,rom.relaypwm); // If we want to change state, do so only after at least some time has passed since the last change if((!state && seconds()-lastChange>rom.holdon) || (state && seconds()-lastChange>HOLD_OFF)) { if(state) // If we switch the heater on... pumpState(LOW); // Switch off the auxilliary pump if(digitalRead(HEATER)!=state) { // Need to change the heater state? lastChange=seconds(); delay(20); if(state) analogWrite12V(HEATER,100); // Set heater output to 100% else analogWrite12V(HEATER,0); // Set heater output to 0% // digitalWrite(HEATER,state); // Switch the heater if(rom.hwv_timer && !hwv && state) // If we have a HWV and it is not on, and we switch on the heater... setHWV(true,HWVBLEED); // ... switch on the HWV to equalize the pressure } } } void pumpState(boolean state) { digitalWrite(AUX,state); } void setHWV(bool state, uint16_t hwvtmr) { if(state) { // We want to switch on the Hot Water Valve? HWVtmr=seconds(); // Record the time hwv_timer=hwvtmr; // Set the duration } hwv=state; // Register the new state if(state) // We want to switch it on analogWrite12V(HWV,rom.hwv_pwm); // Use PWM for this else // Otherwise digitalWrite(HWV,LOW); // Switch it off } void analogWrite12V(uint8_t port,uint8_t percent) { if(percent>=100) { digitalWrite(port,HIGH); // Set port to high so we can read back port state if(rom.vmul) // If 24V analogWrite(port,127); // Set an initial value of 50%. } else { uint16_t val = ((uint16_t)percent*256)/100; if(rom.vmul) // In case of 24V val/=2; // Divide PWM by 2 analogWrite(port,val); } if(!percent) // If we want to switch it completely off digitalWrite(port,LOW); // Do a digitalWrite } // This routine will keep track whether there is a legionella chance boolean sanitycheck() { static boolean sancounting=false; static boolean keepalarm=false; // Prevent the alarm switching off after timer rollover static uint32_t santotal=0; static uint32_t sancount=0x80000000; if(readtemp(0)>=rom.tsanity) { // The temperature in boiler is high enough to kill legionella, so clear all alarms. santotal=0; sancounting=false; keepalarm=false; } else { if(readtemp(0)>rom.tnosan) { if(!sancounting) { sancounting=true; sancount=seconds(); } if(santotal+(seconds()-sancount)>((uint32_t)rom.tsandays)*60L*60L*24L) { keepalarm=true; } } else { if(sancounting) { sancounting=false; santotal+=(seconds()-sancount); } } } return keepalarm; } void brightness(uint8_t brightness) { // Controlling the brightness of the LCD backlight could be done with a single PWM output, but then we couldn't use the sleep function of the processor anymore. // We have enough IO ports anyway, so we use three outputs with different resistors with which we can make 8 different brightness values. // Setup an array with the output ports in binary order const uint8_t blports[]={BACKLIGHT_B0, BACKLIGHT_B1, BACKLIGHT_B2}; if(brightness) bright=brightness; // Remember the new brightness uint8_t inputmode=INPUT; // If a port is not used, set it as input if(!brightness && rom.lcdglow) // but if the brightness is 0 and we selected "glow".... inputmode=INPUT_PULLUP; // ...make it input_pullup to allow some leak current through the LCD backlight for(uint8_t i=0; i<3; i++) { // Process all three bits if(brightness & (1<<i)) { // Isolate the bit we're looking for. If this bit is on... pinMode(blports[i],OUTPUT); // ... select it as an output digitalWrite(blports[i],HIGH); // ... and pull it high } else // otherwise pinMode(blports[i],inputmode); // select it as an input to neutralize it (or put it into glow mode) } } // Function Pointer routines for some special setup menu processing. The addresses of these functions are referred to in the menu table void fptr_dummy(void) { } void fptr_brightness() { brightness(rom.brightness); } void fptr_showvoltage() { lcd.setCursor(11,1); lcd.print((float)readvoltage(10)/10.0,1); // We use readvoltage here because getvoltage only reads once per second and is too sluggish lcd.write('V'); getvoltage(); // Update the voltage in the median array, so when we exit the menu a matching voltage is shown right away instead of the old value } void fptr_showTemp() { lcd.setCursor(9,1); tempdata[0].mmaTemp.samples=0; tempdata[0].mmaTemp.sum=0; printTemperature(readtemp(0),1); } void fptr_pumpsource() { switch(rom.pumpMode) { case 2: { lcd.print(F("Solar")); } break; case 1: { lcd.print(F("Engine")); } break; default: { lcd.print(F("None")); } } } void fptr_showtemps() { lcd.setCursor(5,1); if(sensors.useDS && sensors.temphwa[1][0]) { printTemperature(readtemp(0),0); lcd.print(" "); printTemperature(readtemp(1),0); } else { lcd.print("N/A"); swapTsens=false; } } void fptr_vmul() { if(rom.vmul) { // If 24V range is selected lcd.print(24); if(rom.vreq<180) { // Multiply all configurable voltages by 2 if the configured voltage is below 18 volts rom.vreq*=2; rom.vfloat*=2; rom.voff*=2; } } else { // If 12V range is selected lcd.print(12); if(rom.vreq>180) { // Divide all configurable voltages by 2 if the configured voltage is above 18 volts rom.vreq/=2; rom.vfloat/=2; rom.voff/=2; } } lcd.print(F("Volt")); } void fptr_units() { if(rom.fahrenheit) lcd.print(F("Fahrenheit")); else lcd.print(F("Celsius")); } void fptr_ledmode() { static const char lmod0[] PROGMEM = "Touch"; static const char lmod1[] PROGMEM = "Warm water"; static const char lmod2[] PROGMEM = "Heating up"; static const char lmod3[] PROGMEM = "Solenoid"; static const char lmod4[] PROGMEM = "Legionella"; static const char* const ledmods[] = {lmod0,lmod1,lmod2,lmod3,lmod4}; lcd.setCursor(4,1); pgmLcdPrint(ledmods[rom.ledmode]); } void fptr_versions() { lcd.print(F("SW " VERSION " HW " HWVER )); } #if HWMODEL != ARDUINO_ void fptr_lumsens() { lcd.setCursor(8,1); lcd.print(lum.avg); // Show the current sensor output lcd.print(" "); } void fptr_serialnr() { char signature[10]; char base64str[17]; lcd.setCursor(0,1); for(uint8_t i=0;i<10;i++) signature[i]=boot_signature_byte_get(i+14); base64_encode(base64str, signature, 10); lcd.print(base64str); } #endif void printMenu(uint8_t item, boolean edit) { uint8_t code = getPgmVal(&menu[item].code); uint16_t value = getRomValue(code,item); if(!edit) lcd.write('>'); if(code==MENU_SHOW) { if(edit) lcd.print(F("Hold to exit")); else pgmLcdPrint(menu[item].txt); lcd.setCursor(2,1); } else { pgmLcdPrint(menu[item].txt); lcd.setCursor(0,1); if(edit) lcd.print("> "); else lcd.print(" "); if(code & (MENU_8 | MENU_16)) { if(code & MENU_FLOAT) if(code & MENU_TEMP) printTemperature(value,0); else lcd.print((float) ((float)value/pow(10.0,(float)(code & MENU_FLOAT))), code & MENU_FLOAT); else if(code & MENU_SIGNED) lcd.print((int)value); else lcd.print(value); } uint8_t i=pgm_read_byte(&menu[item].sfx) & 0x0F; if(i) pgmLcdPrint(suffixes[i-1]); if(code & MENU_BOOLEAN) if(value) lcd.print("Yes"); else lcd.print("No "); } // If the menu has a function pointer, process it now lcd.setCursor(2,1); void (*function)(void); // function buffer function = (funcptr_t)pgm_read_ptr(&menu[item].funcptr);// get it from PROGMEM function(); // Call the function } uint16_t getRomValue(uint8_t code, uint8_t item) { if(code & (MENU_8 | MENU_BOOLEAN)) { if(code & MENU_SIGNED) return (uint16_t)((int16_t)*((int8_t *)pgm_read_word(&menu[item].varptr))); else return (uint16_t)*((uint8_t *)pgm_read_word(&menu[item].varptr)); } if(code & MENU_16) return (uint16_t)*((uint16_t *)pgm_read_word(&menu[item].varptr)); } void editItem(uint8_t item) { uint8_t code = getPgmVal(&menu[item].code); if(code==MENU_SHOW) return; int16_t value = getRomValue(code,item); int16_t step=getPgmVal(&menu[item].step); int16_t max=getPgmVal(&menu[item].max); int16_t min=getPgmVal(&menu[item].min); romdirty=true; // Remember to save changes in EEPROM if(code & MENU_VOLTS && rom.vmul) { // If some voltage menu option is selected and we operate on a 24V system... step*=2; max*=2; min*=2; // Multiply all voltage steps, minimums and maximums by two. } value+=step; value-=value%step; if(value>max || value<min) value=min; if(code & (MENU_8 | MENU_BOOLEAN)) { if(code & MENU_SIGNED) *((int8_t *)pgm_read_word(&menu[item].varptr)) = (int8_t)value; else *((uint8_t *)pgm_read_word(&menu[item].varptr)) = (uint8_t)value; } if(code & MENU_16) if(code & MENU_SIGNED) *((int16_t *)pgm_read_word(&menu[item].varptr)) = value; else *((uint16_t *)pgm_read_word(&menu[item].varptr)) = value; } void nextMenuItem() { uint8_t fn; do { menusel++; // Advance to the next item if(getPgmVal(menu[menusel-100].txt)=='\0') menusel=100; // Reached the last one, rewind to the first one fn=getPgmVal(&menu[menusel-100].sfx); } while((rom.altfunc && ( fn & NOALT)) || (!rom.altfunc && (fn & FNALT)) ); printMenu(menusel-100,menuedit); } uint16_t readvoltage(uint32_t mag) { #ifdef SIMVOLT return SIMVOLT; #endif analogRead(VCCREF); return (uint16_t)(((((((((uint32_t)analogRead(VCCREF))*(R1+R2))/R2)*ADCREF)+(rom.calibration/2))/(uint32_t)rom.calibration)+(mag*5))/(mag*10)); } uint16_t getvoltage() { static uint8_t index=0; rawVoltages[index++]=readvoltage(10); index%=5; return medianp(rawVoltages); } int16_t readtemp(uint8_t sensor) { uint8_t data[9]; static uint8_t index=0; static uint32_t temptimer=0; static uint8_t currsensor=0; #ifdef SIMTEMP return SIMTEMP; #endif if(sensors.useDS) { if(!sensors.temphwa[sensor][0]) // No hardware address for this sensor slot? return tempdata[sensor].fallback; // Return a "safe" but indicative value. } else { analogRead(TEMP); thermists[index++]=analogRead(TEMP); // Always read the thermistor value and put it in the median array. index%=5; // The array can only hold 5 values } if(seconds()-temptimer<1) // Do we need a new temperature now? return tempdata[sensor].temperature; // No, use the old one temptimer=seconds(); if(sensors.useDS) { if(sensors.temphwa[currsensor][0]) { ds.reset(); ds.select(sensors.temphwa[currsensor]); // Select the current sensor ds.write(0xBE); // Issue Read scratchpad command for(byte i = 0; i < 9; i++) // Receive 9 bytes data[i] = ds.read(); if(OneWire::crc8(data,8)==data[8]) { // Check what we've got. Is the data valid? int16_t t = (int16_t)((data[1] << 8) + data[0] ) * 10 / 16; if(t>1100 || t<-350) tempdata[currsensor].failures++; else { tempdata[currsensor].temperature = t; tempdata[currsensor].failures=0; } } else { tempdata[currsensor].failures++; } } // Setup the sensors for measuring the temperature, so we have new data ready at the next reading attempt ds.reset(); currsensor=1-currsensor; ds.select(sensors.temphwa[currsensor]); ds.write(0x44,1); // Temperature conversion } else { float ratio = ((float)medianp(thermists))/1024.0; float R = TEMPR * ratio / (1.0-ratio); // Calculate thermistor resistor value tempdata[0].temperature = modMovAvg(&tempdata[0].mmaTemp,(int16_t)round(10.0* ((1.0 / ((log(R/sensors.thermistor) / sensors.thermbeta) + (1.0 / 298.15)))-273.15)),TEMPSAMPLES); } if(tempdata[sensor].failures>10) // Too many failures, start returning a "safe" but unusual value. tempdata[sensor].temperature = tempdata[sensor].fallback; return tempdata[sensor].temperature; } uint8_t searchSensors() { uint8_t owaddr[8]; uint8_t sensor=0; sensors.temphwa[0][0]=0; sensors.temphwa[1][0]=0; ds.reset(); while(ds.search(owaddr) && sensor<2) { // We have found a one-wire device. Is it a temperature sensor and is the data valid? if(owaddr[0]==0x28 && OneWire::crc8( owaddr, 7) == owaddr[7]) { // Seems to be ok. So copy this hardware address for(uint8_t i=0;i<8;i++) sensors.temphwa[sensor][i]=owaddr[i]; // Now we have it, configure the sensor ds.reset(); ds.select(sensors.temphwa[sensor]); ds.write(0x4E); // Write scratchpad command ds.write(0); // TL data ds.write(0); // TH data ds.write(0x7F); // Configuration Register (resolution) 7F=12bits 5F=11bits 3F=10bits 1F=9bits ds.reset(); // This "reset" sequence is mandatory, it allows the DS18B20 to understand the copy scratchpad to EEPROM command ds.select(sensors.temphwa[sensor]); ds.write(0x48); // Copy Scratchpad command sensor++; ds.reset(); } } return sensor; } int16_t getPgmVal(const int16_t *ptr) { return pgm_read_word(ptr); } uint16_t getPgmVal(const uint16_t *ptr) { return pgm_read_word(ptr); } char getPgmVal(const char *ptr) { return pgm_read_byte(ptr); } byte getPgmVal(const byte *ptr) { return pgm_read_byte(ptr); } // Prints a string from flash memory const char* pgmLcdPrint(const char *txt) { char c=pgm_read_byte_near(txt); while(c) { lcd.write(c); txt++; c=pgm_read_byte_near(txt); } txt++; return txt; } void printTemperature(int16_t value,uint8_t precision) { if(rom.fahrenheit) { lcd.print(((float)value*0.18)+32.0,precision); lcd.print("\xDF""F"); if(precision && (float)value*0.18+32.0<100) lcd.write(' '); } else { lcd.print((float)value/10.0,precision); lcd.print("\xDF""C"); if(precision) { if(value<100 && value>0) lcd.write(' '); if(value>-100) lcd.write(' '); } } } uint32_t seconds() { static uint32_t prevmillis=0; uint32_t dsecs=(millis()-prevmillis)/1000L; if(dsecs) { secs+=dsecs; prevmillis+=dsecs*1000L; } return secs; } // This function handles the button functionality. Button presses are separated into long, short and double clicks presses. int chkbutton(struct button_t* button) { if(digitalRead(button->button)==LOW) { // button pressed if(button->pressed==0) button->pressed=millis(); if(millis()-button->pressed>=LONGPRESS) { if(!button->longpressed) { button->longpressed=true; return 2; } } return 0; } else { // button released if(button->longpressed) { button->longpressed=false; button->pressed=0; } if(button->pressed==0) return 0; unsigned long pressed=millis()-button->pressed; button->pressed=0; #ifdef SHORTPRESS if(pressed<SHORTPRESS) return 0; #endif if(pressed<LONGPRESS) { return 1; } return 0; } } // ******** Modified Moving Average filter ********* int16_t modMovAvg(mma_t* mma, const int16_t val, const uint16_t maxSamples) { if(mma->samples<maxSamples) mma->samples++; // This is going to be an additional sample else mma->sum-=mma->avg; // Max amount of samples reached, substract average to make room for new value mma->sum+=val; mma->avg=(int16_t)((mma->sum+(mma->samples>>1))/mma->samples); return mma->avg; } // ******** Routine to find the median in an array of 5 values ********* // We use this to reject voltage spikes and erroneous temperature readings, rather than averaging them. // Trick using XOR to swap two variables #define swap(a,b) a ^= b; b ^= a; a ^= b; #define sort(a,b) if(a>b){ swap(a,b); } uint16_t median(uint16_t a, uint16_t b, uint16_t c, uint16_t d, uint16_t e) { sort(a, b); sort(d, e); sort(a, c); sort(b, c); sort(a, d); sort(c, d); sort(b, e); sort(b, c); return c; } uint16_t medianp(uint16_t values[5]) { return median(values[0], values[1], values[2], values[3], values[4]); } #if HWMODEL != ARDUINO_ // ********* These routine are necessary to represent the serial number in Base64 ************ // In the commercial version we like to keep track of serial numbers. // In the Arduino versions we don't need that. // Routines to encode to base64 unsigned char binary_to_base64(unsigned char v) { if(v < 26) return v + 'A'; // Capital letters - 'A' is ascii 65 and base64 0 if(v < 52) return v + 71; // Lowercase letters - 'a' is ascii 97 and base64 26 if(v < 62) return v - 4; // Digits - '0' is ascii 48 and base64 52 if(v == 62) return '+'; // Special case for value 62 if(v == 63) return '/'; // Special case for value 63 return 64; } void a3_to_a4(uint8_t *a4, uint8_t *a3) { a4[0] = (a3[0] & 0xfc) >> 2; a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); a4[3] = (a3[2] & 0x3f); } int8_t base64_encode(char *output, char *input, int8_t inputLen) { int8_t i = 0, j = 0; int8_t encLen = 0; uint8_t a3[3]; uint8_t a4[4]; while(inputLen--) { a3[i++] = *(input++); if(i == 3) { a3_to_a4(a4, a3); for(i = 0; i < 4; i++) output[encLen++] = binary_to_base64(a4[i]); i = 0; } } if(i) { for(j = i; j < 3; j++) a3[j] = '\0'; a3_to_a4(a4, a3); for(j = 0; j < i + 1; j++) output[encLen++] = binary_to_base64(a4[j]); while((i++ < 3)) output[encLen++] = '='; } output[encLen] = '\0'; return encLen; } #endif