#include <PinChangeInterrupt.h>
#include <RotaryEncoder.h>
#include <LiquidCrystal_I2C.h>
#include <TimerOne.h>
#include <EEPROM.h>
#include <TimeAlarms.h>
#include <time.h>
RotaryEncoder *encoder = nullptr;
LiquidCrystal_I2C lcd(0x27, 20, 4);
#define buttonPin 6
#define rotAPin 7
#define rotBPin 8
#define buzzerPin 9
#define hotPlatePin 10 //Can use 9 or 10. Timer1 will interfere with normal pwm functions on these pins
volatile bool buttonFlag;
uint8_t block[8] = {0xff,0xff,0xff,0xff,0xff,0xff,0xff};
//we need some structs now to store each step and to store the programs
struct Step {
bool buzz;
byte timer;
byte power;
};
struct Program {
byte numSteps;
byte index;
Step steps[30];
};
void setup() {
//Rotary encoder setup
encoder = new RotaryEncoder(rotAPin, rotBPin, RotaryEncoder::LatchMode::FOUR3);
attachPCINT(digitalPinToPCINT(rotAPin), checkPosition, CHANGE);
attachPCINT(digitalPinToPCINT(rotBPin), checkPosition, CHANGE);
//Button setup
pinMode(buttonPin, INPUT_PULLUP);
attachPCINT(digitalPinToPCINT(buttonPin), buttonFlagger, RISING);
//hotplate setup
pinMode(hotPlatePin, OUTPUT);
Timer1.initialize(1000000); //initialize timer to 1 second
Timer1.start();
Timer1.pwm(hotPlatePin, 0);
//LCD setup
lcd.init();
lcd.backlight();
//buzzer setup
pinMode(buzzerPin, OUTPUT);
//Custom char for power indicator, of course
lcd.createChar(0, block);
//Why am I doing this
randomSeed(analogRead(0));
//Serial.begin(115200);
for(int x = 1; x <= 9; x++){
resetProgram(x);
}
//lcdPrint(1,1, giveRandom(80*2));
}
void loop() {
// put your main code here, to run repeatedly:
//lcdPrint(1,1, giveRandom(80*2));
stoveLoop();
//lcdPrint(1,1, giveRandom(80*2));
programSelectLoop();
}
////////////////////////////////////////////////////////////////////////////////////////
// Interrupt stuff
void checkPosition() { //Call this as interrupt fuction to make encoder work
encoder->tick();
}
void buttonFlagger(){
buttonFlag = 1;
}
int flagChecker(){
if (buttonFlag == 1) {
buttonFlag = 0;
return 2;
}
int direction = (int)(encoder->getDirection());
return direction;
}
////////////////////////////////////////////////////////////////////////////////////////////
void lcdPrint(int x, int y, String text) {
lcd.setCursor(x - 1, y - 1);
lcd.print(text);
}
void clear(){
lcd.setCursor(0,0);
for (int x = 0; x < 80; x++){
lcd.print(" ");
}
}
void updatePower(long power, bool ring = false){
//takes a number between 0 and 100 and updates duty cycle with numbers 0 to 1023
int duty;
digitalWrite(buzzerPin, ring);
duty = power * 1023 / 100;
Timer1.setPwmDuty(hotPlatePin, duty);
}
String giveRandom(int numChars){//This program is solely to generate random characters for the screen transition
String randomString = "";
for (int i = 0; i < numChars; i++) {
char randomChar = random(32, 127); // Generate a random ASCII character
randomString += randomChar;
}
return randomString;
}
void powerVisual(byte power){//This function does the bar at the bottom of the screen for power
byte numBlocks = power/5;
lcd.setCursor(0,3);
for (byte x = 0; x < numBlocks; x++){
lcd.write(0);
}
}
void stoveLoop(){
//This is where the program initializes and this is where we live when we use the stove manually.
//We want to listen for a button press to move to the program select, and we want to listen to
//encoder rotation to change the duty cycle of the stove.
lcd.blink_off();
int flag = 0;
int power = 0;
lcdPrint(1,1,"Manual Stove Control ");
while (true){
flag = flagChecker(); //Check to see if user did something
switch (flag) {
case 0:
break;
case -1:
if (power > 0) {
power -= 9;
power = constrain(power, 0, 100);
lcdPrint(1,4," ");
updatePower(power);
}
break;
case 1:
if (power < 100) {
power += 10;
power = constrain(power, 0, 100);
updatePower(power);
}
break;
case 2:
updatePower(0); //Shuts off stove
return;
break;
}
lcdPrint(9,3,((String)(power) + " % "));
powerVisual(power);
}
}
void programSelectLoop(){
//this is gonna be weird. I need a return button at the top and a series of program buttons below.
//I can probably just use an array of strings to hold the different options and include some basic logic for moving around.
const char progSelectOptions[12][11] = {
"Programs ",
" Return ",
" Program 1",
" Program 2",
" Program 3",
" Program 4",
" Program 5",
" Program 6",
" Program 7",
" Program 8",
" Program 9",
" Reset ALL"
};
byte selectedIndex = 1;
byte screenIndex = 0;
byte maxScreenIndex = 8;
int flag = 0;
lcd.blink_off();
clear();
for (byte i = 1; i < 5; i++){
lcdPrint(1,i,progSelectOptions[i+screenIndex-1]);
}
lcdPrint(1, selectedIndex-screenIndex+1, ">");
while (true) {
flag = flagChecker(); //Check to see if user did something
delay(20); //This helps with user input somehow
switch (flag) {
case 0:
continue;
break;
case 1:
if (selectedIndex - screenIndex == 0 && screenIndex > 0) screenIndex--;
if (selectedIndex > 1) selectedIndex--;
break;
case -1:
if (selectedIndex - screenIndex == 3 && screenIndex < maxScreenIndex) screenIndex++;
if (selectedIndex < 11) selectedIndex++;
break;
case 2:
if (selectedIndex == 1) {
return; // Go back to stove screen
}
if (selectedIndex > 1 && selectedIndex < 11){
//lcdPrint(1,1, giveRandom(80*2));
programChoices(selectedIndex - 1); //Enter options for selected program
}
if (selectedIndex == 11) {
for(int x = 1; x <= 9; x++){
resetProgram(x);
}
}
break;
}
//Display stuff
clear();
for (byte i = 1; i < 5; i++){
lcdPrint(1,i,progSelectOptions[i+screenIndex-1]);
}
lcdPrint(1, selectedIndex - screenIndex + 1, ">");
}
}
void programChoices(byte programIndex){
//We choose whether to run the program or edit it here.
const char progSelectOptions[4][21] = {
" Program # ",
" Run ",
" Edit ",
" Return "
};
byte selectedIndex = 1;
int flag = 0;
lcd.blink_off();
for (byte i = 1; i < 5; i++){
lcdPrint(1,i,progSelectOptions[i - 1]);
}
lcdPrint(15, 1, (String)(programIndex));
lcdPrint(1, selectedIndex + 1, ">");
while (true) {
flag = flagChecker();
delay(20);
switch (flag) {
case 0:
continue;
break;
case -1:
if (selectedIndex < 3) selectedIndex++;
break;
case 1:
if (selectedIndex > 1) selectedIndex--;
break;
case 2:
if (selectedIndex == 3) {
//lcdPrint(1,1, giveRandom(80*2));
return;
}
else if (selectedIndex == 2) {
//lcdPrint(1,1, giveRandom(80*2));
programEditLoop(programIndex);
lcd.blink_off();
break;
}
else if(selectedIndex == 1) {
//lcdPrint(1,1, giveRandom(80*2));
programRunLoop(programIndex);
break;
}
break;
}
//Display stuff
for (byte i = 1; i < 5; i++){
lcdPrint(1,i,progSelectOptions[i - 1]);
}
lcdPrint(15, 1, (String)(programIndex));
lcdPrint(1, selectedIndex + 1, ">");
}
}
const short progLocations[9] = {
142,
234,
326,
418,
510,
602,
694,
786,
878
};
void resetProgram(byte progNumber){
//ProgNumber starts at 1
Program emptyProg;
Step emptyStep;
emptyStep.buzz = false;
emptyStep.power = 0;
emptyStep.timer = 0;
emptyProg.index = progNumber;
emptyProg.numSteps = 1;
for(int x = 0; x < 30; x++){
emptyProg.steps[x] = emptyStep;
}
EEPROM.put(progLocations[progNumber-1],emptyProg);
}
void updateEditDisplay(Program tempProg, int sind){
int windowLength = tempProg.numSteps + 3;
int row = 0;
for (int x = sind; x < sind + 4; x++){
if (x == 0){
char labelLine[21];
sprintf(labelLine, "Return Program #%i", tempProg.index);
lcd.setCursor(0, row);
lcd.print(labelLine);
}
else if (x == windowLength - 2) {
lcd.setCursor(0, row);
lcd.print("Add Step ");
}
else if (x == windowLength - 1){
lcd.setCursor(0, row);
lcd.print("Remove Step ");
}
else { //Write out step information
char lineString[21];
char buzzChar = tempProg.steps[x - 1].buzz ? 'R' : 'S';
String timeString = formatTime(nonLinearTime(tempProg.steps[x - 1].timer));
sprintf(lineString, "%02u: %c %3u%% %s", x, buzzChar, tempProg.steps[x - 1].power, timeString.c_str());
lcd.setCursor(0, row);
lcd.print(lineString);
}
row++;
}
}
void programEditLoop(int programIndex){
// The program index is the displayed number. So, the program stored at progLocations[0] has programIndex = 1.
// Program.index also starts at 1
int hind = 0; // horizontal index
int vind = 0; // vertical index
int sind = 0; // screen index, this is the index of the line that shows up as the first row
int flag = 0;
Program tempProg;
tempProg = EEPROM.get(progLocations[programIndex-1], tempProg); //Load program into temp
updateEditDisplay(tempProg, 0);
lcd.setCursor(0,0);
lcd.blink_on();
while (true){
verticalScrolling:
flag = flagChecker(); //checks for user input
delay(20);
byte mind = tempProg.numSteps + 2; //maximum vertical index
//Switch for navigating vertically
switch (flag) {
case 0:
continue;
case -1: //cursor down
if (vind == mind) continue;
vind++;
if (vind > sind + 3) sind++;
break;
case 1: //cursor up
if (vind == 0) continue;
vind--;
if (vind < sind) sind = vind;
break;
case 2:
//We're pressing the button on a menu item
if (vind == 0){
//return selected, save and exit
EEPROM.put(progLocations[tempProg.index-1], tempProg);
return;
}
else if (vind == tempProg.numSteps + 1){
//add selected, add another step
if (tempProg.numSteps < 30) tempProg.numSteps++;
}
else if (vind == tempProg.numSteps + 2){
//remove selected, remove a step and scroll up one
if (tempProg.numSteps > 1) {
tempProg.numSteps--;
sind--;
vind--;
}
}
else {
// Step selected. Move into step and cycle between parameters.
int flag2 = 0;
hind = 0;
const byte hpos[4] = {2,4,9,17};
byte vpos = vind-sind; //vpos is for tracking vertical position on screen, ranges 0-3
while (true) {
horizontalScrolling:
lcd.setCursor(hpos[hind], vpos);
lcd.blink_on();
flag2 = flagChecker();
delay(20);
switch (flag2){
case 0:
continue;
case 1:
if (hind < 3) hind++;
break;
case -1:
if (hind > 0) hind--;
break;
case 2:
if (hind == 0){
goto verticalScrolling;
}
else {
//Third nested loop!!!!! Whooo, we're getting crazy!
int flag3 = 0;
while (true) {
flag3 = flagChecker();
switch (flag3) {
case 0:
continue;
case 2:
goto horizontalScrolling;
break;
case 1:
if (hind == 1){
tempProg.steps[vind - 1].buzz = !tempProg.steps[vind - 1].buzz;
}
else if (hind == 2){
int power = tempProg.steps[vind - 1].power + 10;
power = constrain(power, 0, 100);
tempProg.steps[vind - 1].power = power;
}
else if (hind == 3) {
int time = tempProg.steps[vind - 1].timer + 5;
time = constrain(time, 0, 255);
tempProg.steps[vind - 1].timer = time;
}
break;
case -1:
if (hind == 1){
tempProg.steps[vind - 1].buzz = !tempProg.steps[vind - 1].buzz;
}
else if (hind == 2){
int power = tempProg.steps[vind - 1].power - 9;
power = constrain(power, 0, 100);
tempProg.steps[vind - 1].power = power;
}
else if (hind == 3) {
int time = tempProg.steps[vind - 1].timer - 1;
time = constrain(time, 0, 255);
tempProg.steps[vind - 1].timer = time;
}
break;
}
lcd.blink_off();
updateEditDisplay(tempProg, sind);
lcd.setCursor(hpos[hind],vpos);
lcd.blink_on();
}
}
break;
}
}
}
break;
}
lcd.blink_off();
updateEditDisplay(tempProg, sind);
lcd.blink_on();
lcd.setCursor(0, vind-sind);
}
}
volatile bool screenUpdate;
void screenUpdateFlag(){
screenUpdate = true;
}
void programRunLoop(byte programIndex){
//Load Program
Program runningProgram;
EEPROM.get(progLocations[programIndex - 1], runningProgram);
//Calculate program duration
//Steps are displayed 1-30 and stored 0-29
unsigned long programDuration = 0;
for (int x = 0; x < runningProgram.numSteps; x++){
programDuration += nonLinearTime(runningProgram.steps[x].timer);
}
//Initialize display///////////////////////////////////////////////////////////
char runningDisplay[4][21];
sprintf(runningDisplay[0], "Prog %i in progress", runningProgram.index);
sprintf(runningDisplay[1], "Step %02u of %02u", 1, runningProgram.numSteps);
sprintf(runningDisplay[2], "%s|%s", formatTime(nonLinearTime(runningProgram.steps[0].timer)).c_str(), formatTime(programDuration).c_str());
sprintf(runningDisplay[3], "Heat %3u%%", runningProgram.steps[0].power);
for (int x = 0; x<4; x++){
lcd.setCursor(0,x);
lcd.print(runningDisplay[x]);
}
/////////////////////////////////////////////////////////////////////////////////
//Loop for going through steps
int flag = 0; //Used for receiving button press to abandon program.. Maybe ask for two button presses?
long progStartTime = millis();
//Steps are stored 0-29 and printed as 1-30. We will work from the storage perspective.
for (int currentStep = 0; currentStep < runningProgram.numSteps; currentStep++){
updatePower(runningProgram.steps[currentStep].power, runningProgram.steps[currentStep].buzz); //Update the stove's power
long stepStartTime = millis();
while (true){
//check for breaks
flag = flagChecker;
if (flag == 2){
updatePower(0);
return;
}
//Compute step time so far and check for program advance
long stepTime = millis() - stepStartTime;
long advanceTime = timeOfAdvance(runningProgram, currentStep); //in seconds
long programTime = (millis() - progStartTime); //in milliseconds
long remainingProgramTime = (programDuration * 1000 - programTime); //in milliseconds
if (ceilDiv(programTime, 1000) > advanceTime){
updatePower(0);
break;
}
if (remainingProgramTime < 100) {
updatePower(0);
return;
}
//Update screen step, timers, and heat
sprintf(runningDisplay[1], "Step %02u of %02u", currentStep + 1, runningProgram.numSteps);
sprintf(runningDisplay[2], "%s|%s", formatTime(nonLinearTime(runningProgram.steps[currentStep].timer) - stepTime / 1000).c_str(), formatTime(ceilDiv(remainingProgramTime, 1000)).c_str());
sprintf(runningDisplay[3], "Heat %03u%%", runningProgram.steps[currentStep].power);
for (int x = 1; x<4; x++){
lcd.setCursor(0,x);
lcd.print(runningDisplay[x]);
}
delay(50);
}
}
}
unsigned long nonLinearTime(byte compressedTime){ //This is my silly way of having fewer big numbers
unsigned long result = 0;
for(int i = 1; i <= compressedTime + 1; i++){
result++;
if (i > 10) result++;
if (i > 15) result += 2;
if (i > 20) result++;
if (i > 24) result += 5;
if (i > 36) result += 10;
if (i > 57) result += 10;
if (i > 97) result += 30;
if (i > 127) result += 240;
if (i > 175) result += 300;
if (i > 217) result += 600;
if (i > 229) result += 2400;
if (i > 235) result += 3600;
if (i > 240) result += 7200*5;
}
return result;
}
String formatTime(unsigned long totalSeconds) {
//negative input bug fix
if (totalSeconds < 1) return String("000:00:00");
// Calculate hours, minutes, and seconds
unsigned int hours = totalSeconds / 3600;
unsigned int minutes = (totalSeconds % 3600) / 60;
unsigned int seconds = totalSeconds % 60;
// Format the time into a string "xxx:xx:xx"
char timeString[10]; // "xxx:xx:xx" + null terminator
sprintf(timeString, "%03u:%02u:%02u", hours, minutes, seconds);
return String(timeString);
}
int ceilDiv(int numerator, int denominator) {
int result = numerator / denominator;
// If there's a remainder, increment the result to round up
if (numerator % denominator != 0) {
result++;
}
return result;
}
int timeOfAdvance(Program runningProgram, int currentStep){
//Takes the program and the current step and returns the quantity of time between the
//program start and the next step advance
int result = 0;
for (int x = 0; x <= currentStep; x++){
result += nonLinearTime(runningProgram.steps[x].timer);
}
return result;
}