/* * coffee.cpp * * Created on: Sep 25, 2017 * Author: sebastian */ #include #include #include #include #include #include #include #include #include #include #include #include #include "coffee.h" #include "hal.h" #include "logger.h" #include "timer.h" #include "database.h" coffee_status_t state; coffee_menuPage_t page; coffee_mode_t mode; int sigValue; int menuTimeout; uint16_t brewTime; //Brew time in ms uint16_t lastBrewTime; timer brewTimer(&brewTimeHandler); timer menuTimer(&menuTimeHandler); uint64_t totalHeatingTime; //local copies of the corresponding database entries uint16_t brewCounter; uint16_t currentCleanCycle; bool initalHeating; bool descaling; //flag to indicate descaling and cleaning uint16_t descBrewcount; time_t descRawTimestamp; const char* PageName[] = { "SoftOff", "Kill", "Stats", "Stats2", "Descaling", "Temp", "Clean", "Restart", "Exit" }; const char* StateName[] = { "OFF", "HEATING", "INITHEAT", "IDLE", "BREW", "BREWMAN", "CLEAN", "ERROR", "WAITOFF" }; /** * Thread for the finite state machine * It represents the current state of the machine and handles signals coming from * the pressure control, buttons, the brew switch and the proximity sensor * @param threadid the ID of the thread */ void *coffeeThread(void *threadid) { logger(V_BASIC, "Initializing coffee thread...\n"); //installing new Signal handler for coffeethread struct sigaction action; sigemptyset(&action.sa_mask); action.sa_flags = SA_SIGINFO; action.sa_sigaction = coffeeHandler; sigaction(SIGUSR2, &action, NULL); brewTimer.setDivider(4); brewTimer.stop(); brewTime = 0; currentCleanCycle = 0; menuTimer.setDivider(4); menuTimer.stop(); menuTimeout = 0; initalHeating = true; mode = MODE_STATE; //Unless we enter the menu we start in state mode page = PAGE_SOFTOFF; descaling = false; //read the database values if (!(totalHeatingTime = sqlGetConf(CFGHeatingTime))) { logger_error("coffee.cpp: Couldn't read the heating time from the database\n"); //pthread_exit(EXIT_SUCCESS); exit(EXIT_FAILURE); } if (!(brewCounter = sqlGetConf(CFGbrewcounter))) { logger_error("coffee.cpp: Couldn't read the brew counter from the database\n"); //pthread_exit(EXIT_SUCCESS); exit(EXIT_FAILURE); } if (!(descRawTimestamp = (time_t) sqlGetConf(CFGDescTimestamp))) { logger_error("coffee.cpp: Couldn't read the descaling time from the database\n"); //pthread_exit(EXIT_SUCCESS); exit(EXIT_FAILURE); } if (!(descBrewcount = sqlGetConf(CFGDescBrewCount))) { logger_error("coffee.cpp: Couldn't read the descaling brewcount time from the database\n"); //pthread_exit(EXIT_SUCCESS); exit(EXIT_FAILURE); } checkDescaling(); event_subscribe("terminate", &coffeeTerminate); logger(V_BREW, "Determining inital state\n"); //determine inital state if (halGetRelaisState(RELAIS_POWER) && halGetRelaisState(RELAIS_HEAT) && !halGetRelaisState(RELAIS_PUMP)) { //wait for heat relais to switch coffeeNap(1,0); if (halIsHeating()) { //Heating is on changeState(STATE_INITALHEATING); } else { initalHeating = false; changeState(STATE_IDLE); } } else if (halGetRelaisState(RELAIS_PUMP)) { logger_error("Whoops, why is the pump running...\n"); changeState(STATE_ERROR); } else { changeState(STATE_OFF); } logger(V_BREW, "Starting Coffee FSM\n"); //begin FSM while (1) { /* * Menue FSM */ switch (page) { case PAGE_NULL: break; case PAGE_SOFTOFF: //this page is only available when the machine is on if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigInt0Rls: if(halIsHeating()){ changeState(STATE_WAIT_OFF); } else { changeState(STATE_OFF); } leaveMenu(); break; case SigRotCW: changePage(PAGE_KILL); break; case SigRotCCW: changePage(PAGE_EXIT); break; } break; case PAGE_KILL: //this page is only available when the machine is on if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigInt0Rls: if (halIsHeating()) { coffeeIncreaseHeatingTime(halgetHeatingTime()); } changeState(STATE_OFF); leaveMenu(); break; case SigRotCW: if (state == STATE_IDLE || state == STATE_HEATING) { changePage(PAGE_CLEAN); } else { changePage(PAGE_RESTART); } break; case SigRotCCW: changePage(PAGE_SOFTOFF); break; } break; case PAGE_CLEAN: //this page is only be available when the machine is hot if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigInt0Rls: changeMode(MODE_STATE); if (!halProxSensorCovered()) { changeState(STATE_CLEANING); leaveMenu(); } else { changeState(STATE_FULLTANK); leaveMenu(); } break; case SigRotCW: changePage(PAGE_RESTART); break; case SigRotCCW: changePage(PAGE_KILL); break; } break; case PAGE_RESTART: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigInt0Rls: event_trigger("terminate"); coffeeNap(4, 0); exit(EXIT_SUCCESS); break; case SigRotCW: changePage(PAGE_TEMP); break; case SigRotCCW: if(state == STATE_IDLE || state == STATE_HEATING){ changePage(PAGE_CLEAN); } else if (state == STATE_ERROR || state == STATE_INITALHEATING) { changePage(PAGE_KILL); } else { changePage(PAGE_EXIT); } break; } break; case PAGE_TEMP: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigRotCW: changePage(PAGE_STATS); break; case SigRotCCW: changePage(PAGE_RESTART); break; } break; case PAGE_STATS: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigRotCW: changePage(PAGE_STATS2); break; case SigRotCCW: changePage(PAGE_TEMP); break; } break; case PAGE_STATS2: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigRotCW: changePage(PAGE_DESCALING); break; case SigRotCCW: changePage(PAGE_STATS); break; } break; case PAGE_DESCALING: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigRotCW: changePage(PAGE_EXIT); break; case SigRotCCW: changePage(PAGE_STATS2); break; } break; case PAGE_EXIT: if (SigValueEmpty() && mode == MODE_MENU) pause(); switch (getSigValue(MODE_MENU)) { case SigInt0Rls: leaveMenu(); break; case SigRotCW: if (state == STATE_HEATING || state == STATE_ERROR || state == STATE_IDLE || state == STATE_INITALHEATING) { changePage(PAGE_SOFTOFF); } else { changePage(PAGE_RESTART); } break; case SigRotCCW: changePage(PAGE_DESCALING); break; } break; } //end switch (page) /* * Hardware FSM */ switch (state) { case STATE_NULL: break; /* * */ case STATE_OFF: if (mode == MODE_STATE) { halMachineOff(); writeBackCache(); //TODO this might be a bit confusing to change the page here even if the menu isnt actually displayed changePage(PAGE_RESTART); if (SigValueEmpty()) pause(); } switch (getSigValue(MODE_STATE)) { case SigInt0Rls: case SigPowerUp: //Check waterlevel in gray water tank //turn machine on halMachineOn(); coffeeNap(1,0); if (halIsHeating() && !halProxSensorCovered()) { //check if System starts to heat when turned on changeState(STATE_INITALHEATING); } else if (!halIsHeating() && !halProxSensorCovered()) { changeState(STATE_IDLE); } else if (halProxSensorCovered()) { logger_error("Empty Tank please!\n"); changeState(STATE_FULLTANK); } if (page != PAGE_SOFTOFF) changePage(PAGE_SOFTOFF); //the machine is on, the menu starts with the turning off page break; case SigRotCCW: case SigRotCW: case SigInt1Rls: //Enter the menu /* This should be not necessary! * if (page != PAGE_DEMO) changePage(PAGE_DEMO); //machine is off, the menu starts with the demo page*/ enterMenu(); break; } break; /* * */ case STATE_WAIT_OFF: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { case SigPressOpn: coffeeNap(1,0); //wait so no load will be switched coffeeIncreaseHeatingTime(halgetHeatingTime()); changeState(STATE_OFF); break; case SigInt0Rls: case SigInt1Rls: case SigPowerUp: if (halProxSensorCovered()) { changeState(STATE_FULLTANK); } else if (initalHeating) { changeState(STATE_INITALHEATING); } else { changeState(STATE_HEATING); } break; } break; /* * */ case STATE_INITALHEATING: initalHeating = true; if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { // case SigInt0RlsLong: // //Turn machine off again // coffeeIncreaseHeatingTime(halgetHeatingTime()); // changeState(STATE_OFF); // break; // // case SigInt0Rls: // changeState(STATE_WAIT_OFF); // break; case SigProxCvrd: changeState(STATE_FULLTANK); break; case SigPressOpn: //Inital heating finished initalHeating = false; coffeeIncreaseHeatingTime(halgetHeatingTime()); changeState(STATE_IDLE); break; case SigPowerDown: changeState(STATE_WAIT_OFF); break; case SigRotCCW: case SigRotCW: case SigInt1Rls: enterMenu(); break; } break; /* * */ case STATE_HEATING: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { // case SigInt1RlsLong: // //Turn machine _immediately_ off again // coffeeIncreaseHeatingTime(halgetHeatingTime()); // changeState(STATE_OFF); // break; // // case SigInt1Rls: // //turn machine off when heating is finished // changeState(STATE_WAIT_OFF); // break; case SigPressOpn: coffeeIncreaseHeatingTime(halgetHeatingTime()); changeState(STATE_IDLE); break; case SigInt0Rls: //start to brew a delicious coffee changeState(STATE_BREW); break; case SigProxCvrd: changeState(STATE_FULLTANK); break; case SigBrewOn: //someone brews manually coffeeManualBrewStart(); changeState(STATE_BREWMANUAL); break; case SigPowerDown: changeState(STATE_WAIT_OFF); break; case SigRotCCW: case SigRotCW: case SigInt1Rls: //Enter the menu enterMenu(); break; } break; /* * */ case STATE_IDLE: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { // case SigInt1RlsLong: // //turn machine _immediately_ off // changeState(STATE_OFF); // break; // // case SigInt1Rls: // changeState(STATE_OFF); // break; case SigPressCls: changeState(STATE_HEATING); break; case SigInt0Rls: changeState(STATE_BREW); break; case SigProxCvrd: changeState(STATE_FULLTANK); break; case SigBrewOn: //someone brews manually coffeeManualBrewStart(); changeState(STATE_BREWMANUAL); break; case SigPowerDown: changeState(STATE_OFF); break; case SigRotCCW: case SigRotCW: case SigInt1Rls: //Enter the menu enterMenu(); break; } break; /* * */ case STATE_BREW: //make sure the tank is not full if (halProxSensorCovered()) { changeState(STATE_FULLTANK); logger_error("coffee.cpp: Full tank detection failed..\n"); } else { coffeeBrew(); checkDescaling(); logger(V_BREW, "Finishing brewing\n"); if (!halProxSensorCovered()) { if (halIsHeating()) { changeState(STATE_HEATING); } else { changeState(STATE_IDLE); } } else { changeState(STATE_FULLTANK); } } break; /* * */ case STATE_BREWMANUAL: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { case SigBrewOff: coffeeManualBrewStop(); if (halIsHeating()) { changeState(STATE_HEATING); } else { changeState(STATE_IDLE); } break; } break; /* * */ case STATE_CLEANING: //this can only be executed once the machine is hot! if (!halProxSensorCovered()) { //execute the cleaning procedure coffeeClean(); if (halIsHeating()) { changeState(STATE_HEATING); } else { changeState(STATE_IDLE); } } else { changeState(STATE_FULLTANK); } break; /* * Full tank is detected at the beginning and the end of a brewing process, during * idle times, initial heating and heating */ case STATE_FULLTANK: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { case SigInt1Rls: case SigInt0Rls: if (halIsHeating() && initalHeating) { changeState(STATE_INITALHEATING); } else if (halIsHeating() && !initalHeating) { changeState(STATE_HEATING); } else { changeState(STATE_IDLE); } break; } break; /* * */ case STATE_ERROR: if (SigValueEmpty() && mode == MODE_STATE) pause(); switch (getSigValue(MODE_STATE)) { case SigInt0RlsLong: if (halIsHeating()) { coffeeIncreaseHeatingTime(halgetHeatingTime()); } changeState(STATE_OFF); break; } } } pthread_exit(EXIT_SUCCESS); } /** * Handler for the Signal send to this thread * It saves the type of signal received and tracks the time between a push and release event of up to 4 signals * The time is stored in the HalEvent variable when a release event is received * @param signum * @param siginfo * @param context */ void coffeeHandler(int signum, siginfo_t *siginfo, void *context) { sigval_t sigVal = (siginfo->si_value); sigValue = sigVal.sival_int; logger(V_BREW, "coffee.cpp: CoffeeHandler called with Signal %s\n", SigName[sigValue]); } /** * returns the Signal value from the last received Signal for the given mode and clears the variable * @return value sent with the last signal */ int getSigValue(coffee_mode_t mode) { int tmp = sigValue; if (mode == MODE_MENU) { switch (sigValue) { case SigInt0Psh: case SigInt0Rls: case SigInt0RlsLong: case SigInt1Psh: case SigInt1Rls: case SigRotCCW: case SigRotCW: sigValue = 0; menuTimeout = 0; return tmp; break; default: break; } } else { //State Mode sigValue = 0; return tmp; } //int tmp = sigValue; //sigValue = 0; //return tmp; return 0; } bool SigValueEmpty(void) { if (sigValue == 0) return true; else return false; } /** * Changes the state of the machine to newState * prints the change to the logger * @param newState */ void changeState(coffee_status_t newState) { logger(V_BREW, "Changing state to %s\n", StateName[newState]); state = newState; event_trigger("statechange", &state, sizeof(state)); } /* * Change Page to new menu page */ void changePage(coffee_menuPage_t newPage) { logger(V_BREW, "Change Page to %s\n", PageName[newPage]); event_trigger("pagechange", &newPage, sizeof(newPage)); page = newPage; } /* * Changes the mode of the machine to the given mode */ void changeMode(coffee_mode_t newMode) { if (newMode == MODE_MENU) logger(V_BREW, "Changing to menu mode\n"); else logger(V_BREW, "Changing to state mode\n"); event_trigger("modechange", &newMode, sizeof(newMode)); mode = newMode; } /* * leaving the menu * sets the start page for the next menu call to softoff */ void leaveMenu(void) { logger(V_BREW, "Leaving the menu again\n"); //leave the menu again changeMode(MODE_STATE); //change page to initial page changePage(PAGE_SOFTOFF); //stop the timeout counter menuTimeout = 0; menuTimer.stop(); } /** * entering the menu * starts the timeoutcounter and changes the mode to the Menu Mode */ void enterMenu(void) { logger(V_BREW, "Entering the menu\n"); changeMode(MODE_MENU); menuTimeout = 0; menuTimer.start(); } /** * Returns the current cleaning cycle */ uint16_t getCurrentCleanCycle(void) { return currentCleanCycle; } /** * Returns the current state of the FSM */ coffee_status_t getState(void) { return state; } uint16_t getBrewTime(void) { return brewTime; } /** * Returns the last elapsed brew time in ms */ uint16_t getLastBrewTime(void) { return lastBrewTime; } /** * Returns the local up-to-date brewcounter */ uint16_t getBrewCounter(void) { return brewCounter; } /** * Returns the total heating time in seconds */ uint64_t getTotalHeatingTime(void) { return totalHeatingTime; } /** * Returns the value of the brewcounter when the last descaling happened */ uint16_t getDescBrewCounter (void) { return descBrewcount; } /** * Returns the raw time stamp when the last descaling happened */ time_t * getDescTimestamp (void) { return &descRawTimestamp; } /** * Counter for the brew time * refresh every 200ms */ void brewTimeHandler(void) { brewTime += 200; } /** * Counter for the menu timeout * When no input is coming from the user the machine leaves the menu automatically after MENUTIMEOUT seconds */ void menuTimeHandler(void){ menuTimeout += 200; if((menuTimeout/1000) >= MENUTIMEOUT) { leaveMenu(); } } /** * handles program termination */ void coffeeTerminate(event_t *event) { logger(V_BREW, "Coffee.cpp: Terminating\n"); //stop brewing halRelaisOff(RELAIS_PUMP); brewTimer.stop(); writeBackCache(); } /** * coffeeNap does almost the same as sleep() but resumes to sleep after a signal got delivered * sec: seconds to sleep * nanosec: nanoseconds to sleep */ void coffeeNap (uint64_t sec, uint64_t nanosec){ struct timespec sleepTime; clock_gettime(CLOCK_REALTIME, &sleepTime); sleepTime.tv_sec += sec; sleepTime.tv_nsec += nanosec; //overflow if(sleepTime.tv_nsec >= 1000000000) { sleepTime.tv_nsec -= 1000000000; sleepTime.tv_sec++; } int errval = 0; do{ errval = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &sleepTime, NULL); } while(errval == EINTR); if(errval) logger_error("coffee.cpp: suspending thread failed with error %d\n", errval); } /** * Function to write back the values of the local copies * brewCounter and totalHeatingTime * */ void writeBackCache(void) { logger(V_BREW, "Writing back cache...\n"); logger(V_BREW, "Writing back brewCounter with %d\n", brewCounter); if (sqlSetConf(CFGbrewcounter, brewCounter)) { logger_error("coffee.cpp: Couldn't write brewcounter to database"); return; } logger(V_BREW, "Writing back total heating Time with %lld sec\n", totalHeatingTime); if (sqlSetConf(CFGHeatingTime, totalHeatingTime)) { logger_error("coffee.cpp: Couldn't write heating time to database"); return; } logger(V_BREW, "Writing back descaling brew counter %d\n", descBrewcount); if (sqlSetConf(CFGDescBrewCount, descBrewcount)) { logger_error("coffee.cpp: Couldn't write descaling brewcount to database"); return; } struct tm * timeinfo; timeinfo = localtime(&descRawTimestamp); logger(V_BREW, "Writing back descaling timestamp %d-%d-%d %d:%d\n", timeinfo->tm_mday, timeinfo->tm_mon, timeinfo->tm_year+1900, timeinfo->tm_hour, timeinfo->tm_min); if (sqlSetConf(CFGDescTimestamp, (uint64_t)descRawTimestamp)) { logger_error("coffee.cpp: Couldn't write descaling timestamp to database"); return; } } /* * Procedure for cleaning the machine */ void coffeeClean(void) { logger(V_BREW, "Cleaning...\n"); for (int i = 0; i < CLEANING_CYCLES; i++) { currentCleanCycle++; halPumpOn(); coffeeNap(3,0); halPumpOff(); coffeeNap(15,0); } updateDescaling(); descaling = false; currentCleanCycle = 0; event_trigger("descaling", &descaling, sizeof(bool)); } /* * */ void coffeeManualBrewStart (void) { logger(V_BREW, "Starting manual brewing...\n"); coffeeIncreaseBrewCounter(); brewTime = 0; brewTimer.start(); } /* * */ void coffeeManualBrewStop (void) { lastBrewTime = brewTime; brewTime = 0; brewTimer.stop(); } /** * Brewing process */ void coffeeBrew(void) { coffeeIncreaseBrewCounter(); halIncreaseLogCycleCounter(); /* * Preinfusion */ logger(V_BREW, "Starting preinfusion...\n"); halResetFlow(); halPumpOn(); brewTime = 0; brewTimer.start(); while (halGetFlow() < AMOUNT_PREINFUSION && brewTime < TIME_PREINFUSION) { //TODO don't use coffeeNap here since we don't want to resume to sleep after a signal got caught... coffeeNap(0, 50000000); if (getSigValue(MODE_STATE) == SigInt0Rls){ stopBrewing(); return; } } stopBrewing(); /* * Wait for coffee to soak in infused water */ brewTimer.start(); while (brewTime < TIME_SOAK) { coffeeNap(1, 100000000); if (getSigValue(MODE_STATE) == SigInt0Rls) { stopBrewing(); return; } } stopBrewing(); /* * Brewing the actual espresso */ logger(V_BREW, "Starting infusion...\n"); halPumpOn(); brewTimer.start(); while (brewTime < TIME_INFUSION && halGetFlow() < AMOUNT_DBLESPRESSO) { coffeeNap(1, 100000000); if (getSigValue(MODE_STATE) == SigInt0Rls){ stopBrewing(); break; } } stopBrewing(); halIncreaseLogCycleCounter(); return; } /* * Wrapper function for the end of a brewing process * this function stops the pump, brewtimer and resets the flow and brew time to zero */ void stopBrewing() { halPumpOff(); brewTimer.stop(); lastBrewTime = brewTime; brewTime = 0; halResetFlow(); } /** * */ void coffeeIncreaseBrewCounter(void) { brewCounter++; } /** * */ void coffeeIncreaseHeatingTime(uint64_t heatingTime) { totalHeatingTime += heatingTime; } /** * Checks if the descaling is necessary * uses descBrewcount and descTimestamp */ void checkDescaling(){ int16_t dirtyEspresso = checkDirtyEspresso (); int16_t dirtyTime = checkDirtyTime (); if(dirtyEspresso <= 0) { logger(V_BREW, "Descaling necessary due to quantity: %d\n", dirtyEspresso); descaling = true; event_trigger("descaling", &descaling, sizeof(bool)); } if(dirtyTime <= 0) { logger(V_BREW, "Descaling necessary due to time in days: %d\n", dirtyTime); descaling = true; event_trigger("descaling", &descaling, sizeof(bool)); } } /** * this function returns the remaining espressi to be brewed before the descaling event is fired * returns a positive integer when there are cups remaining * and a negative when the number of cups are exceeded * Number of cups after a descaling is defined with DIRTY_ESPRESSO */ int16_t checkDirtyEspresso (void) { return descBrewcount + DIRTY_ESPRESSO - brewCounter; } /** * Returns the remaining days before the next descaling event is fired * returns a positive integer if there is time left and a negative one if the descaling time was exceeded */ int16_t checkDirtyTime (void) { time_t rawtime; time(&rawtime); double diffseconds = difftime(rawtime, descRawTimestamp); diffseconds /= 24*60*60; //calculate the days return DIRTY_TIME - diffseconds; } /** * updates the corresponding variables after a descaling process */ void updateDescaling(){ descBrewcount = brewCounter; time_t newDesTimestamp; time(&newDesTimestamp); if(newDesTimestamp == -1){ logger(V_BREW, "Whoops, couldn't retrieve new descaling timestamp\n"); } else { descRawTimestamp = newDesTimestamp; } }