#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include "Button_SL.hpp"
#include <EEPROM.h>
#include "PwmTimer1.hpp"
// Comment out the following #define if you are using an I2C(HW) Display.
// #define U8G2_SPI_DISPLAY
//
// Global constant definitions
//
constexpr uint8_t PIN_IN1 {2}; // Encoder pin (counter clockwise)
constexpr uint8_t PIN_IN2 {3}; // Encoder pin (clockwise)
constexpr uint8_t PIN_BTN {4}; // Encoder button
constexpr uint8_t MENUTXT_X {20}; // Column = X coordinate Text
constexpr uint8_t MENUTXT_Y {10}; // Row = Y coordinate Text
constexpr uint8_t MENUTXT_Y_OFFSET {17};
constexpr uint8_t MENUCSR_X {5}; // Column = X coordinate cursor
constexpr uint8_t MENUCSR_Y {MENUTXT_Y}; // Row = Y coordinate cursor
constexpr uint8_t MAX_MENU_ITEMS {4}; // Number of menu items
constexpr uint8_t DRAWCOLOR_INVERS {2}; // Inverted text display
constexpr uint8_t DATA_BUFFER_SIZE {4}; // Textbuffersize for data values (Hertz / Dutycycle)
constexpr uint8_t HERTZ {1}; // Default frequency in Hertz
constexpr uint8_t MAX_HERTZ {15}; // Maximum frequency
constexpr uint8_t MIN_HERTZ {1}; // Minimum frequency
constexpr uint8_t DUTYCYCLE {50}; // Default duty cycle
constexpr uint8_t MIN_DUTYCYCLE {0}; // Minimum duty cycle
constexpr uint8_t MAX_DUTYCYCLE {100}; // Maximum duty cycle
// Indicator character forces whether values have already been stored in the EEProm.
constexpr uint8_t INDICATOR_CHAR {'@'};
constexpr int EE_BASE_ADDRESS {0}; // Start address of the data storage area
struct Point {
const uint8_t x;
const uint8_t y;
};
struct PwmData {
const uint8_t ichar; // Indicator character
uint8_t hertz;
const uint8_t minHertz;
const uint8_t maxHertz;
uint8_t dutyCycle;
const uint8_t minDutyCycle;
const uint8_t maxDutyCycle;
};
struct MenuEntryType {
Point coord;
const char *text;
const char *textAction;
};
//
// Data structure for menu display and data input control
//
struct MenuInput {
// Methods
bool askEncoder(uint8_t &, uint8_t, uint8_t);
void resetOnValueChange();
void displayMenue();
// Data
uint8_t curIndex {0};
const uint8_t maxIndex {MAX_MENU_ITEMS - 1};
bool isMenuActive {true};
bool save {false};
bool pwmActive {false};
PwmData pwm {INDICATOR_CHAR, HERTZ, MIN_HERTZ, MAX_HERTZ, DUTYCYCLE, MIN_DUTYCYCLE, MAX_DUTYCYCLE};
#ifndef U8G2_SPI_DISPLAY
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2 {U8G2_R0, U8X8_PIN_NONE}; // Display object
#else
U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2 {U8G2_R0, /* cs=*/10, /* dc=*/7, /* reset=*/8};
#endif
Btn::ButtonSL btn {PIN_BTN};
private:
const Point curCoords[MAX_MENU_ITEMS] {
{MENUCSR_X, MENUCSR_Y },
{MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 1},
{MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 2},
{MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 3}
};
const Point dataCoords[2] {
{MENUTXT_X + 80, MENUCSR_Y },
{MENUTXT_X + 80, MENUCSR_Y + MENUTXT_Y_OFFSET * 1}
};
// clang-format off
MenuEntryType const menuEntryList[MAX_MENU_ITEMS] { // Coordinates and text for the menu display
{{MENUTXT_X, MENUCSR_Y}, "Frequenz :", ""},
{{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 1}, "Tastverh.:", ""},
{{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 2}, "Speichern", "Gespeichert"},
{{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 3}, "Start", "Stopp"}
};
// clang-format on
RotaryEncoder encoder {PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::FOUR3};
char cursor[2] {">"};
char textBuffer[DATA_BUFFER_SIZE];
} mInput;
//
// (Struct) Method for querying the rotary encoder
//
bool MenuInput::askEncoder(uint8_t &value, uint8_t minValue, uint8_t maxValue) {
bool flag {true};
encoder.tick();
switch (encoder.getDirection()) {
case RotaryEncoder::Direction::NOROTATION: flag = false; break;
case RotaryEncoder::Direction::CLOCKWISE: value = (++value > maxValue) ? minValue : value; break;
case RotaryEncoder::Direction::COUNTERCLOCKWISE:
--value;
value = (value < minValue || value > maxValue) ? maxValue : value;
break;
}
return flag;
}
//
// When the frequency/duty cycle value changes, the states for "stored" and "stop" also change.
// Pwm must be switched off
//
void MenuInput::resetOnValueChange() {
save = false;
pwmActive = false;
Pwm.stop();
}
//
// (Struct) Method for Text output on the display
//
void MenuInput::displayMenue() {
u8g2.firstPage();
do {
for (uint8_t i = 0; i <= maxIndex; ++i) {
u8g2.setCursor(menuEntryList[i].coord.x, menuEntryList[i].coord.y);
// Toggle text to match the action
if ((save && i == 2) || (pwmActive && i == 3)) {
u8g2.print(menuEntryList[i].textAction);
} else {
u8g2.print(menuEntryList[i].text);
}
}
u8g2.setCursor(dataCoords[0].x, dataCoords[0].y);
sprintf(textBuffer, "%3d", pwm.hertz);
u8g2.print(textBuffer);
u8g2.setCursor(dataCoords[1].x, dataCoords[1].y);
sprintf(textBuffer, "%3d", pwm.dutyCycle);
u8g2.print(textBuffer);
u8g2.setCursor(curCoords[curIndex].x, curCoords[curIndex].y);
u8g2.print(cursor);
if (!isMenuActive) {
uint8_t charHeight = u8g2.getMaxCharHeight();
u8g2.setDrawColor(DRAWCOLOR_INVERS);
u8g2.drawBox(2, curCoords[curIndex].y - (charHeight - 2), 126, charHeight);
}
} while (u8g2.nextPage());
}
//
// Function forward declarations
//
bool menuControl(MenuInput &);
void readEEPROM(int, PwmData &);
void writeEEPROM(int, PwmData &);
//
// Setup
//
void setup(void) {
// Serial.begin(115200);
mInput.btn.begin();
mInput.btn.setDebounceTime_ms(100);
mInput.u8g2.begin();
mInput.u8g2.setFont(u8g2_font_6x13B_mr);
readEEPROM(EE_BASE_ADDRESS, mInput.pwm);
Pwm.begin(mInput.pwm.hertz, mInput.pwm.dutyCycle);
}
//
// Main Program
//
void loop(void) {
static bool refreshDisplay {true};
if (refreshDisplay) {
mInput.displayMenue();
refreshDisplay = false;
}
// One button press toggles between menu item selection and data entry
if (mInput.isMenuActive && mInput.btn.tick() != Btn::ButtonState::notPressed) {
mInput.isMenuActive = !mInput.isMenuActive;
refreshDisplay = true;
}
switch (mInput.isMenuActive) {
case true: // Menu control
if (mInput.askEncoder(mInput.curIndex, 0, mInput.maxIndex)) { refreshDisplay = true; }
break;
case false: // Data input
if (menuControl(mInput)) { refreshDisplay = true; }
break;
}
}
//
// Data input control
// Returns true if the screen content needs to be refreshed
//
bool menuControl(MenuInput &mI) {
bool screenContentChanged = true;
switch (mI.curIndex) {
case 0: // Menu item frequency (Hertz)
if (mI.askEncoder(mI.pwm.hertz, mI.pwm.minHertz, mI.pwm.maxHertz)) {
mI.resetOnValueChange();
} else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
mI.isMenuActive = true;
} else {
screenContentChanged = false;
}
break;
case 1: // Menu item duty cycle
if (mI.askEncoder(mI.pwm.dutyCycle, mI.pwm.minDutyCycle, mI.pwm.maxDutyCycle)) {
mI.resetOnValueChange();
} else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
mI.isMenuActive = true;
} else {
screenContentChanged = false;
}
break;
case 2: // Menu item save/saved
if (mI.askEncoder(mI.curIndex, 0, mI.maxIndex)) {
mI.isMenuActive = true;
} else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
writeEEPROM(EE_BASE_ADDRESS, mI.pwm);
mI.save = true;
mI.isMenuActive = true;
} else {
screenContentChanged = false;
}
break;
case 3: // Menu item start/stop
if (mI.askEncoder(mI.curIndex, 0, mI.maxIndex)) {
mI.isMenuActive = true;
} else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
mI.pwmActive = !mI.pwmActive;
if (mI.pwmActive) {
Pwm.setHz(mI.pwm.hertz);
Pwm.setDutyCycle(mI.pwm.dutyCycle);
Pwm.start();
} else {
Pwm.stop();
}
mI.isMenuActive = true;
} else {
screenContentChanged = false;
}
break;
}
return screenContentChanged;
}
//
// Reading pwm data from EEProm
//
void readEEPROM(int eeAddr, PwmData &pd) {
if (EEPROM.read(eeAddr) != pd.ichar) { // Executed if no data has ever been saved.
EEPROM.put(eeAddr, pd);
} else {
EEPROM.get(eeAddr, pd); // If data has been saved, read it out.
}
}
//
// Write pwm data to EEProm
//
void writeEEPROM(int eeAddr, PwmData &pd) { EEPROM.put(eeAddr, pd); }