/* 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