diff --git a/src/Main.cpp b/src/Main.cpp index 3d76732..4f911c7 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -4,6 +4,7 @@ WatchFace watchy; void setup() { + Serial.begin(9600); watchy.Init(); } diff --git a/src/Menu.cpp b/src/Menu.cpp new file mode 100644 index 0000000..c4f63bc --- /dev/null +++ b/src/Menu.cpp @@ -0,0 +1,129 @@ +#include "config.h" +#include "Menu.h" +#include +#include + +RTC_DATA_ATTR WatchyDisplay * Menu::m_display = nullptr; +RTC_DATA_ATTR std::function Menu::m_exitCallback = nullptr; +RTC_DATA_ATTR uint8_t Menu::m_numPages = 0, Menu::m_currentPage = 0, Menu::m_numMenuItemsinCurrentPage = 0, Menu::m_currentMenuItem = 0; + +Menu::Menu() +{ + +} + +Menu::~Menu() +{ + +} + +void Menu::Init(WatchyDisplay & display) +{ + m_display = &display; + m_pages.clear(); +} + +void Menu::HandleButtonPress(uint64_t buttonMask) +{ + if (m_currentPage == 0) { + if (buttonMask & BACK_BTN_MASK) { + if (m_exitCallback) { + m_exitCallback(); + } + } + } + + if (m_pages.size() == 0) { + if (m_exitCallback) { + m_exitCallback(); + } + + return; + } + + if (buttonMask & UP_BTN_MASK) { + if (m_currentMenuItem > 0) { + m_currentMenuItem--; + } + } + + if (buttonMask & DOWN_BTN_MASK) { + if (m_currentMenuItem < m_pages[m_currentPage].menuItems.size() - 1) { + m_currentMenuItem++; + } + } + + if (buttonMask & MENU_BTN_MASK) { + if (m_pages[m_currentPage].menuItems[m_currentMenuItem].callback) { + m_pages[m_currentPage].menuItems[m_currentMenuItem].callback(); + } + + if (m_pages[m_currentPage].menuItems[m_currentMenuItem].pageNum > 0) { + m_currentPage = m_pages[m_currentPage].menuItems[m_currentMenuItem].pageNum; + m_currentMenuItem = 0; + } + } +} + +void Menu::SetPages(const std::vector & pages) +{ + // If the number of pages has changed, reset the current page and menu item. + if (pages.size() != m_numPages) { + m_currentPage = 0; + m_currentMenuItem = 0; + } + + // If the number of menu items has changed, reset the current menu item. + if (pages.size() > 0 && pages[m_currentMenuItem].menuItems.size() != m_numMenuItemsinCurrentPage) { + m_currentMenuItem = 0; + } + + m_pages = pages; +} + +void Menu::SetExitCallback(std::function callback) +{ + m_exitCallback = callback; +} + +void Menu::Redraw(bool partialRefresh) +{ + if (m_display == nullptr) { + return; + } + + if (!partialRefresh) { + m_display->clearScreen(); + } + + m_display->setFullWindow(); + m_display->fillScreen(GxEPD_WHITE); + + if (m_pages.size() > 0) { + bool hasTitle = false; + if (m_pages[m_currentPage].title.length() > 0) { + hasTitle = true; + + m_display->setFont(&FreeSansBold9pt7b); + m_display->setTextColor(GxEPD_BLACK); + int16_t x, y; + uint16_t w, h; + + m_display->getTextBounds(m_pages[m_currentPage].title.c_str(), 0, 0, &x, &y, &w, &h); + + if (m_pages[m_currentPage].titleAlignment == ALIGNMENT_LEFT) { // Left + x = 5; + } else if (m_pages[m_currentPage].titleAlignment == ALIGNMENT_CENTER) { // Center + x = (m_display->width() - w) / 2; + } else if (m_pages[m_currentPage].titleAlignment == ALIGNMENT_RIGHT) { // Right + x = m_display->width() - w - 5; + } + + m_display->setCursor(x, 15); + m_display->print(m_pages[m_currentPage].title.c_str()); + m_display->drawLine(5, 25, m_display->width() - 10, 25, GxEPD_BLACK); + } + } + + m_display->display(partialRefresh); +} \ No newline at end of file diff --git a/src/Menu.h b/src/Menu.h new file mode 100644 index 0000000..d947022 --- /dev/null +++ b/src/Menu.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include "WatchyDisplay.h" + +struct MenuItem +{ + std::string title; + // If callback is set, when the menu item is selected, the callback will be called. + // If pageNum is set, when the menu item is selected, the menu will switch to the page with the given pageNum. + std::function callback; + uint8_t pageNum; +}; + +enum MenuTextAlignment { + ALIGNMENT_LEFT, + ALIGNMENT_CENTER, + ALIGNMENT_RIGHT +}; + +struct MenuPage +{ + std::string title; + MenuTextAlignment titleAlignment; + std::string body; + MenuTextAlignment bodyAlignment; + std::vector menuItems; +}; + +class Menu +{ +public: + Menu(); + ~Menu(); + void Init(WatchyDisplay & display); + void HandleButtonPress(uint64_t buttonMask); + void SetPages(const std::vector & pages); + void SetExitCallback(std::function callback); + void Redraw(bool partialRefresh = false); + +private: + + static RTC_DATA_ATTR WatchyDisplay * m_display; + static RTC_DATA_ATTR std::function m_exitCallback; + static RTC_DATA_ATTR uint8_t m_numPages, m_currentPage, m_numMenuItemsinCurrentPage, m_currentMenuItem; + std::vector m_pages; +}; \ No newline at end of file diff --git a/src/WatchFace.cpp b/src/WatchFace.cpp index ce5fcd1..072003b 100644 --- a/src/WatchFace.cpp +++ b/src/WatchFace.cpp @@ -2,8 +2,61 @@ #include "SevenSegment.h" #include -void WatchFace::DrawWatchFace() +RTC_DATA_ATTR bool WatchFace::m_menuSetup = false; +RTC_DATA_ATTR bool WatchFace::m_inMenu = false; +Menu WatchFace::m_menu; + +static const std::vector menuPages = { + { + "Watchy", + ALIGNMENT_CENTER, + "", + ALIGNMENT_CENTER, + { + { + "Sync NTP", + nullptr, + 0 + } + } + } +}; + +void WatchFace::Setup() // Called after hardware is set up { + if (!m_menuSetup) { + m_menuSetup = true; + m_menu.Init(m_display); + } + + m_menu.SetPages(menuPages); + m_menu.SetExitCallback(std::bind(&WatchFace::MenuExited, this)); +} + +void WatchFace::HandleButtonPress(uint64_t buttonMask) +{ + if (m_inMenu) { + m_menu.HandleButtonPress(buttonMask); + } else { + if (buttonMask & MENU_BTN_MASK) { + m_inMenu = true; + m_menu.Redraw(false); + } + } +} + +void WatchFace::DrawWatchFace(bool partialRefresh) +{ + if (m_inMenu) { + return; + } + + m_display.setFullWindow(); + + if (!partialRefresh) { + m_display.clearScreen(); + } + m_display.fillScreen(GxEPD_WHITE); DrawBatteryIcon(); @@ -29,6 +82,8 @@ void WatchFace::DrawWatchFace() m_display.fillRect(87, 90, 5, 5, GxEPD_BLACK); m_display.fillRect(87, 110, 5, 5, GxEPD_BLACK); + + m_display.display(partialRefresh); } void WatchFace::DrawBatteryIcon() @@ -45,4 +100,12 @@ void WatchFace::DrawBatteryIcon() level = 0.5f - sin(asin(1.0f - 2.0f * level) / 3.0f); m_display.fillRect(200 - 44, 9, (int)std::round(35.0f * level), 15, GxEPD_BLACK); +} + +void WatchFace::MenuExited() +{ + if (m_inMenu) { + m_inMenu = false; + DrawWatchFace(false); + } } \ No newline at end of file diff --git a/src/WatchFace.h b/src/WatchFace.h index d406cb8..01197a8 100644 --- a/src/WatchFace.h +++ b/src/WatchFace.h @@ -1,12 +1,20 @@ #pragma once #include "Watchy.h" +#include "Menu.h" class WatchFace : public Watchy { public: - void DrawWatchFace(); + void Setup() override; // Called after hardware is set up + void HandleButtonPress(uint64_t buttonMask) override; + void DrawWatchFace(bool partialRefresh = false) override; private: + RTC_DATA_ATTR static bool m_menuSetup; + RTC_DATA_ATTR static bool m_inMenu; + RTC_DATA_ATTR static Menu m_menu; + void DrawBatteryIcon(); + void MenuExited(); }; \ No newline at end of file diff --git a/src/Watchy.cpp b/src/Watchy.cpp index 9031c08..31f0277 100644 --- a/src/Watchy.cpp +++ b/src/Watchy.cpp @@ -1,7 +1,7 @@ #include "Watchy.h" WatchyDisplayBase Watchy::m_displayBase; -GxEPD2_BW Watchy::m_display(Watchy::m_displayBase); +WatchyDisplay Watchy::m_display(Watchy::m_displayBase); WatchyRTC Watchy::m_RTC; RTC_DATA_ATTR bool g_displayFullInit = true; @@ -22,30 +22,31 @@ void Watchy::Init() m_display.init(0, g_displayFullInit, 10, true); m_display.epd2.setBusyCallback(DisplayBusyCallback); + Setup(); + switch(wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT0: + { // RTC interrupt ShowWatchFace(true); break; + } case ESP_SLEEP_WAKEUP_EXT1: + { // Button press + uint64_t wakeupBit = esp_sleep_get_ext1_wakeup_status(); + HandleButtonPress(wakeupBit); break; + } default: + { ConnectWiFi(); SyncNTPTime(); DisconnectWiFi(); - m_display.clearScreen(0xFF); ShowWatchFace(false); - - // pinMode(VIB_MOTOR_PIN, OUTPUT); - // bool motorOn = false; - // for (int i = 0; i < 4; i++) { - // motorOn = !motorOn; - // digitalWrite(VIB_MOTOR_PIN, motorOn); - // delay(75); - // } break; + } } DeepSleep(); @@ -137,7 +138,5 @@ void Watchy::DisconnectWiFi() void Watchy::ShowWatchFace(bool partialRefresh) { - m_display.setFullWindow(); - DrawWatchFace(); - m_display.display(partialRefresh); + DrawWatchFace(partialRefresh); } \ No newline at end of file diff --git a/src/Watchy.h b/src/Watchy.h index a6cdece..42bdbe7 100644 --- a/src/Watchy.h +++ b/src/Watchy.h @@ -19,13 +19,16 @@ public: void SyncNTPTime(); void DisconnectWiFi(); void ShowWatchFace(bool partialRefresh = false); - - virtual void DrawWatchFace() = 0; + + // Called after hardware is setup + virtual void Setup() = 0; + virtual void HandleButtonPress(uint64_t buttonMask) = 0; + virtual void DrawWatchFace(bool partialRefresh = false) = 0; protected: static void DisplayBusyCallback(const void *); static WatchyDisplayBase m_displayBase; - static GxEPD2_BW m_display; + static WatchyDisplay m_display; static WatchyRTC m_RTC; }; \ No newline at end of file