#include <avr/sleep.h>
#include <Streaming.h>
#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include <Button_SL.hpp>
#include <EEPROM.h>
#include "ToneSequence.hpp"
Print& cout {Serial};
namespace gc {
constexpr byte pin_btn {3};
constexpr byte pin_in1 {4};
constexpr byte pin_in2 {5};
constexpr byte pin_signal {13};
constexpr char indicator {'@'}; // Indicator of whether a memory location has ever been written to (must be != 0xFF).
constexpr unsigned int eep_address {0}; // Startaddress for saving data in the eeprom
constexpr unsigned long time_until_sleep {60 * 1000 * 30}; // 30 Seconds
// Used buzzer as a resonance frequency from 2700Hz +- 300Hz
constexpr Note melody[] {
{note::f7, 1000 / 4 },
{0, 1000 / 20},
{note::f7, 1000 / 8 },
{0, 1000 / 20},
{note::f7, 1000 / 4 },
};
} // namespace gc
using MillisType = decltype(millis());
//////////////////////////////////////////////////////////////////////////////
/// \brief Helperclass for a non blocking delay
///
//////////////////////////////////////////////////////////////////////////////
class NbDelay {
public:
void start() { timestamp = millis(); }
boolean operator()(const MillisType duration) { return millis() - timestamp >= duration; }
private:
MillisType timestamp {0};
};
class Interval {
public:
bool operator()(const MillisType duration) {
if (false == is_started) { return start(false); }
return (millis() - timestamp >= duration) ? start(true) : false;
}
void reset() { is_started = false; }
private:
bool start(bool state = false) {
is_started = !state; // Set the value to true on the first call
timestamp = millis();
return state;
}
bool is_started {false}; // Flag = true if the first Operator() call has been made.
MillisType timestamp {0};
};
class SimpleCounter {
public:
SimpleCounter(unsigned int max_counter) : max_counter {max_counter} {}
~SimpleCounter() {}
// ___
// SimpleCounter& operator=(const SimpleCounter& rValue) {
// if (rValue.out_of_range && this != &rValue) {
// rValue.is_count_up ? operator++() : operator--();
// } else {
// out_of_range = false;
// }
// return *this;
// }
// ___
constexpr operator bool() const { return out_of_range; }
constexpr unsigned int operator()() const { return counter; }
unsigned int scMax() const { return max_counter; }
SimpleCounter& operator++() { // pre increment (++x)
is_count_up = true;
++counter;
if (counter >= max_counter) {
counter = 0;
out_of_range = true;
} else {
out_of_range = false;
}
return *this;
}
// post increment (x++): int needed (other method call pattern than operator++()) but not used
const SimpleCounter operator++(int) {
SimpleCounter temp {*this};
operator++();
return temp;
}
SimpleCounter& operator--() { // pre increment (++x)
is_count_up = false;
--counter;
if (counter > max_counter) {
counter = max_counter - 1;
out_of_range = true;
} else {
out_of_range = false;
}
return *this;
}
// post increment (x++): int needed (other method call pattern than operator++()) but not used
const SimpleCounter operator--(int) {
SimpleCounter temp {*this};
operator--();
return temp;
}
private:
unsigned int max_counter;
unsigned int counter {0};
bool out_of_range {false};
bool is_count_up {true};
};
//////////////////////////////////////////////////////////////////////////////
/// \brief Enum for a state machine
///
//////////////////////////////////////////////////////////////////////////////
enum class State : byte { Input, Check, Run, Alarm };
constexpr byte toByte(State s) noexcept { return static_cast<byte>(s); }
//////////////////////////////////////////////////////////////////////////////
/// \brief Structure for representing minutes and seconds with the help
/// of SimpleCounter objects.
///
//////////////////////////////////////////////////////////////////////////////
struct Timer {
Timer(unsigned int max_m = 0, unsigned int max_s = 0) : minutes {max_m}, seconds {max_s} {}
// bool isZero() { return not(minutes() + seconds() > 0); }
bool isZero() { return minutes() + seconds() == 0; }
unsigned long sumSeconds() { return minutes() * seconds.scMax() + seconds(); }
SimpleCounter minutes;
SimpleCounter seconds;
};
//////////////////////////////////////////////////////////////////////////////
/// \brief Coordinates on a display.
///
//////////////////////////////////////////////////////////////////////////////
struct Pos {
int x;
int y;
};
//////////////////////////////////////////////////////////////////////////////
/// \brief Auxiliary structure for displaying text on a display.
///
//////////////////////////////////////////////////////////////////////////////
struct Row {
Pos pos;
char text[6];
};
//
// ----------------- Class / struct definitions end ------------------------------------------------
//
//
// Global variables
//
Row rows[] {
{{18, 28}, "00:00"},
{{18, 60}, "00:00"},
};
Timer time[2] {
{60, 60},
{60, 60}
};
using Display = U8G2_SSD1306_128X64_NONAME_1_HW_I2C;
Display u8g2(U8G2_R0);
Interval interval;
NbDelay wait_for_sleep;
Btn::ButtonSL btn {gc::pin_btn};
RotaryEncoder encoder {gc::pin_in1, gc::pin_in2, RotaryEncoder::LatchMode::FOUR3};
ToneSequence<gc::pin_signal> signal;
//////////////////////////////////////////////////////////////////////////////
/// \brief Check if a button has been short or long pressed
///
/// \param b Reference to Button Object
/// \param value Marker for how often the button was pressed.
/// \param max Upper value of button presses. Once this is reached, it is reset to zero.
/// \return byte value
//////////////////////////////////////////////////////////////////////////////
byte askButton(Btn::ButtonSL& b, byte value, byte max) {
switch (b.tick()) {
case Btn::ButtonState::shortPressed: ((value + 1) > max) ? value = 1 : ++value; break;
case Btn::ButtonState::longPressed: value = 0; break;
default: break;
}
return value;
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Set the Timer object
///
/// \tparam N Number of timer objects
/// \param enc Reference to encoder object
/// \param t Reference to timer object array
/// \param marker variable for determining the index to the timer object that is to be changed.
/// \return true when the encoder has been actuated.
/// \return false When the encoder has not been actuated.
//////////////////////////////////////////////////////////////////////////////
template <size_t N> bool setTime(RotaryEncoder& enc, Timer (&t)[N], byte marker) {
if (!marker) { return false; }
// determine timer index from marker value. Two marker values per timer.
decltype(marker) idx = (marker < 3) ? 0 : 1;
enc.tick();
switch (enc.getDirection()) {
case RotaryEncoder::Direction::CLOCKWISE:
if (!(marker % 2)) {
++t[idx].minutes;
} else {
++t[idx].seconds && ++t[idx].minutes;
}
return true;
case RotaryEncoder::Direction::COUNTERCLOCKWISE:
if (!(marker % 2)) {
--t[idx].minutes;
} else {
--t[idx].seconds && --t[idx].minutes;
}
return true;
default: break;
}
return false;
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Determine the screen position of the marker line for
/// the active time object.
///
/// \param r row
/// \param width of indicator
/// \param marker to determine the screen position for the marker line
/// \return Pos Position of marker line on the screen
//////////////////////////////////////////////////////////////////////////////
inline Pos markerPos(const Row* r, byte width, byte marker) {
switch (marker) {
case 1: return {r[0].pos.x + width * 3, r[0].pos.y + 1}; // Seconds line 1
case 2: return {r[0].pos.x, r[0].pos.y + 1}; // Minutes line 1
case 3: return {r[1].pos.x + width * 3, r[1].pos.y + 1}; // Seconds line 2
case 4: return {r[1].pos.x, r[1].pos.y + 1}; // Minutes line 2
}
return {0, 0};
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Display all data on the screen
///
/// \tparam N Number of Elements in the given arrays (row and time objekts)
/// \param dp Reference to display object
/// \param r Reference to row object array
/// \param t Reference to timer object array
/// \param marker indicator for actual choosen timer object
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void displayTime(Display& dp, Row (&r)[N], const Timer (&t)[N], byte marker = 0) {
sprintf(r[0].text, "%02d:%02d", t[0].minutes(), t[0].seconds());
sprintf(r[1].text, "%02d:%02d", t[1].minutes(), t[1].seconds());
dp.firstPage();
do {
dp.drawStr(r[0].pos.x, r[0].pos.y, r[0].text);
dp.drawStr(r[1].pos.x, r[1].pos.y, r[1].text);
if (marker) {
auto width {dp.getMaxCharWidth()};
auto mPos = markerPos(r, width, marker);
dp.drawHLine(mPos.x, mPos.y, width * 2);
}
} while (dp.nextPage());
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Reading data from eeprom
///
/// \param eeAddr flash address.
/// \param indicator Indicator of whether data has ever been stored.
/// \param t Timer data to be read.
//////////////////////////////////////////////////////////////////////////////
void readEEPROM(unsigned int eeAddr, char indicator, Timer& t) {
if (EEPROM.read(eeAddr) != indicator) { // Executed if no data has ever been saved.
EEPROM.put(eeAddr, indicator);
EEPROM.put(eeAddr + 1, t);
} else {
EEPROM.get(eeAddr + 1, t); // If data has been saved, read it out.
}
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Write pwm data to EEProm
///
/// \param eeAddr flash address.
/// \param indicator Indicator of whether data has ever been stored.
/// \param t Timer data to be saved.
//////////////////////////////////////////////////////////////////////////////
void writeEEPROM(unsigned int eeAddr, char indicator, Timer& t) {
EEPROM.put(eeAddr, indicator);
EEPROM.put(eeAddr + 1, t); // eeAddr + 1 -> don't override indicator char
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Interrupt service routine for wake up
///
//////////////////////////////////////////////////////////////////////////////
void intWakeup() { detachInterrupt(gc::pin_btn); }
//////////////////////////////////////////////////////////////////////////////
/// \brief Start (and stop) the sleep mode
///
/// \param wakeupPin Number of the wake up interrupt pin
//////////////////////////////////////////////////////////////////////////////
void powerDown(byte wakeupPin, Display& dp) {
// go into deep Sleep
attachInterrupt(digitalPinToInterrupt(wakeupPin), intWakeup, LOW);
dp.setPowerSave(true);
delay(20);
sleep_cpu(); // sleep
// switch anything on
delay(20);
dp.setPowerSave(false);
}
// Sleepmode not supported by wokwi!!!!!!
//
// Main program
//
void setup() {
// Serial.begin(115200);
u8g2.begin();
// u8g2.setBusClock(400000);
u8g2.setFont(u8g2_font_logisoso26_tn); // 16 Width 32 Height
btn.begin();
btn.releaseOn();
displayTime(u8g2, rows, time, 1);
wait_for_sleep.start();
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Set sleep mode to POWER DOWN mode
sleep_enable(); // Enable sleep mode, but not yet
}
void loop() {
constexpr byte max_marker {4}; // maximum value vor marker. The value 0 indicates a long button press.
static byte marker {1}; // Indicator for which time is to be set (minute/second per timer).
static Timer save_time[2]; // Timer array / only two timers are possible.
static size_t idx {0}; // Index for timer array.
static auto state {State::Input}; // initial state for the state machine
static auto is_signal {false}; // Flag for whether an acoustic signal should be emitted or not.
byte new_marker = askButton(btn, marker, max_marker);
// new_marker != 0 then Button short pressed. If clock is running stop it.
// If the clock is not runnig set marker line to the next position.
// new_marker == 0 then Button is long pressed -> start Clock.
if (new_marker && state == State::Run) {
// count down has been stopped. Recover the initial time values for display.
wait_for_sleep.start(); // Start wait for sleep mode. Becomes active if no actions will be done.
time[0] = save_time[0];
time[1] = save_time[1];
state = State::Input;
idx = 0; // Set index to the first timer
is_signal = signal.stop(); // signal.stop() returns false = signal off
} else if (!new_marker && state != State::Run) {
// Start count down if button was long pressed (new_marker = 0). Save the set time values into the flash memory.
// If the time values have not been adjusted, nothing is written to the memory.
writeEEPROM(gc::eep_address, gc::indicator, time[0]);
writeEEPROM(gc::eep_address + sizeof(Timer) + 1, gc::indicator, time[1]);
state = State::Check;
}
// set time by rotating the encoder
if (setTime(encoder, time, marker) || new_marker != marker) {
marker = new_marker;
wait_for_sleep.start();
displayTime(u8g2, rows, time, marker);
}
if (is_signal) { is_signal = signal(gc::melody); } // plays a melody as long as signal returns true
switch (state) {
case State::Check:
if (time[0].isZero() && time[1].isZero()) {
state = State::Input;
marker = 1;
displayTime(u8g2, rows, time, marker);
break; // no timevalues to count down -> quit Check, don't Run anything.
}
// Safe initial time values for the next Run
save_time[0] = time[0];
save_time[1] = time[1];
if (time[0].isZero()) { idx = 1; }
state = State::Run;
// tone(gc::pin_signal, note::as7, 125); // 1000ms / 8 = 125 (eighth note)
tone(gc::pin_signal, note::f7, 125); // 1000ms / 8 = 125 (eighth note)
delay(125);
[[fallthrough]];
case State::Run:
if (interval(1000)) {
!time[idx].minutes() ? static_cast<bool>(--time[idx].seconds) : --time[idx].seconds && --time[idx].minutes;
displayTime(u8g2, rows, time, marker);
if (time[idx].isZero()) { // Timer is zero -> switch to the next timer
is_signal = signal.reset(); // signal.reset() returns true = signal on
time[idx] = save_time[idx]; // recover time value for the next Run.
decltype(idx) tmp_idx = (idx + 1) % 2;
idx = (!save_time[tmp_idx].isZero()) ? tmp_idx : idx;
}
}
break;
case State::Input:
// Put the microcontroller into deep sleep if no actions are performed.
if (wait_for_sleep(gc::time_until_sleep)) {
pinMode(gc::pin_signal, OUTPUT); // Saves power
powerDown(gc::pin_btn, u8g2); // Go to deep sleep here
wait_for_sleep.start(); // Start timer so that the display does not go off immediately after wake up
// A delay so that the minute/second changeover is not triggered immediately after waking up.
delay(200);
}
break;
default: break;
}
}