Initial commit
This commit is contained in:
commit
62bcff8485
17 changed files with 1035 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.pio
|
||||
.vscode/**
|
39
include/README
Normal file
39
include/README
Normal 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
46
lib/README
Normal 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
23
platformio.ini
Normal 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
12
src/Main.cpp
Normal file
|
@ -0,0 +1,12 @@
|
|||
#include "WatchFace.h"
|
||||
|
||||
WatchFace watchy;
|
||||
|
||||
void setup()
|
||||
{
|
||||
watchy.Init();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
}
|
76
src/SevenSegment.cpp
Normal file
76
src/SevenSegment.cpp
Normal 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
14
src/SevenSegment.h
Normal 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
50
src/WatchFace.cpp
Normal 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
12
src/WatchFace.h
Normal 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
143
src/Watchy.cpp
Normal 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
31
src/Watchy.h
Normal 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
357
src/WatchyDisplay.cpp
Normal 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
50
src/WatchyDisplay.h
Normal 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
112
src/WatchyRTC.cpp
Normal 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
30
src/WatchyRTC.h
Normal 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
27
src/config.h
Normal 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
11
test/README
Normal 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
|
Loading…
Reference in a new issue