commit 62bcff84855e8cea08a239aa8ea383280162bacd Author: Lewis Jackson <> Date: Mon May 29 13:03:05 2023 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8da6879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.pio +.vscode/** diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..76172bd --- /dev/null +++ b/platformio.ini @@ -0,0 +1,23 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:watchy] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + adafruit/Adafruit GFX Library@^1.11.5 + GxEPD2 + Time + Rtc_Pcf8563 + NTPClient + +lib_ldf_mode = deep+ +board_build.partitions = min_spiffs.csv diff --git a/src/Main.cpp b/src/Main.cpp new file mode 100644 index 0000000..3d76732 --- /dev/null +++ b/src/Main.cpp @@ -0,0 +1,12 @@ +#include "WatchFace.h" + +WatchFace watchy; + +void setup() +{ + watchy.Init(); +} + +void loop() +{ +} \ No newline at end of file diff --git a/src/SevenSegment.cpp b/src/SevenSegment.cpp new file mode 100644 index 0000000..71002a0 --- /dev/null +++ b/src/SevenSegment.cpp @@ -0,0 +1,76 @@ +#include "SevenSegment.h" + +SevenSegment::SevenSegment(uint16_t digitWidth, uint16_t digitHeight, uint16_t digitGap, uint16_t segmentThickness, uint16_t segmentGap) + : m_digitWidth(digitWidth), m_digitHeight(digitHeight), m_digitGap(digitGap), m_segmentThickness(segmentThickness), m_segmentGap(segmentGap) +{ +} + +void SevenSegment::DrawDigit(GxEPD2_BW & display, uint8_t digit, uint16_t x, uint16_t y, uint8_t color) +{ + uint16_t digitX = x; + uint16_t digitY = y; + + // top top-right bottom-right bottom bottom-left top-left middle + + bool drawSegments0[7] = { true, true, true, true, true, true, false }; + bool drawSegments1[7] = { false, true, true, false, false, false, false }; + bool drawSegments2[7] = { true, true, false, true, true, false, true }; + bool drawSegments3[7] = { true, true, true, true, false, false, true }; + bool drawSegments4[7] = { false, true, true, false, false, true, true }; + bool drawSegments5[7] = { true, false, true, true, false, true, true }; + bool drawSegments6[7] = { false, false, true, true, true, true, true }; + bool drawSegments7[7] = { true, true, true, false, false, false, false }; + bool drawSegments8[7] = { true, true, true, true, true, true, true }; + bool drawSegments9[7] = { true, true, true, true, false, true, true }; + bool * drawSegments[10] = { drawSegments0, drawSegments1, drawSegments2, drawSegments3, drawSegments4, drawSegments5, drawSegments6, drawSegments7, drawSegments8, drawSegments9 }; + bool * segments = drawSegments[digit]; + + // A + if (segments[0]) { + display.fillRect(digitX + m_segmentThickness, digitY, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color); + } else { + display.fillRect(digitX + m_segmentThickness, digitY, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color ^ 0xFF); + } + + // B + if (segments[1]) { + display.fillRect(digitX + m_digitWidth - m_segmentThickness, digitY + m_segmentThickness, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color); + } else { + display.fillRect(digitX + m_digitWidth - m_segmentThickness, digitY + m_segmentThickness, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color ^ 0xFF); + } + + // C + if (segments[2]) { + display.fillRect(digitX + m_digitWidth - m_segmentThickness, digitY + m_digitHeight / 2, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color); + } else { + display.fillRect(digitX + m_digitWidth - m_segmentThickness, digitY + m_digitHeight / 2, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color ^ 0xFF); + } + + // D + if (segments[3]) { + display.fillRect(digitX + m_segmentThickness, digitY + m_digitHeight - m_segmentThickness * 2, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color); + } else { + display.fillRect(digitX + m_segmentThickness, digitY + m_digitHeight - m_segmentThickness * 2, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color ^ 0xFF); + } + + // E + if (segments[4]) { + display.fillRect(digitX, digitY + m_digitHeight / 2, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color); + } else { + display.fillRect(digitX, digitY + m_digitHeight / 2, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color ^ 0xFF); + } + + // F + if (segments[5]) { + display.fillRect(digitX, digitY + m_segmentThickness, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color); + } else { + display.fillRect(digitX, digitY + m_segmentThickness, m_segmentThickness, m_digitHeight / 2 - m_segmentThickness * 2, color ^ 0xFF); + } + + // G + if (segments[6]) { + display.fillRect(digitX + m_segmentThickness, digitY + m_digitHeight / 2 - m_segmentThickness, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color); + } else { + display.fillRect(digitX + m_segmentThickness, digitY + m_digitHeight / 2 - m_segmentThickness, m_digitWidth - 2 * m_segmentThickness, m_segmentThickness, color ^ 0xFF); + } +} \ No newline at end of file diff --git a/src/SevenSegment.h b/src/SevenSegment.h new file mode 100644 index 0000000..840cdff --- /dev/null +++ b/src/SevenSegment.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "WatchyDisplay.h" + +class SevenSegment +{ +public: + SevenSegment(uint16_t digitWidth, uint16_t digitHeight, uint16_t digitGap, uint16_t segmentThickness, uint16_t segmentGap); + void DrawDigit(GxEPD2_BW & display, uint8_t digit, uint16_t x, uint16_t y, uint8_t color); + +private: + uint16_t m_digitWidth, m_digitHeight, m_digitGap, m_segmentThickness, m_segmentGap; +}; \ No newline at end of file diff --git a/src/WatchFace.cpp b/src/WatchFace.cpp new file mode 100644 index 0000000..bf9c404 --- /dev/null +++ b/src/WatchFace.cpp @@ -0,0 +1,50 @@ +#include "WatchFace.h" +#include "SevenSegment.h" +#include + +void WatchFace::DrawWatchFace() +{ + m_display.fillScreen(GxEPD_WHITE); + DrawBatteryIcon(); + + tmElements_t currentTime; + m_RTC.read(currentTime, TZ_OFFSET); + SevenSegment sevenSegment(30, 60, 6, 5, 5); + + if (currentTime.Hour < 10) { + sevenSegment.DrawDigit(m_display, 0, 10, 75, GxEPD_BLACK); + } else { + sevenSegment.DrawDigit(m_display, currentTime.Hour / 10, 10, 75, GxEPD_BLACK); + } + + sevenSegment.DrawDigit(m_display, currentTime.Hour % 10, 50, 75, GxEPD_BLACK); + + if (currentTime.Minute < 10) { + sevenSegment.DrawDigit(m_display, 0, 100, 75, GxEPD_BLACK); + } else { + sevenSegment.DrawDigit(m_display, currentTime.Minute / 10, 100, 75, GxEPD_BLACK); + } + + sevenSegment.DrawDigit(m_display, currentTime.Minute % 10, 140, 75, GxEPD_BLACK); + + m_display.fillRect(87, 90, 5, 5, GxEPD_BLACK); + m_display.fillRect(87, 110, 5, 5, GxEPD_BLACK); + +// sevenSegment.DrawDigit(m_display, 9, 10, 75, GxEPD_BLACK); +} + +void WatchFace::DrawBatteryIcon() +{ + m_display.fillRect(200 - 48, 5, 43, 23, GxEPD_BLACK); + m_display.fillRect(200 - 46, 7, 39, 19, GxEPD_WHITE); + float VBAT = GetBatteryVoltage(); + float level = (VBAT - 3.6f) / 0.6f; + if (level > 1.0f) { + level = 1.0f; + } else if (level < 0.0f) { + level = 0.0f; + } + 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); +} \ No newline at end of file diff --git a/src/WatchFace.h b/src/WatchFace.h new file mode 100644 index 0000000..d406cb8 --- /dev/null +++ b/src/WatchFace.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Watchy.h" + +class WatchFace : public Watchy +{ +public: + void DrawWatchFace(); + +private: + void DrawBatteryIcon(); +}; \ No newline at end of file diff --git a/src/Watchy.cpp b/src/Watchy.cpp new file mode 100644 index 0000000..9031c08 --- /dev/null +++ b/src/Watchy.cpp @@ -0,0 +1,143 @@ +#include "Watchy.h" + +WatchyDisplayBase Watchy::m_displayBase; +GxEPD2_BW Watchy::m_display(Watchy::m_displayBase); +WatchyRTC Watchy::m_RTC; + +RTC_DATA_ATTR bool g_displayFullInit = true; + +Watchy::Watchy() +{ +} + +void Watchy::Init() +{ + esp_sleep_wakeup_cause_t wakeup_reason; + wakeup_reason = esp_sleep_get_wakeup_cause(); + + Wire.begin(SDA, SCL); + m_RTC.init(); + + m_display.epd2.selectSPI(SPI, SPISettings(20000000, MSBFIRST, SPI_MODE0)); + m_display.init(0, g_displayFullInit, 10, true); + m_display.epd2.setBusyCallback(DisplayBusyCallback); + + switch(wakeup_reason) { + case ESP_SLEEP_WAKEUP_EXT0: + // RTC interrupt + ShowWatchFace(true); + break; + case ESP_SLEEP_WAKEUP_EXT1: + // Button press + 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(); +} + +void Watchy::DeepSleep() +{ + m_display.hibernate(); + if (g_displayFullInit) { // For some reason, seems to be enabled on first boot + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + } + + g_displayFullInit = false; // Notify not to init it again + m_RTC.clearAlarm(); // resets the alarm flag in the RTC + + // Set GPIOs 0-39 to input to avoid power leaking out + const uint64_t ignore = 0b11110001000000110000100111000010; // Ignore some GPIOs due to resets + for (int i = 0; i < GPIO_NUM_MAX; i++) { + if ((ignore >> i) & 0b1) + continue; + pinMode(i, INPUT); + } + esp_sleep_enable_ext0_wakeup((gpio_num_t)RTC_INT_PIN, + 0); // enable deep sleep wake on RTC interrupt + esp_sleep_enable_ext1_wakeup( + BTN_PIN_MASK, + ESP_EXT1_WAKEUP_ANY_HIGH); // enable deep sleep wake on button press + esp_deep_sleep_start(); +} + +void Watchy::DisplayBusyCallback(const void *) +{ + gpio_wakeup_enable((gpio_num_t)DISPLAY_BUSY, GPIO_INTR_LOW_LEVEL); + esp_sleep_enable_gpio_wakeup(); + esp_light_sleep_start(); +} + +void Watchy::VibeMotor(uint8_t intervalMs, uint8_t length) +{ + pinMode(VIB_MOTOR_PIN, OUTPUT); + bool motorOn = false; + for (int i = 0; i < length; i++) { + motorOn = !motorOn; + digitalWrite(VIB_MOTOR_PIN, motorOn); + delay(intervalMs); + } +} + +float Watchy::GetBatteryVoltage() +{ + return analogReadMilliVolts(BATT_ADC_PIN) / 1000.0f * 2.0f; +} + +void Watchy::ConnectWiFi() +{ + if(WiFi.begin(WIFI_SSID, WIFI_PASS) == WL_CONNECT_FAILED) { + Serial.begin(9600); + Serial.println("Failed to connect to WiFi"); + return; + } + + if(WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.begin(9600); + Serial.println("Failed to connect to WiFi"); + return; + } +} + +void Watchy::SyncNTPTime() +{ + WiFiUDP ntpUDP; + // GMT offset should be, RTC class will adjust to local time + NTPClient timeClient(ntpUDP, "pool.ntp.org", 0); + timeClient.begin(); + bool success = timeClient.forceUpdate(); + if (!success) { + Serial.begin(9600); + Serial.println("Failed to get NTP time"); + } + tmElements_t tm; + breakTime((time_t)timeClient.getEpochTime(), tm); + m_RTC.set(tm); +} + +void Watchy::DisconnectWiFi() +{ + WiFi.mode(WIFI_OFF); +} + +void Watchy::ShowWatchFace(bool partialRefresh) +{ + m_display.setFullWindow(); + DrawWatchFace(); + m_display.display(partialRefresh); +} \ No newline at end of file diff --git a/src/Watchy.h b/src/Watchy.h new file mode 100644 index 0000000..a6cdece --- /dev/null +++ b/src/Watchy.h @@ -0,0 +1,31 @@ +#pragma once + +#include "config.h" +#include "WatchyDisplay.h" +#include "WatchyRTC.h" +#include +#include +#include + +class Watchy +{ +public: + Watchy(); + void Init(); + void DeepSleep(); + void VibeMotor(uint8_t intervalMs = 100, uint8_t length = 20); + float GetBatteryVoltage(); + void ConnectWiFi(); + void SyncNTPTime(); + void DisconnectWiFi(); + void ShowWatchFace(bool partialRefresh = false); + + virtual void DrawWatchFace() = 0; + +protected: + static void DisplayBusyCallback(const void *); + + static WatchyDisplayBase m_displayBase; + static GxEPD2_BW m_display; + static WatchyRTC m_RTC; +}; \ No newline at end of file diff --git a/src/WatchyDisplay.cpp b/src/WatchyDisplay.cpp new file mode 100644 index 0000000..2bfb116 --- /dev/null +++ b/src/WatchyDisplay.cpp @@ -0,0 +1,357 @@ +#include "WatchyDisplay.h" + +WatchyDisplayBase::WatchyDisplayBase() + : GxEPD2_EPD(DISPLAY_CS, DISPLAY_DC, DISPLAY_RES, DISPLAY_BUSY, HIGH, 10000000, DISPLAY_WIDTH, DISPLAY_HEIGHT, panel, false, true, true) +{ +} + +// init controller memory and screen (default white) +void WatchyDisplayBase::clearScreen(uint8_t value) +{ + writeScreenBuffer(value); + refresh(true); + + if (!_using_partial_mode) { + InitPart(); + } + + _startTransfer(); + TransferCommand(0x24); + for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { + _transfer(value); + } + _endTransfer(); + + // Is this necessary? Come back to this - FIXME + _startTransfer(); + TransferCommand(0x26); + for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { + _transfer(value); + } + _endTransfer(); + _initial_write = false; // initial full screen buffer clean done +} + +// init controller memory (default white) +void WatchyDisplayBase::writeScreenBuffer(uint8_t value) +{ + if (!_using_partial_mode) { + InitPart(); + } + + if (_initial_write) { + _startTransfer(); + TransferCommand(0x26); + + for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { + _transfer(value); + } + + _endTransfer(); + + _initial_write = false; + } +} + +void WatchyDisplayBase::writeImage(const uint8_t * bitmap, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) +{ + if (_initial_write) { + if (!_using_partial_mode) { + InitPart(); + } + + writeScreenBuffer(0x24); // initial full screen buffer clean + _initial_write = false; + } +#if defined(ESP8266) || defined(ESP32) + yield(); // avoid wdt +#endif + int16_t wb = (w + 7) / 8; // width bytes, bitmaps are padded + x -= x % 8; // byte boundary + w = wb * 8; // byte boundary + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) { + return; + } + + if (!_using_partial_mode) { + InitPart(); + } + + SetPartialRamArea(x1, y1, w1, h1); + _startTransfer(); + TransferCommand(0x24); + + for (int16_t i = 0; i < h1; i++) { + for (int16_t j = 0; j < w1 / 8; j++) { + uint8_t data; + // use wb, h of bitmap for index! + int16_t idx = mirror_y ? j + dx / 8 + ((h - 1 - (i + dy))) * wb : j + dx / 8 + (i + dy) * wb; + if (pgm) { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&bitmap[idx]); +#else + data = bitmap[idx]; +#endif + } else { + data = bitmap[idx]; + } + + if (invert) { + data = ~data; + } + + _transfer(data); + } + } + _endTransfer(); +#if defined(ESP8266) || defined(ESP32) + yield(); // avoid wdt +#endif +} + +// screen refresh from controller memory, partial screen +void WatchyDisplayBase::writeImagePart(const uint8_t * bitmap, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) +{ + if (_initial_write) { + writeScreenBuffer(); // initial full screen buffer clean + } +#if defined(ESP8266) || defined(ESP32) + yield(); // avoid wdt +#endif + if ((w_bitmap < 0) || (h_bitmap < 0) || (w < 0) || (h < 0)) return; + if ((x_part < 0) || (x_part >= w_bitmap)) return; + if ((y_part < 0) || (y_part >= h_bitmap)) return; + int16_t wb_bitmap = (w_bitmap + 7) / 8; // width bytes, bitmaps are padded + x_part -= x_part % 8; // byte boundary + w = w_bitmap - x_part < w ? w_bitmap - x_part : w; // limit + h = h_bitmap - y_part < h ? h_bitmap - y_part : h; // limit + x -= x % 8; // byte boundary + w = 8 * ((w + 7) / 8); // byte boundary, bitmaps are padded + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) { + return; + } + if (!_using_partial_mode) { + InitPart(); + } + SetPartialRamArea(x1, y1, w1, h1); + _startTransfer(); + TransferCommand(0x24); + for (int16_t i = 0; i < h1; i++) + { + for (int16_t j = 0; j < w1 / 8; j++) + { + uint8_t data; + // use wb_bitmap, h_bitmap of bitmap for index! + int16_t idx = mirror_y ? x_part / 8 + j + dx / 8 + ((h_bitmap - 1 - (y_part + i + dy))) * wb_bitmap : x_part / 8 + j + dx / 8 + (y_part + i + dy) * wb_bitmap; + if (pgm) + { +#if defined(__AVR) || defined(ESP8266) || defined(ESP32) + data = pgm_read_byte(&bitmap[idx]); +#else + data = bitmap[idx]; +#endif + } + else + { + data = bitmap[idx]; + } + if (invert) data = ~data; + _transfer(data); + } + } + _endTransfer(); +#if defined(ESP8266) || defined(ESP32) + yield(); // avoid wdt +#endif +} + +// screen refresh from controller memory to full screen +void WatchyDisplayBase::refresh(bool partial_update_mode) +{ + if (partial_update_mode) { + refresh(0, 0, WIDTH, HEIGHT); + } else { + if (_using_partial_mode) { + InitFull(); + } + + UpdateFull(); + _initial_refresh = false; // initial full update done + } +} + +void WatchyDisplayBase::refresh(int16_t x, int16_t y, int16_t w, int16_t h) +{ + if (_initial_refresh) { + return refresh(false); // initial update needs be full update + } + + // intersection with screen + int16_t w1 = x < 0 ? w + x : w; // reduce + int16_t h1 = y < 0 ? h + y : h; // reduce + int16_t x1 = x < 0 ? 0 : x; // limit + int16_t y1 = y < 0 ? 0 : y; // limit + w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit + h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit + if ((w1 <= 0) || (h1 <= 0)) { + return; + } + + // make x1, w1 multiple of 8 + w1 += x1 % 8; + if (w1 % 8 > 0) w1 += 8 - w1 % 8; + x1 -= x1 % 8; + if (!_using_partial_mode) { + InitPart(); + } + + SetPartialRamArea(x1, y1, w1, h1); + UpdatePart(); +} + + +void WatchyDisplayBase::powerOff() +{ + if (_power_is_on) { + _startTransfer(); + TransferCommand(0x22); + _transfer(0x83); + TransferCommand(0x20); + _endTransfer(); + _waitWhileBusy("_PowerOff", power_off_time); + } + + _power_is_on = false; + _using_partial_mode = false; +} + +void WatchyDisplayBase::hibernate() +{ + if (_rst >= 0) { + _writeCommand(0x10); // deep sleep mode + _writeData(0x1); // enter deep sleep + _hibernating = true; + } +} + +void WatchyDisplayBase::powerOn() +{ + if (!_power_is_on) { + _startTransfer(); + TransferCommand(0x22); + _transfer(0xf8); + TransferCommand(0x20); + _endTransfer(); + _waitWhileBusy("_PowerOn", power_on_time); + } + + _power_is_on = true; +} + +void WatchyDisplayBase::InitFull() +{ + InitDisplay(); + powerOn(); + _using_partial_mode = false; +} + +void WatchyDisplayBase::InitPart() +{ + InitDisplay(); + powerOn(); + _using_partial_mode = true; +} + +void WatchyDisplayBase::InitDisplay() +{ + if (_hibernating) { + _reset(); + } + + _writeCommand(0x12); // soft reset + _waitWhileBusy("_SoftReset", 10); // 10ms max according to specs + + _startTransfer(); + TransferCommand(0x01); // Driver output control + _transfer(0xC7); + _transfer(0x00); + _transfer(0x00); + TransferCommand(0x3C); // BorderWavefrom + _transfer(darkBorder ? 0x02 : 0x05); + TransferCommand(0x18); // Read built-in temperature sensor + _transfer(0x80); + _endTransfer(); + + SetPartialRamArea(0, 0, WIDTH, HEIGHT); +} + +void WatchyDisplayBase::UpdateFull() +{ + _startTransfer(); + TransferCommand(0x22); + _transfer(0xf4); + TransferCommand(0x20); + _endTransfer(); + _waitWhileBusy("_Update_Full", full_refresh_time); +} + +void WatchyDisplayBase::UpdatePart() +{ + _startTransfer(); + TransferCommand(0x22); + //_transfer(0xcc); // skip temperature load (-5ms) + _transfer(0xfc); + TransferCommand(0x20); + _endTransfer(); + _waitWhileBusy("_Update_Part", partial_refresh_time); +} + +void WatchyDisplayBase::TransferCommand(uint8_t value) +{ + if (_dc >= 0) { + digitalWrite(_dc, LOW); + } + + SPI.transfer(value); + + if (_dc >= 0) { + digitalWrite(_dc, HIGH); + } +} + +void WatchyDisplayBase::SetPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + _startTransfer(); + TransferCommand(0x11); // set ram entry mode + _transfer(0x03); // x increase, y increase : normal mode + TransferCommand(0x44); + _transfer(x / 8); + _transfer((x + w - 1) / 8); + TransferCommand(0x45); + _transfer(y % 256); + _transfer(y / 256); + _transfer((y + h - 1) % 256); + _transfer((y + h - 1) / 256); + TransferCommand(0x4e); + _transfer(x / 8); + TransferCommand(0x4f); + _transfer(y % 256); + _transfer(y / 256); + _endTransfer(); +} \ No newline at end of file diff --git a/src/WatchyDisplay.h b/src/WatchyDisplay.h new file mode 100644 index 0000000..a409654 --- /dev/null +++ b/src/WatchyDisplay.h @@ -0,0 +1,50 @@ +#pragma once + +#include "config.h" +#include +#include + +class WatchyDisplayBase : public GxEPD2_EPD +{ +public: + static const uint16_t WIDTH = DISPLAY_WIDTH; + static const uint16_t HEIGHT = DISPLAY_HEIGHT; + static const uint16_t WIDTH_VISIBLE = WIDTH; + + WatchyDisplayBase(); + + // init controller memory and screen (default white) + void clearScreen(uint8_t value = 0xFF) override; + + // init controller memory (default white) + void writeScreenBuffer(uint8_t value = 0xFF) override; + void writeImage(const uint8_t * bitmap, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) override; + + // screen refresh from controller memory, partial screen + void writeImagePart(const uint8_t * bitmap, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) override; + + // screen refresh from controller memory to full screen + void refresh(bool partial_update_mode = false) override; + void refresh(int16_t x, int16_t y, int16_t w, int16_t h) override; + void powerOff() override; + void hibernate() override; + + void powerOn(); + + static const GxEPD2::Panel panel = GxEPD2::GDEH0154D67; + +private: + static const uint16_t power_on_time = 100; // ms, e.g. 95583us + static const uint16_t power_off_time = 150; // ms, e.g. 140621us + static const uint16_t full_refresh_time = 2600; // ms, e.g. 2509602us + static const uint16_t partial_refresh_time = 500; // ms, e.g. 457282us + static const bool darkBorder = false; + + void InitFull(); + void InitPart(); + void InitDisplay(); + void UpdateFull(); + void UpdatePart(); + void TransferCommand(uint8_t value); + void SetPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h); +}; \ No newline at end of file diff --git a/src/WatchyRTC.cpp b/src/WatchyRTC.cpp new file mode 100644 index 0000000..0e8e7a8 --- /dev/null +++ b/src/WatchyRTC.cpp @@ -0,0 +1,112 @@ +#include "WatchyRTC.h" + +WatchyRTC::WatchyRTC() +{ +} + +void WatchyRTC::init() { + byte error; + Wire.beginTransmission(RTC_PCF_ADDR); + error = Wire.endTransmission(); +} + +void WatchyRTC::config(String datetime) { // String datetime format is YYYY:MM:DD:HH:MM:SS + _PCFConfig(datetime); +} + +void WatchyRTC::clearAlarm() { + int nextAlarmMinute = 0; + rtc_pcf.clearAlarm(); // resets the alarm flag in the RTC + nextAlarmMinute = rtc_pcf.getMinute(); + nextAlarmMinute = (nextAlarmMinute == 59) ? 0 : (nextAlarmMinute + 1); // set alarm to trigger 1 minute from now + rtc_pcf.setAlarm(nextAlarmMinute, 99, 99, 99); +} + +void WatchyRTC::read(tmElements_t & tm, int offsetInSeconds) { + rtc_pcf.getDate(); + tm.Year = y2kYearToTm(rtc_pcf.getYear()); + tm.Month = rtc_pcf.getMonth(); + tm.Day = rtc_pcf.getDay(); + tm.Wday = rtc_pcf.getWeekday() + 1; // TimeLib & DS3231 has Wday range of 1-7, but PCF8563 stores day of week in 0-6 range + tm.Hour = rtc_pcf.getHour(); + tm.Minute = rtc_pcf.getMinute(); + tm.Second = rtc_pcf.getSecond(); + + int day = tm.Day; + int hour = tm.Hour; + int minute = tm.Minute; + int second = tm.Second; + + // adjust for offset - making some assumptions here: month and year will never change. + second += offsetInSeconds; + if (second >= 60 || second < 0) { + minute += second / 60; + second = second % 60; + } + + if (minute >= 60 || minute < 0) { + hour += minute / 60; + minute = minute % 60; + } + + if (hour >= 24 || hour < 0) { + day += hour / 24; + hour = hour % 24; + } + + tm.Day = day; + tm.Hour = hour; + tm.Minute = minute; + tm.Second = second; +} + +void WatchyRTC::set(tmElements_t tm) { + time_t t = makeTime(tm); // make and break to calculate tm.Wday + breakTime(t, tm); + // day, weekday, month, century(1=1900, 0=2000), year(0-99) + rtc_pcf.setDate(tm.Day, tm.Wday - 1, tm.Month, 0, tmYearToY2k(tm.Year)); + rtc_pcf.setTime(tm.Hour, tm.Minute, tm.Second); + clearAlarm(); +} + +void WatchyRTC::_PCFConfig(String datetime) { // String datetime is YYYY:MM:DD:HH:MM:SS + if (datetime != "") { + tmElements_t tm; + tm.Year = CalendarYrToTm(_getValue(datetime, ':', 0).toInt()); // YYYY - + // 1970 + tm.Month = _getValue(datetime, ':', 1).toInt(); + tm.Day = _getValue(datetime, ':', 2).toInt(); + tm.Hour = _getValue(datetime, ':', 3).toInt(); + tm.Minute = _getValue(datetime, ':', 4).toInt(); + tm.Second = _getValue(datetime, ':', 5).toInt(); + time_t t = makeTime(tm); // make and break to calculate tm.Wday + breakTime(t, tm); + // day, weekday, month, century(1=1900, 0=2000), year(0-99) + rtc_pcf.setDate( + tm.Day, tm.Wday - 1, tm.Month, 0, + tmYearToY2k(tm.Year)); // TimeLib & DS3231 has Wday range of 1-7, but + // PCF8563 stores day of week in 0-6 range + // hr, min, sec + rtc_pcf.setTime(tm.Hour, tm.Minute, tm.Second); + } + + // on POR event, PCF8563 sets month to 0, which will give an error since + // months are 1-12 + clearAlarm(); +} + +String WatchyRTC::_getValue(String data, char separator, int index) { + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length() - 1; + + for (int i = 0; i <= maxIndex && found <= index; i++) { + if (data.charAt(i) == separator || i == maxIndex) { + found++; + strIndex[0] = strIndex[1] + 1; + strIndex[1] = (i == maxIndex) ? i + 1 : i; + } + } + + return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; +} diff --git a/src/WatchyRTC.h b/src/WatchyRTC.h new file mode 100644 index 0000000..238d99e --- /dev/null +++ b/src/WatchyRTC.h @@ -0,0 +1,30 @@ +#ifndef WATCHY_RTC_H +#define WATCHY_RTC_H + +#include "config.h" +#include +#include +#include + +#define RTC_PCF_ADDR 0x51 +#define YEAR_OFFSET_PCF 2000 + +class WatchyRTC { +public: + Rtc_Pcf8563 rtc_pcf; + +public: + WatchyRTC(); + void init(); + void config(String datetime); // String datetime format is YYYY:MM:DD:HH:MM:SS + void clearAlarm(); + void read(tmElements_t & tm, int offsetInSeconds = 0); + void set(tmElements_t tm); + +private: + void _PCFConfig(String datetime); + int _getDayOfWeek(int d, int m, int y); + String _getValue(String data, char separator, int index); +}; + +#endif \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..75789f6 --- /dev/null +++ b/src/config.h @@ -0,0 +1,27 @@ +#define MENU_BTN_PIN 26 +#define BACK_BTN_PIN 25 +#define DOWN_BTN_PIN 4 +#define UP_BTN_PIN 35 +#define BATT_ADC_PIN 34 +#define DISPLAY_CS 5 +#define DISPLAY_RES 9 +#define DISPLAY_DC 10 +#define DISPLAY_BUSY 19 +#define ACC_INT_1_PIN 14 +#define ACC_INT_2_PIN 12 +#define VIB_MOTOR_PIN 13 +#define RTC_INT_PIN 27 + +#define MENU_BTN_MASK GPIO_SEL_26 +#define BACK_BTN_MASK GPIO_SEL_25 +#define DOWN_BTN_MASK GPIO_SEL_4 +#define UP_BTN_MASK GPIO_SEL_35 +#define ACC_INT_MASK GPIO_SEL_14 +#define BTN_PIN_MASK MENU_BTN_MASK|BACK_BTN_MASK|UP_BTN_MASK|DOWN_BTN_MASK + +#define DISPLAY_WIDTH 200 +#define DISPLAY_HEIGHT 200 + +#define WIFI_SSID ">" +#define WIFI_PASS "" +#define TZ_OFFSET 3600 * 3 \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html