Initial commit

This commit is contained in:
Lewis Jackson 2023-05-29 13:03:05 +03:00
commit 62bcff8485
17 changed files with 1035 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.pio
.vscode/**

39
include/README Normal file
View file

@ -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

46
lib/README Normal file
View file

@ -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 <Foo.h>
#include <Bar.h>
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

23
platformio.ini Normal file
View file

@ -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

12
src/Main.cpp Normal file
View file

@ -0,0 +1,12 @@
#include "WatchFace.h"
WatchFace watchy;
void setup()
{
watchy.Init();
}
void loop()
{
}

76
src/SevenSegment.cpp Normal file
View file

@ -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<WatchyDisplayBase, WatchyDisplayBase::HEIGHT> & 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);
}
}

14
src/SevenSegment.h Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
#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<WatchyDisplayBase, WatchyDisplayBase::HEIGHT> & 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;
};

50
src/WatchFace.cpp Normal file
View file

@ -0,0 +1,50 @@
#include "WatchFace.h"
#include "SevenSegment.h"
#include <cmath>
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);
}

12
src/WatchFace.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include "Watchy.h"
class WatchFace : public Watchy
{
public:
void DrawWatchFace();
private:
void DrawBatteryIcon();
};

143
src/Watchy.cpp Normal file
View file

@ -0,0 +1,143 @@
#include "Watchy.h"
WatchyDisplayBase Watchy::m_displayBase;
GxEPD2_BW<WatchyDisplayBase, WatchyDisplayBase::HEIGHT> 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);
}

31
src/Watchy.h Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include "config.h"
#include "WatchyDisplay.h"
#include "WatchyRTC.h"
#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
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<WatchyDisplayBase, WatchyDisplayBase::HEIGHT> m_display;
static WatchyRTC m_RTC;
};

357
src/WatchyDisplay.cpp Normal file
View file

@ -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();
}

50
src/WatchyDisplay.h Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include "config.h"
#include <GxEPD2_EPD.h>
#include <GxEPD2_BW.h>
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);
};

112
src/WatchyRTC.cpp Normal file
View file

@ -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]) : "";
}

30
src/WatchyRTC.h Normal file
View file

@ -0,0 +1,30 @@
#ifndef WATCHY_RTC_H
#define WATCHY_RTC_H
#include "config.h"
#include <Arduino.h>
#include <TimeLib.h>
#include <Rtc_Pcf8563.h>
#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

27
src/config.h Normal file
View file

@ -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 "<my ssid>>"
#define WIFI_PASS "<my pass>"
#define TZ_OFFSET 3600 * 3

11
test/README Normal file
View file

@ -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