#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include "Yabl.hpp"
#include "KitchenTimer.hpp"
#include "AlarmTone.hpp"
//#define DISPLAY_Y32 // Remove the comment if the display has only 32 instead of 64 pixel lines
#define MINUTES_DEFAULT // Remove the comment if you want the time setting to start with the minutes.
//
// gobal constants
//
constexpr unsigned int SECOND{1000}; // 1000ms = 1 Second (If the clock runs too slowly, the value can be reduced a little.)
constexpr byte BUFFERLENGTH{6}; // 5 characters + end-of-string character '\0'.
constexpr byte DISPLAY_MAX_X{128};
#ifndef DISPLAY_Y32
constexpr byte DISPLAY_MAX_Y{64};
#else
constexpr byte DISPLAY_MAX_Y{32};
#endif
// Font u8g2_font_freedoomr25_mn // 19 Width 26 Height
// Font u8g2_font_logisoso42_tn // 24 Width 51 Hight
constexpr byte FONT_WIDTH{24};
constexpr byte FONT_HIGHT{51};
// The following display values are calculated from the upper four values. No change necessary.
constexpr byte DISPLAY_X{(DISPLAY_MAX_X - FONT_WIDTH * (BUFFERLENGTH - 1)) / 2}; // Column = X Coordinate
constexpr byte DISPLAY_Y{(DISPLAY_MAX_Y + FONT_WIDTH) / 2}; // Row = Y coordinate
constexpr byte MINUTES_LINE_X{DISPLAY_X}; // LINE = Coordinates for the line under minute and second digits
constexpr byte SECONDS_LINE_X{DISPLAY_X + FONT_WIDTH * 3};
constexpr byte LINE_Y{DISPLAY_Y + 2}; // Line below the numbers
constexpr byte LINE_WIDTH{FONT_WIDTH * 2}; // Line length = font width * 2
constexpr byte PIN_IN1{2};
constexpr byte PIN_IN2{3};
constexpr byte PIN_BTN{4};
constexpr byte PIN_ALARM{13};
constexpr unsigned int NOTE_F6{1397};
constexpr unsigned int NOTE_A6{1760};
//
// Global objects / variables
//
struct InputState {
enum class state : byte { seconds = 0, minutes };
RotaryEncoder encoder{PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::FOUR3};
#ifndef MINUTES_DEFAULT
const state defaultState{state::seconds};
state lastState{state::minutes};
#else
const state defaultState{state::minutes};
state lastState{state::seconds};
#endif
state currentState{defaultState};
} input;
#ifndef DISPLAY_Y32
U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2(U8G2_R0);
#else
U8G2_SSD1306_128X32_UNIVISION_2_HW_I2C u8g2(U8G2_R0);
#endif
KitchenTimer ktTimer;
AlarmTone alarm{PIN_ALARM};
using namespace Yabl;
Button btn{PIN_BTN};
//
// Forward declaration function(s).
//
KitchenTimerState runTimer(KitchenTimer &);
bool askEncoder(RotaryEncoder &, KitchenTimer &);
bool processInput(KitchenTimer &, InputState &);
void displayTime(KitchenTimer &, bool);
void setDisplayForInput(KitchenTimer &kT, InputState &iS);
void askRtButton(Button &, KitchenTimer &, InputState &);
//////////////////////////////////////////////////////////////////////////////
/// @brief Initialization part of the main program
///
//////////////////////////////////////////////////////////////////////////////
void setup(void) {
Serial.begin(115200);
u8g2.begin();
// u8g2.setFont(u8g2_font_logisoso28_tr);
// u8g2.setFont(u8g2_font_7_Seg_33x19_mn);
// u8g2.setFont(u8g2_font_freedoomr25_mn); // 19 Width 26 Hight
u8g2.setFont(u8g2_font_logisoso42_tn); // 24 Width 51 Hight
btn.begin();
btn.setAutoRelease_ms(1000);
}
//////////////////////////////////////////////////////////////////////////////
/// @brief main program
///
//////////////////////////////////////////////////////////////////////////////
void loop() {
KitchenTimerState ktState{ktTimer.getState()};
switch (ktState) {
case KitchenTimerState::active: runTimer(ktTimer); break;
case KitchenTimerState::off: processInput(ktTimer, input); break;
case KitchenTimerState::alarm:
alarm.playAlarm();
if (btn() != ButtonState::released) { // Switch alarm off with encoder button
setDisplayForInput(ktTimer, input);
}
if (askEncoder(input.encoder, ktTimer)) { // Switch alarm off with encoder rotation
ktTimer.setSeconds(0); // Reset count from rotation
setDisplayForInput(ktTimer, input);
}
break;
}
// If the alarm is active, only the encoder query in the switch instruction may be active.
if (ktState != KitchenTimerState::alarm) { askRtButton(btn, ktTimer, input); }
}
//////////////////////////////////////////////////////////////////////////////
/// @brief The set time is continuously counted down
/// by 1 per second until the value is 0.
///
/// @param kT Reference on kitchen timer object
/// @return KitchenTimerState
//////////////////////////////////////////////////////////////////////////////
KitchenTimerState runTimer(KitchenTimer &kT) {
if (kT(SECOND)) {
--kT;
switch (kT.timeIsUp()) {
case false: kT.start(); break;
case true: kT.setState(KitchenTimerState::alarm); break;
}
displayTime(kT, false);
}
return kT.getState();
}
//////////////////////////////////////////////////////////////////////////////
/// @brief The encoder signals are evaluated
///
/// @param enc Reference on encoder object
/// @param kT Reference on kitchen timer object
/// @return true if the an encoder signal was evaluated
/// @return false if no encoder signal was evaluated
//////////////////////////////////////////////////////////////////////////////
bool askEncoder(RotaryEncoder &enc, KitchenTimer &kT) {
byte flag{true};
enc.tick();
switch (enc.getDirection()) {
case RotaryEncoder::Direction::NOROTATION: flag = false; break;
case RotaryEncoder::Direction::CLOCKWISE: ++kT; break;
case RotaryEncoder::Direction::COUNTERCLOCKWISE: --kT; break;
}
return flag;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Control the inputs and set the input states
///
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
/// @return true when the encoder has been actuated
/// @return false if no encoder operation has occurred
//////////////////////////////////////////////////////////////////////////////
bool processInput(KitchenTimer &kT, InputState &iS) {
bool encoderActuated{false};
if (iS.lastState != iS.currentState) {
switch (iS.currentState) {
case InputState::state::seconds: kT.setUnitSeconds(); break;
case InputState::state::minutes: kT.setUnitMinutes(); break;
}
iS.lastState = iS.currentState;
displayTime(kT, true);
}
if (askEncoder(iS.encoder, ktTimer)) {
switch (ktTimer.getState()) {
case KitchenTimerState::alarm: ktTimer.setState(KitchenTimerState::off); break;
default: displayTime(kT, true); break;
}
encoderActuated = true;
}
return encoderActuated;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Set the correct input status for the display indication
///
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void setDisplayForInput(KitchenTimer &kT, InputState &iS) {
ktTimer.setState(KitchenTimerState::off);
iS.lastState =
(iS.defaultState == InputState::state::seconds) ? InputState::state::minutes : InputState::state::seconds;
iS.currentState = iS.defaultState;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Write the two time units into a string and output the string
/// on the display.
///
/// @param kT Reference on kitchen timer object
/// @param showLine If true, a line will be displayed under the digits active
/// for the input. If false, then no line is displayed.
//////////////////////////////////////////////////////////////////////////////
void displayTime(KitchenTimer &kT, bool showLine) {
char charBuffer[BUFFERLENGTH];
sprintf(charBuffer, "%02d:%02d", kT.getMinutes(), kT.getSeconds());
u8g2.firstPage();
do {
u8g2.drawStr(DISPLAY_X, DISPLAY_Y, charBuffer); // Output string on the display.
if (showLine) {
if (kT.getActiveUnit() == ActiveUnit::seconds) {
u8g2.drawHLine(SECONDS_LINE_X, LINE_Y, LINE_WIDTH);
} else {
u8g2.drawHLine(MINUTES_LINE_X, LINE_Y, LINE_WIDTH);
}
}
} while (u8g2.nextPage());
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Query of the encoder's button function
///
/// @param b Reference on button object
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void askRtButton(Button &b, KitchenTimer &kT, InputState &iS) {
switch (b()) {
// If the button is pressed for a long time, it switches between timer active and timer off.
case ButtonState::released: break;
case ButtonState::pressedLong:
if (!kT.timeIsUp()) { // Switch on timer only if a time iS set.
tone(PIN_ALARM, NOTE_A6, 30);
switch (kT.getState()) {
case KitchenTimerState::active: setDisplayForInput(ktTimer, input); break;
case KitchenTimerState::off:
kT.setState(KitchenTimerState::active);
kT.setUnitSeconds();
displayTime(kT, false); // Delete underline
kT.start(); // Start the countdown
break;
case KitchenTimerState::alarm: break;
}
}
break;
case ButtonState::pressed:
if (kT.getState() == KitchenTimerState::active) { break; }
switch (iS.currentState) {
case InputState::state::seconds:
tone(PIN_ALARM, NOTE_F6, 30);
iS.currentState = InputState::state::minutes;
break;
case InputState::state::minutes:
tone(PIN_ALARM, NOTE_F6, 30);
iS.currentState = InputState::state::seconds;
break;
default: break;
} // inner switch
break;
default: break;
}
}
/*
//////////////////////////////////////////////////////////////////////////////
/// @brief Query of the encoder's button function
///
/// @param b Reference on button object
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void askRtButton(Button &b, KitchenTimer &kT, InputState &iS) {
uint32_t duration;
ButtonState isReleased = b();
if (b.hasNewState() && isReleased) {
duration = b.getDuration_ms();
b.resetDuration();
Serial.println(duration);
} else {
return;
}
switch (duration) {
case 0 ... 699:
if (kT.getState() == KitchenTimerState::active) { break; }
switch (iS.currentState) {
case InputState::state::seconds:
tone(PIN_ALARM, NOTE_F6, 30);
iS.currentState = InputState::state::minutes;
break;
case InputState::state::minutes:
tone(PIN_ALARM, NOTE_F6, 30);
iS.currentState = InputState::state::seconds;
break;
default: break;
} // inner switch
break;
default: // If the button is pressed for a long time, it switches between timer active and timer off.
if (!kT.timeIsUp()) { // Switch on timer only if a time iS set.
tone(PIN_ALARM, NOTE_A6, 30);
switch (kT.getState()) {
case KitchenTimerState::active: setDisplayForInput(ktTimer, input); break;
case KitchenTimerState::off:
kT.setState(KitchenTimerState::active);
kT.setUnitSeconds();
displayTime(kT, false); // Delete underline
kT.start(); // Start the countdown
break;
case KitchenTimerState::alarm: break;
}
}
break;
}
}
*/