#include <LiquidCrystal.h>
void DisplayLidStatus(bool state);
void DisplayStr(char msg[], int row, int col);
void DisplayTime(int seconds, byte row, byte col);
void PrettyTime(char* outStr, int time_seconds);
void DisplayPrettyTime(int time_seconds, byte row, byte col);
void Reset(char msg[] = {});
#define P_BTN_INC A1
#define P_BTN_DEC A2
#define P_BTN_START A3
#define P_BTN_RESET A0
#define P_LID A4
#define P_BUZZER 6
#define P_RELAY 2
#define PRESSED LOW
#define BOUNCE_DELAY 260
#define TICK_TIME 128
#define SECOND_TICK_TIME 1000
#define LONG_TICK_TIME 10000
#define NOTE_G4 392
#define NOTE_G5 784
#define NOTE_G6 1568
#define NOTE_E4 330
#define NOTE_G3 196
#define NOTE_C4 261.63
#define NOTE_F4 349.23
#define NOTE_C5 523.25
#define NOTE_A2 110
#define NOTE_FS4 370
#define NOTE_A4 440
#define NOTE_CS5 554
#define NOTE_D4 294
#define REST 0
byte custom_shine[] = {
B01000,
B00101,
B00011,
B10111,
B01000,
B00101,
B00011,
B00111
};
byte custom_updown[] = {
B00100,
B01110,
B11111,
B00100,
B00100,
B11111,
B01110,
B00100
};
byte custom_up[] = {
B00100,
B01110,
B11111,
B00100,
B00100,
B00100,
B00100,
B00000
};
byte custom_down[] = {
B00000,
B00100,
B00100,
B00100,
B00100,
B11111,
B01110,
B00100
};
byte custom_lid[] = {
B00010,
B00100,
B01000,
B10000,
B11111,
B11111,
B11111,
B00000
};
byte custom_asterix[] = {
B00000,
B01010,
B00100,
B11111,
B00100,
B01010,
B00000,
B00000
};
const int melody[] PROGMEM = {
NOTE_FS4,8, REST,8, NOTE_A4,8, NOTE_CS5,8, REST,8,NOTE_A4,8, REST,8, NOTE_FS4,8, NOTE_D4,8
};
enum action { LID_OPEN, INCREASE, DECREASE, SAVE, RESET, FINISHED, RUNNING, NONE };
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
//char buffer[16];
bool IS_RUNNING = false;
bool settings_changed = false;
bool blink = false;
unsigned long start_time = 0;
int run_time = 30;
int time_left = 0;
// Keep track of changes
bool current_relay = HIGH;
bool current_lid_state = true;
bool display_updated = false;
int last_action = NONE;
unsigned long last_button_press = 0;
unsigned long last_tick = 0;
unsigned long last_second_tick = 0;
unsigned long last_long_tick = 0;
enum action ACTION;
void setup() {
Serial.begin(115200);
lcd.begin(16, 2);
pinMode(P_BTN_INC, INPUT_PULLUP);
pinMode(P_BTN_DEC, INPUT_PULLUP);
pinMode(P_BTN_START, INPUT_PULLUP);
pinMode(P_BTN_RESET, INPUT_PULLUP);
pinMode(P_BUZZER, OUTPUT);
pinMode(P_RELAY, OUTPUT);
pinMode(P_LID, INPUT);
lcd.clear();
lcd.createChar(0, custom_shine);
lcd.createChar(1, custom_updown);
lcd.createChar(2, custom_up);
lcd.createChar(3, custom_down);
lcd.createChar(4, custom_lid);
lcd.createChar(5, custom_asterix);
// Check lid
current_lid_state = (digitalRead(P_LID) == HIGH);
DisplayLidStatus(current_lid_state);
SetAction(NONE);
StartUpMelody();
}
void loop() {
bool val_btn_inc = digitalRead(P_BTN_INC);
bool val_btn_dec = digitalRead(P_BTN_DEC);
bool val_btn_start = digitalRead(P_BTN_START);
bool val_btn_reset = digitalRead(P_BTN_RESET);
bool lid_state = digitalRead(P_LID);
// ---------------------------------------------------
// Handle Input
// ---------------------------------------------------
if (lid_state == HIGH && IsSecondTick()) {
if (blink) {
lcd.setCursor(13, 0);
lcd.write(4);
}
else {
lcd.setCursor(13, 0);
lcd.write(" ");
}
blink = !blink;
}
if (lid_state == LOW && IsSecondTick() && ACTION != FINISHED) {
lcd.setCursor(13, 0);
lcd.write(" ");
}
// Lid opened while running. Cancel everything.
if (lid_state == HIGH && ACTION == RUNNING) {
digitalWrite(P_RELAY, LOW);
lcd.clear();
lcd.setCursor(3, 0);
lcd.write("LID OPENED");
lcd.setCursor(1, 1);
lcd.write("Runtime: ");
char time_str[12];
time_str[11] = '\0';
unsigned long secs_elapsed = (millis() - start_time) / 1000;
PrettyTime(time_str, secs_elapsed, false);
lcd.setCursor(10,1);
lcd.write(time_str);
CancelBlip();
delay(2000);
SetAction(LID_OPEN);
}
if (val_btn_reset == PRESSED && IsDebounced(last_button_press)) {
if (ACTION == RUNNING) {
char msg[] = " STOPPED\0";
Reset(msg, sizeof(msg));
CancelBlip();
delay(1500);
lcd.clear();
}
else {
Reset({}, 0);
}
}
if (ACTION == LID_OPEN) {
//if (IsSecondTick())
//Serial.println("Open lid");
return;
}
if (val_btn_inc == PRESSED && IsDebounced(last_button_press) && !IS_RUNNING) {
int tmp = run_time;
byte amount = run_time < 60 ? 10 : 30;
amount = run_time >= 600 ? 60 : amount;
run_time += run_time + amount >=3600 ? 0 : amount;
DisplayPrettyTime(run_time, 0, 0);
DisplayArrows(true);
if (run_time == tmp)
ErrorBlip();
else
blip();
}
if (val_btn_dec == PRESSED && IsDebounced(last_button_press) && !IS_RUNNING) {
int tmp = run_time;
byte amount = run_time <= 60 ? 10 : 30;
amount = run_time > 600 ? 60 : amount;
run_time -= run_time - amount <=0 ? 0 : amount;
DisplayPrettyTime(run_time, 0, 0);
DisplayArrows(true);
if (run_time == tmp)
ErrorBlip();
else
blip();
}
if (val_btn_start == PRESSED && IsDebounced(last_button_press) && !IS_RUNNING) {
if (lid_state == HIGH) {
lcd.clear();
//char msg[] = " STOPPED\0";
lcd.setCursor(4, 0);
lcd.write("LID OPEN");
lcd.setCursor(2, 0);
lcd.write(4);
lcd.setCursor(13, 0);
lcd.write(4);
CancelBlip();
delay(1800);
lcd.clear();
display_updated = false;
return;
}
//Serial.println("Starting relay");
digitalWrite(P_RELAY, HIGH);
start_time = millis();
IS_RUNNING = true;
SetAction(RUNNING);
DisplayArrows(false);
current_relay = HIGH;
StartBlip();
time_left = run_time;
//Serial.print("Setting time: ");
//Serial.println(time_left);
}
// ---------------------------------------------------
// Handle ACTION
// ---------------------------------------------------
if (ACTION == NONE && IsTick() && !display_updated) {
//Serial.print("Handling action NONE, last_action: ");
//Serial.println(last_action);
DisplayTimeSetting(run_time);
//last_action = NONE;
display_updated = true;
}
if (ACTION == FINISHED && IsLongTick()) {
FinishedSound();
last_long_tick = millis();
}
//Serial.println("#1");
if (IS_RUNNING && IsTick()) {
DisplayRunTime(run_time, 2, 2);
if (time_left <= 0) {
char msg[] = " Finished\0";
Reset(msg, sizeof(msg));
SetAction(FINISHED);
}
}
}
bool IsTick() {
if (millis() >= (TICK_TIME + last_tick)) {
last_tick = millis();
return true;
}
return false;
}
bool IsSecondTick() {
if (millis() >= (SECOND_TICK_TIME + last_second_tick)) {
last_second_tick = millis();
return true;
}
return false;
}
bool IsLongTick() {
//Serial.print("longTick: ");
//Serial.println((millis() >= (LONG_TICK_TIME + last_long_tick)));
if (millis() >= (LONG_TICK_TIME + last_long_tick)) {
last_long_tick = millis();
return true;
}
return false;
}
void Reset(char msg[], byte length) {
IS_RUNNING = false;
start_time = 0;
time_left = 0;
digitalWrite(P_RELAY, LOW);
lcd.clear();
display_updated = false;
SetAction(NONE);
if (length > 0) {
lcd.clear();
lcd.setCursor(3, 0);
lcd.write(msg);
lcd.setCursor(2, 0);
lcd.write(5);
lcd.setCursor(13, 0);
lcd.write(5);
//delay(2000);
//lcd.clear();
}
}
bool IsDebounced(unsigned long last_time) {
if (millis() - last_time >= BOUNCE_DELAY)
{
last_button_press = millis();
return true;
}
return false;
}
void DisplayTimeSetting(int seconds) {
char time_str[12];
time_str[11] = '\0';
PrettyTime(time_str, seconds, true);
lcd.setCursor(0, 0);
lcd.write(time_str);
DisplayArrows(true);
}
void DisplayArrows(bool visible) {
if (visible) {
lcd.setCursor(15, 0);
lcd.write(2);
lcd.setCursor(15, 1);
lcd.write(3);
}
else {
lcd.setCursor(15, 0);
lcd.write(" ");
lcd.setCursor(15, 1);
lcd.write(" ");
}
}
void DisplayPrettyTime(int time_seconds, byte row, byte col) {
char time_str[12];
time_str[11] = '\0';
PrettyTime(time_str, run_time, true);
lcd.clear();
lcd.setCursor(col, row);
lcd.write(time_str);
}
void PrettyTime(char* outStr, int time_seconds, bool include_text) {
byte minutes = time_seconds / 60;
byte seconds = time_seconds % 60;
if (include_text)
sprintf(outStr, "Time: %02d:%02d", minutes, seconds);
else
sprintf(outStr, "%02d:%02d", minutes, seconds);
}
void DisplayLidStatus(bool state) {
if (state) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.write(" LID OPEN ");
}
}
void DisplayRunTime(int runtime_in_seconds, byte row, byte col) {
unsigned long secs_elapsed = (millis() - start_time) / 1000;
int secs_remaining = run_time - secs_elapsed;
// Custom character
lcd.setCursor(0, 1);
lcd.write(byte(0));
char buf[16];
byte t1 = secs_elapsed / 60;
byte t2 = secs_elapsed % 60;
byte t3 = secs_remaining / 60;
byte t4 = secs_remaining % 60;
sprintf(buf, "%02d:%02d (%02d:%02d)", t1,t2,t3,t4);
// sprintf(buf, "%02d:%02d (%02d:%02d)",
// (secs_elapsed / 60),
// (secs_elapsed % 60),
// (secs_remaining / 60),
// (secs_remaining % 60)
// );
DisplayStr(buf, row, col);
time_left = secs_remaining;
}
void DisplayStr(char msg[], int row, int col) {
lcd.setCursor(col, row);
lcd.write(msg);
}
void FinishedSound() {
//
// Stolen from: https://github.com/robsoncouto/arduino-songs
//
// sizeof gives the number of bytes, each int value is composed of two bytes (16 bits)
// there are two values per note (pitch and duration), so for each note there are four bytes
int notes = sizeof(melody) / sizeof(melody[0]) / 2;
// change this to make the song slower or faster
int tempo = 225;
//int tempo = 114;
// this calculates the duration of a whole note in ms
int wholenote = (60000 * 4) / tempo;
int divider = 0, noteDuration = 0;
// iterate over the notes of the melody.
// Remember, the array is twice the number of notes (notes + durations)
for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) {
// calculates the duration of each note
divider = pgm_read_word_near(melody+thisNote + 1);
if (divider > 0) {
// regular note, just proceed
noteDuration = (wholenote) / divider;
} else if (divider < 0) {
// dotted notes are represented with negative durations!!
noteDuration = (wholenote) / abs(divider);
noteDuration *= 1.5; // increases the duration in half for dotted notes
}
// we only play the note for 90% of the duration, leaving 10% as a pause
tone(P_BUZZER, pgm_read_word_near(melody+thisNote), noteDuration * 0.9);
// Wait for the specief duration before playing the next note.
delay(noteDuration);
// stop the waveform generation before the next note.
noTone(P_BUZZER);
}
}
/*
tone(P_BUZZER, NOTE_G4, 35);
delay(35);
tone(P_BUZZER, NOTE_G5, 35);
delay(35);
tone(P_BUZZER, NOTE_G6, 35);
*/
void blip() {
/* Mario coin
tone(P_BUZZER, NOTE_E4, 35);
delay(35);
tone(P_BUZZER, NOTE_E5, 35);
delay(35);
noTone(8);
*/
tone(P_BUZZER, NOTE_D4, 35);
delay(35);
tone(P_BUZZER, NOTE_G4, 35);
delay(35);
noTone(P_BUZZER);
}
void ErrorBlip() {
tone(P_BUZZER, NOTE_A2, 100);
delay(80);
noTone(P_BUZZER);
}
void CancelBlip() {
tone(P_BUZZER, NOTE_A2, 160);
delay(230);
tone(P_BUZZER, NOTE_A2, 160);
delay(230);
tone(P_BUZZER, NOTE_A2, 700);
delay(700);
noTone(P_BUZZER);
}
void StartBlip() {
tone(P_BUZZER, NOTE_C4, 100);
delay(80);
tone(P_BUZZER, NOTE_F4, 100);
delay(80);
tone(P_BUZZER, NOTE_C5, 100);
delay(80);
noTone(P_BUZZER);
}
void SetAction(int action) {
ACTION = action;
//last_action = action;
}
// I let Copilot write this one.
void StartUpMelody() {
int melody[] = {
NOTE_G4, 8, NOTE_G4, 8, NOTE_E4, 8, NOTE_C5, 8
};
int notes = sizeof(melody) / sizeof(melody[0]) / 2;
int tempo = 200;
int wholenote = (60000 * 4) / tempo;
int divider = 0, noteDuration = 0;
for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) {
divider = melody[thisNote + 1];
if (divider > 0) {
noteDuration = (wholenote) / divider;
} else if (divider < 0) {
noteDuration = (wholenote) / abs(divider);
noteDuration *= 1.5;
}
tone(P_BUZZER, melody[thisNote], noteDuration * 0.9);
delay(noteDuration);
noTone(P_BUZZER);
}
}