Merge branch 'feature/weather-page'
All checks were successful
Compile / Compile (push) Successful in 1m25s

This commit is contained in:
Lewis Jackson 2023-06-02 22:45:10 +03:00
commit 2d11d69067
28 changed files with 3218 additions and 758 deletions

View file

@ -11,4 +11,5 @@ jobs:
- name: Compile - name: Compile
run: | run: |
cd ${{ github.workspace }} cd ${{ github.workspace }}
echo -en '#define WIFI_SSID "<ssid>"\n#define WIFI_PASS "<pass>"\n' >src/secrets.h
/root/.platformio/penv/bin/pio run /root/.platformio/penv/bin/pio run

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.pio .pio
.vscode/** .vscode/**
src/secrets.h

View file

@ -38,27 +38,27 @@
#include <Arduino.h> #include <Arduino.h>
#include "Rtc_Pcf8563.h" #include "Rtc_Pcf8563.h"
Rtc_Pcf8563::Rtc_Pcf8563(void) Rtc_Pcf8563::Rtc_Pcf8563()
{ {
Wire.begin(); Wire.begin();
Rtcc_Addr = RTCC_R>>1; Rtcc_Addr = RTCC_R >> 1;
} }
Rtc_Pcf8563::Rtc_Pcf8563(int sdaPin, int sdlPin) Rtc_Pcf8563::Rtc_Pcf8563(int sdaPin, int sdlPin)
{ {
Wire.begin(sdaPin, sdlPin); Wire.begin(sdaPin, sdlPin);
Rtcc_Addr = RTCC_R>>1; Rtcc_Addr = RTCC_R >> 1;
} }
/* Private internal functions, but useful to look at if you need a similar func. */ /* Private internal functions, but useful to look at if you need a similar func. */
byte Rtc_Pcf8563::decToBcd(byte val) byte Rtc_Pcf8563::decToBcd(byte val)
{ {
return ( (val/10*16) + (val%10) ); return ((val / 10 * 16) + (val % 10));
} }
byte Rtc_Pcf8563::bcdToDec(byte val) byte Rtc_Pcf8563::bcdToDec(byte val)
{ {
return ( (val/16*10) + (val%16) ); return ((val / 16 * 10) + (val % 16));
} }
void Rtc_Pcf8563::zeroClock() void Rtc_Pcf8563::zeroClock()
@ -102,7 +102,7 @@ byte Rtc_Pcf8563::readStatus2()
return getStatus2(); return getStatus2();
} }
void Rtc_Pcf8563::clearVoltLow(void) void Rtc_Pcf8563::clearVoltLow()
{ {
getDateTime(); getDateTime();
// Only clearing is possible on device (I tried) // Only clearing is possible on device (I tried)
@ -114,7 +114,7 @@ void Rtc_Pcf8563::clearVoltLow(void)
/* /*
* Atomicly read all device registers in one operation * Atomicly read all device registers in one operation
*/ */
void Rtc_Pcf8563::getDateTime(void) void Rtc_Pcf8563::getDateTime()
{ {
/* Start at beginning, read entire memory in one go */ /* Start at beginning, read entire memory in one go */
Wire.beginTransmission(Rtcc_Addr); Wire.beginTransmission(Rtcc_Addr);
@ -124,8 +124,9 @@ void Rtc_Pcf8563::getDateTime(void)
/* As per data sheet, have to read everything all in one operation */ /* As per data sheet, have to read everything all in one operation */
uint8_t readBuffer[16] = {0}; uint8_t readBuffer[16] = {0};
Wire.requestFrom(Rtcc_Addr, 16); Wire.requestFrom(Rtcc_Addr, 16);
for (uint8_t i=0; i < 16; i++) for (uint8_t i=0; i < 16; i++) {
readBuffer[i] = Wire.read(); readBuffer[i] = Wire.read();
}
// status bytes // status bytes
status1 = readBuffer[0]; status1 = readBuffer[0];
@ -146,10 +147,12 @@ void Rtc_Pcf8563::getDateTime(void)
weekday = bcdToDec(readBuffer[6] & 0x07); weekday = bcdToDec(readBuffer[6] & 0x07);
//get raw month data byte and set month and century with it. //get raw month data byte and set month and century with it.
month = readBuffer[7]; month = readBuffer[7];
if (month & RTCC_CENTURY_MASK) if (month & RTCC_CENTURY_MASK) {
century = true; century = true;
else } else {
century = false; century = false;
}
//0x1f = 0b00011111 //0x1f = 0b00011111
month = month & 0x1f; month = month & 0x1f;
month = bcdToDec(month); month = bcdToDec(month);
@ -157,25 +160,31 @@ void Rtc_Pcf8563::getDateTime(void)
// alarm bytes // alarm bytes
alarm_minute = readBuffer[9]; alarm_minute = readBuffer[9];
if(B10000000 & alarm_minute) if(B10000000 & alarm_minute) {
alarm_minute = RTCC_NO_ALARM; alarm_minute = RTCC_NO_ALARM;
else } else {
alarm_minute = bcdToDec(alarm_minute & B01111111); alarm_minute = bcdToDec(alarm_minute & B01111111);
}
alarm_hour = readBuffer[10]; alarm_hour = readBuffer[10];
if(B10000000 & alarm_hour) if(B10000000 & alarm_hour) {
alarm_hour = RTCC_NO_ALARM; alarm_hour = RTCC_NO_ALARM;
else } else {
alarm_hour = bcdToDec(alarm_hour & B00111111); alarm_hour = bcdToDec(alarm_hour & B00111111);
}
alarm_day = readBuffer[11]; alarm_day = readBuffer[11];
if(B10000000 & alarm_day) if(B10000000 & alarm_day) {
alarm_day = RTCC_NO_ALARM; alarm_day = RTCC_NO_ALARM;
else } else {
alarm_day = bcdToDec(alarm_day & B00111111); alarm_day = bcdToDec(alarm_day & B00111111);
}
alarm_weekday = readBuffer[12]; alarm_weekday = readBuffer[12];
if(B10000000 & alarm_weekday) if(B10000000 & alarm_weekday) {
alarm_weekday = RTCC_NO_ALARM; alarm_weekday = RTCC_NO_ALARM;
else } else {
alarm_weekday = bcdToDec(alarm_weekday & B00000111); alarm_weekday = bcdToDec(alarm_weekday & B00000111);
}
// CLKOUT_control 0x03 = 0b00000011 // CLKOUT_control 0x03 = 0b00000011
squareWave = readBuffer[13] & 0x03; squareWave = readBuffer[13] & 0x03;
@ -196,10 +205,12 @@ void Rtc_Pcf8563::setDateTime(byte day, byte weekday, byte month,
1=19xx 1=19xx
*/ */
month = decToBcd(month); month = decToBcd(month);
if (century)
if (century) {
month |= RTCC_CENTURY_MASK; month |= RTCC_CENTURY_MASK;
else } else {
month &= ~RTCC_CENTURY_MASK; month &= ~RTCC_CENTURY_MASK;
}
/* As per data sheet, have to set everything all in one operation */ /* As per data sheet, have to set everything all in one operation */
Wire.beginTransmission(Rtcc_Addr); // Issue I2C start signal Wire.beginTransmission(Rtcc_Addr); // Issue I2C start signal
@ -270,7 +281,7 @@ void Rtc_Pcf8563::enableAlarm()
void Rtc_Pcf8563::setAlarm(byte min, byte hour, byte day, byte weekday) void Rtc_Pcf8563::setAlarm(byte min, byte hour, byte day, byte weekday)
{ {
getDateTime(); // operate on current values getDateTime(); // operate on current values
if (min <99) { if (min < 99) {
min = constrain(min, 0, 59); min = constrain(min, 0, 59);
min = decToBcd(min); min = decToBcd(min);
min &= ~RTCC_ALARM; min &= ~RTCC_ALARM;
@ -352,9 +363,12 @@ void Rtc_Pcf8563::resetAlarm()
// true if timer interrupt and control is enabled // true if timer interrupt and control is enabled
bool Rtc_Pcf8563::timerEnabled() bool Rtc_Pcf8563::timerEnabled()
{ {
if (getStatus2() & RTCC_TIMER_TIE) if (getStatus2() & RTCC_TIMER_TIE) {
if (timer_control & RTCC_TIMER_TE) if (timer_control & RTCC_TIMER_TE) {
return true; return true;
}
}
return false; return false;
} }
@ -367,7 +381,7 @@ bool Rtc_Pcf8563::timerActive()
// enable timer and interrupt // enable timer and interrupt
void Rtc_Pcf8563::enableTimer(void) void Rtc_Pcf8563::enableTimer()
{ {
getDateTime(); getDateTime();
//set TE to 1 //set TE to 1
@ -396,10 +410,12 @@ void Rtc_Pcf8563::enableTimer(void)
void Rtc_Pcf8563::setTimer(byte value, byte frequency, bool is_pulsed) void Rtc_Pcf8563::setTimer(byte value, byte frequency, bool is_pulsed)
{ {
getDateTime(); getDateTime();
if (is_pulsed) if (is_pulsed) {
status2 |= 0x01 << 4; status2 |= 0x01 << 4;
else } else {
status2 &= ~(0x01 << 4); status2 &= ~(0x01 << 4);
}
timer_value = value; timer_value = value;
// TE set to 1 in enableTimer(), leave 0 for now // TE set to 1 in enableTimer(), leave 0 for now
timer_control |= (frequency & RTCC_TIMER_TD10); // use only last 2 bits timer_control |= (frequency & RTCC_TIMER_TD10); // use only last 2 bits
@ -420,7 +436,7 @@ void Rtc_Pcf8563::setTimer(byte value, byte frequency, bool is_pulsed)
// clear timer flag and interrupt // clear timer flag and interrupt
void Rtc_Pcf8563::clearTimer(void) void Rtc_Pcf8563::clearTimer()
{ {
getDateTime(); getDateTime();
//set status2 TF val to zero //set status2 TF val to zero
@ -447,7 +463,7 @@ void Rtc_Pcf8563::clearTimer(void)
// clear timer flag but leave interrupt unchanged */ // clear timer flag but leave interrupt unchanged */
void Rtc_Pcf8563::resetTimer(void) void Rtc_Pcf8563::resetTimer()
{ {
getDateTime(); getDateTime();
//set status2 TF val to zero to reset timer //set status2 TF val to zero to reset timer
@ -506,22 +522,20 @@ const char *Rtc_Pcf8563::formatTime(byte style)
} }
const char *Rtc_Pcf8563::formatDate(byte style) const char * Rtc_Pcf8563::formatDate(byte style)
{ {
getDate(); getDate();
switch (style) { switch (style) {
case RTCC_DATE_ISO8601:
case RTCC_DATE_ASIA: if (century ) {
//do the asian style, yyyy-mm-dd
if (century ){
strDate[0] = '1'; strDate[0] = '1';
strDate[1] = '9'; strDate[1] = '9';
} } else {
else {
strDate[0] = '2'; strDate[0] = '2';
strDate[1] = '0'; strDate[1] = '0';
} }
strDate[2] = '0' + (year / 10 ); strDate[2] = '0' + (year / 10 );
strDate[3] = '0' + (year % 10); strDate[3] = '0' + (year % 10);
strDate[4] = '-'; strDate[4] = '-';
@ -533,21 +547,22 @@ const char *Rtc_Pcf8563::formatDate(byte style)
strDate[10] = '\0'; strDate[10] = '\0';
break; break;
case RTCC_DATE_US: case RTCC_DATE_US:
//the pitiful US style, mm/dd/yyyy // the utterly bonkers US style, mm/dd/yyyy
strDate[0] = '0' + (month / 10); strDate[0] = '0' + (month / 10);
strDate[1] = '0' + (month % 10); strDate[1] = '0' + (month % 10);
strDate[2] = '/'; strDate[2] = '/';
strDate[3] = '0' + (day / 10); strDate[3] = '0' + (day / 10);
strDate[4] = '0' + (day % 10); strDate[4] = '0' + (day % 10);
strDate[5] = '/'; strDate[5] = '/';
if (century){
if (century) {
strDate[6] = '1'; strDate[6] = '1';
strDate[7] = '9'; strDate[7] = '9';
} } else {
else {
strDate[6] = '2'; strDate[6] = '2';
strDate[7] = '0'; strDate[7] = '0';
} }
strDate[8] = '0' + (year / 10 ); strDate[8] = '0' + (year / 10 );
strDate[9] = '0' + (year % 10); strDate[9] = '0' + (year % 10);
strDate[10] = '\0'; strDate[10] = '\0';
@ -562,20 +577,20 @@ const char *Rtc_Pcf8563::formatDate(byte style)
strDate[4] = '0' + (month % 10); strDate[4] = '0' + (month % 10);
strDate[5] = '-'; strDate[5] = '-';
if (century){ if (century) {
strDate[6] = '1'; strDate[6] = '1';
strDate[7] = '9'; strDate[7] = '9';
} } else {
else {
strDate[6] = '2'; strDate[6] = '2';
strDate[7] = '0'; strDate[7] = '0';
} }
strDate[8] = '0' + (year / 10 ); strDate[8] = '0' + (year / 10 );
strDate[9] = '0' + (year % 10); strDate[9] = '0' + (year % 10);
strDate[10] = '\0'; strDate[10] = '\0';
break; break;
} }
return strDate; return strDate;
} }
@ -626,44 +641,53 @@ void Rtc_Pcf8563::getTime()
getDateTime(); getDateTime();
} }
bool Rtc_Pcf8563::getVoltLow(void) bool Rtc_Pcf8563::getVoltLow()
{ {
return volt_low; return volt_low;
} }
byte Rtc_Pcf8563::getSecond() { byte Rtc_Pcf8563::getSecond()
{
return sec; return sec;
} }
byte Rtc_Pcf8563::getMinute() { byte Rtc_Pcf8563::getMinute()
{
return minute; return minute;
} }
byte Rtc_Pcf8563::getHour() { byte Rtc_Pcf8563::getHour()
{
return hour; return hour;
} }
byte Rtc_Pcf8563::getAlarmMinute() { byte Rtc_Pcf8563::getAlarmMinute()
{
return alarm_minute; return alarm_minute;
} }
byte Rtc_Pcf8563::getAlarmHour() { byte Rtc_Pcf8563::getAlarmHour()
{
return alarm_hour; return alarm_hour;
} }
byte Rtc_Pcf8563::getAlarmDay() { byte Rtc_Pcf8563::getAlarmDay()
{
return alarm_day; return alarm_day;
} }
byte Rtc_Pcf8563::getAlarmWeekday() { byte Rtc_Pcf8563::getAlarmWeekday()
{
return alarm_weekday; return alarm_weekday;
} }
byte Rtc_Pcf8563::getTimerControl() { byte Rtc_Pcf8563::getTimerControl()
{
return timer_control; return timer_control;
} }
byte Rtc_Pcf8563::getTimerValue() { byte Rtc_Pcf8563::getTimerValue()
{
// Impossible to freeze this value, it could // Impossible to freeze this value, it could
// be changing during read. Multiple reads // be changing during read. Multiple reads
// required to check for consistency. // required to check for consistency.
@ -672,52 +696,62 @@ byte Rtc_Pcf8563::getTimerValue() {
last_value = timer_value; last_value = timer_value;
getDateTime(); getDateTime();
} while (timer_value != last_value); } while (timer_value != last_value);
return timer_value; return timer_value;
} }
byte Rtc_Pcf8563::getDay() { byte Rtc_Pcf8563::getDay()
{
return day; return day;
} }
byte Rtc_Pcf8563::getMonth() { byte Rtc_Pcf8563::getMonth()
{
return month; return month;
} }
byte Rtc_Pcf8563::getYear() { byte Rtc_Pcf8563::getYear()
{
return year; return year;
} }
bool Rtc_Pcf8563::getCentury() { bool Rtc_Pcf8563::getCentury()
{
return century; return century;
} }
byte Rtc_Pcf8563::getWeekday() { byte Rtc_Pcf8563::getWeekday()
{
return weekday; return weekday;
} }
byte Rtc_Pcf8563::getStatus1() { byte Rtc_Pcf8563::getStatus1()
{
return status1; return status1;
} }
byte Rtc_Pcf8563::getStatus2() { byte Rtc_Pcf8563::getStatus2()
{
return status2; return status2;
} }
unsigned long Rtc_Pcf8563::getTimestamp(){ unsigned long Rtc_Pcf8563::getTimestamp()
{
getDateTime(); // update date and time getDateTime(); // update date and time
unsigned long timestamp = 0; unsigned long timestamp = 0;
// Convert years in days // Convert years in days
timestamp = (year-epoch_year)*365; // convert years in days timestamp = (year-epoch_year) * 365; // convert years in days
if((year-epoch_year)>1) // add a dy when it's a leap year if((year-epoch_year)>1) { // add a dy when it's a leap year
{ for(unsigned char i = epoch_year; i<year;i++) {
for(unsigned char i = epoch_year; i<year;i++)
{
if(isLeapYear(century, i)) timestamp++; // add a day for each leap years if(isLeapYear(century, i)) timestamp++; // add a day for each leap years
} }
} }
if(month>2&&isLeapYear(century, year)) timestamp++; // test for the year's febuary
if(month>2 && isLeapYear(century, year)) {
timestamp++; // test for the year's febuary
}
// add months converted in days // add months converted in days
if(month>1) timestamp += months_days[month-2]; if(month>1) timestamp += months_days[month-2];
@ -725,11 +759,11 @@ unsigned long Rtc_Pcf8563::getTimestamp(){
// add days // add days
timestamp += (day-epoch_day); timestamp += (day-epoch_day);
timestamp*= 86400; // convert days in seconds timestamp *= 86400; // convert days in seconds
// convert time to second and add it to timestamp // convert time to second and add it to timestamp
unsigned long timeTemp = hour*60+ minute; unsigned long timeTemp = hour*60+ minute;
timeTemp *=60; timeTemp *= 60;
timeTemp += sec; timeTemp += sec;
timestamp += timeTemp; // add hours +minutes + seconds timestamp += timeTemp; // add hours +minutes + seconds
@ -738,3 +772,44 @@ unsigned long Rtc_Pcf8563::getTimestamp(){
return timestamp; return timestamp;
} }
uint64_t Rtc_Pcf8563::getTimestamp64()
{
getDateTime(); // update date and time
uint64_t timestamp = 0;
// Convert years in days
timestamp = (year - epoch_year) * 365; // convert years to days
if((year - epoch_year) > 1) { // add a day when it's a leap year
for(unsigned char i = epoch_year; i<year;i++) {
if(isLeapYear(century, i)) {
timestamp++; // add a day for each leap year
}
}
}
if(month > 2 && isLeapYear(century, year)) {
timestamp++; // test for the year's febuary
}
// add months converted in days
if(month > 1) {
timestamp += months_days[month-2];
}
// add days
timestamp += (day-epoch_day);
timestamp *= 86400; // convert days in seconds
// convert time to second and add it to timestamp
uint64_t timeTemp = hour * 60 + minute;
timeTemp *= 60;
timeTemp += sec;
timestamp += timeTemp; // add hours +minutes + seconds
timestamp += EPOCH_TIMESTAMP; // add epoch reference
return timestamp;
}

View file

@ -102,7 +102,8 @@
/* date format flags */ /* date format flags */
#define RTCC_DATE_WORLD 0x01 #define RTCC_DATE_WORLD 0x01
#define RTCC_DATE_ASIA 0x02 #define RTCC_DATE_ISO8601 0x02
#define RTCC_DATE_ASIA 0x02 // It's not "asian", it's ISO8601, that anybody with any sense uses
#define RTCC_DATE_US 0x04 #define RTCC_DATE_US 0x04
/* time format flags */ /* time format flags */
#define RTCC_TIME_HMS 0x01 #define RTCC_TIME_HMS 0x01
@ -198,6 +199,7 @@ class Rtc_Pcf8563 {
byte getTimerValue(); byte getTimerValue();
unsigned long getTimestamp(); // return unix timestamp unsigned long getTimestamp(); // return unix timestamp
uint64_t getTimestamp64(); // Fixed for 2038+
// Sets date/time to static fixed values, disable all alarms // Sets date/time to static fixed values, disable all alarms
// use zeroClock() above to guarantee lowest possible values instead. // use zeroClock() above to guarantee lowest possible values instead.

View file

@ -2,11 +2,24 @@
namespace Icons namespace Icons
{ {
const unsigned char steps [] PROGMEM = { // 19x23
const unsigned char steps[] PROGMEM = {
0x00, 0x03, 0xc0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xe0, 0x00, 0x0f, 0xe0, 0x78, 0x0f, 0xe0, 0xfc, 0x00, 0x03, 0xc0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xe0, 0x00, 0x0f, 0xe0, 0x78, 0x0f, 0xe0, 0xfc,
0x0f, 0xe0, 0xfc, 0x0f, 0xe0, 0xfc, 0x0f, 0xe0, 0xfe, 0x0f, 0xe0, 0xfe, 0x07, 0xc0, 0xfe, 0x07, 0x0f, 0xe0, 0xfc, 0x0f, 0xe0, 0xfc, 0x0f, 0xe0, 0xfe, 0x0f, 0xe0, 0xfe, 0x07, 0xc0, 0xfe, 0x07,
0xc0, 0xfe, 0x07, 0x80, 0xfe, 0x00, 0x00, 0x7c, 0x0e, 0x00, 0x7c, 0x0f, 0x80, 0x7c, 0x1f, 0x80, 0xc0, 0xfe, 0x07, 0x80, 0xfe, 0x00, 0x00, 0x7c, 0x0e, 0x00, 0x7c, 0x0f, 0x80, 0x7c, 0x1f, 0x80,
0x20, 0x1f, 0x00, 0x06, 0x0f, 0x00, 0x3e, 0x0e, 0x00, 0x3e, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x1e, 0x20, 0x1f, 0x00, 0x06, 0x0f, 0x00, 0x3e, 0x0e, 0x00, 0x3e, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x1e,
0x00, 0x00, 0x1e, 0x00, 0x00 0x00, 0x00, 0x1e, 0x00, 0x00
}; };
};
// 29x23
const unsigned char city[] PROGMEM = {
0x00, 0x07, 0xf8, 0x00, 0x00, 0x07, 0xfc, 0x00, 0x00, 0x07, 0xfc, 0x00, 0x0e, 0x07, 0x1c, 0x00,
0x1f, 0x87, 0x1c, 0x00, 0x3f, 0xc7, 0x1c, 0xc0, 0x3f, 0xc7, 0xfc, 0xc0, 0x3f, 0xc7, 0x1c, 0xc0,
0x7f, 0xc7, 0x1c, 0xc0, 0x7f, 0xe7, 0x1f, 0xf8, 0xff, 0xe7, 0xff, 0xf8, 0xff, 0xe7, 0x1e, 0x38,
0xff, 0xe7, 0x1e, 0x38, 0xff, 0xe7, 0x1e, 0x38, 0x06, 0x07, 0x1e, 0x38, 0x06, 0x07, 0xff, 0xf8,
0x06, 0x07, 0xfe, 0x38, 0x06, 0x07, 0xfe, 0x38, 0x06, 0x07, 0xfe, 0x38, 0x06, 0x07, 0xff, 0xf8,
0x06, 0x07, 0xff, 0xf8, 0x06, 0x07, 0xff, 0xf8, 0x06, 0x07, 0xff, 0xf8
};
}
#include "WeatherIcons.h"

View file

@ -5,7 +5,7 @@ WatchFace watchy;
void setup() void setup()
{ {
Serial.begin(9600); Serial.begin(9600);
watchy.Init(); watchy.Wake();
} }
void loop() void loop()

View file

@ -1,27 +1,36 @@
#include "WatchFace.h" #include "WatchFace.h"
#include "SevenSegment.h" #include "WatchFacePages/Clock.h"
#include "Icons.h"
#include <cmath> #include <cmath>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
RTC_DATA_ATTR bool WatchFace::m_menuSetup = false;
RTC_DATA_ATTR bool WatchFace::m_inMenu = false; RTC_DATA_ATTR bool WatchFace::m_inMenu = false;
RTC_DATA_ATTR int WatchFace::m_tzOffset = TZ_OFFSET; RTC_DATA_ATTR Menu WatchFace::m_menu;
Menu WatchFace::m_menu; RTC_DATA_ATTR uint8_t WatchFace::m_watchFacePage;
void WatchFace::Setup() // Called after hardware is set up void WatchFace::InitBoot()
{ {
if (!m_menuSetup) {
m_menu.Init(m_display); m_menu.Init(m_display);
SetupVolatileMenuStuff();
m_menu.Reset();
if(m_features.wifi.Connect()) {
SyncNTPTime();
m_features.wifi.Disconnect();
} }
for (auto & page : m_pages) {
page->InitBoot();
}
static_cast<WatchFacePages::Weather *>(m_pages[1].get())->Resync();
}
void WatchFace::InitWake()
{
SetupVolatileMenuStuff(); SetupVolatileMenuStuff();
if (!m_menuSetup) { for (auto & page : m_pages) {
m_menu.Reset(); page->InitWake();
m_menuSetup = true;
} }
} }
@ -64,6 +73,30 @@ void WatchFace::HandleButtonPress(uint64_t buttonMask)
} }
} }
delay(10);
}
} else if (buttonMask & UP_BTN_MASK) {
m_watchFacePage = (m_watchFacePage + 1) % m_pages.size();
ShowWatchFace(false);
while (true) {
// Wait for button release
if (digitalRead(UP_BTN_PIN) == LOW){
break;
}
delay(10);
}
} else if (buttonMask & DOWN_BTN_MASK) {
m_watchFacePage = (m_watchFacePage + m_pages.size() - 1) % m_pages.size();
ShowWatchFace(false);
while (true) {
// Wait for button release
if (digitalRead(DOWN_BTN_PIN) == LOW){
break;
}
delay(10); delay(10);
} }
} }
@ -86,68 +119,12 @@ void WatchFace::DrawWatchFace(bool partialRefresh)
return; return;
} }
m_display.setFullWindow(); m_pages[m_watchFacePage]->DrawPage(partialRefresh);
m_display.fillScreen(GxEPD_WHITE); // Resync weather in background
DrawBatteryIcon(); if (m_watchFacePage != 1) {
static_cast<WatchFacePages::Weather *>(m_pages[1].get())->Resync();
tmElements_t currentTime;
m_RTC.Get(currentTime, m_tzOffset);
SevenSegment sevenSegment(30, 60, 6, 5, 5);
if (currentTime.Hour < 10) {
sevenSegment.DrawDigit(m_display, 0, 15, 75, GxEPD_BLACK);
} else {
sevenSegment.DrawDigit(m_display, currentTime.Hour / 10, 20, 75, GxEPD_BLACK);
} }
sevenSegment.DrawDigit(m_display, currentTime.Hour % 10, 60, 75, GxEPD_BLACK);
if (currentTime.Minute < 10) {
sevenSegment.DrawDigit(m_display, 0, 110, 75, GxEPD_BLACK);
} else {
sevenSegment.DrawDigit(m_display, currentTime.Minute / 10, 110, 75, GxEPD_BLACK);
}
sevenSegment.DrawDigit(m_display, currentTime.Minute % 10, 150, 75, GxEPD_BLACK);
m_display.fillRect(97, 90, 5, 5, GxEPD_BLACK);
m_display.fillRect(97, 110, 5, 5, GxEPD_BLACK);
m_display.display(partialRefresh);
}
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 = level * level * (3.0f - 2.0f * level);
m_display.fillRect(200 - 44, 9, (int)std::round(35.0f * level), 15, GxEPD_BLACK);
int x = 200 - 85;
int intLevel = (int)std::round(100.0f * level);
if (intLevel == 100) {
x -= 13;
}
m_display.setFont(&FreeMonoBold9pt7b);
m_display.setCursor(x, 22);
m_display.setTextColor(GxEPD_BLACK);
m_display.print(intLevel);
m_display.print("%");
m_display.setFont(&FreeMonoBold12pt7b);
m_display.drawBitmap(10, 177, Icons::steps, 19, 23, GxEPD_BLACK);
m_display.setCursor(40, 195);
m_display.print(GetSteps());
} }
void WatchFace::SetupVolatileMenuStuff() void WatchFace::SetupVolatileMenuStuff()
@ -266,21 +243,25 @@ void WatchFace::MenuExited()
void WatchFace::MenuNTPSyncSelected() void WatchFace::MenuNTPSyncSelected()
{ {
ConnectWiFi(); if (!m_features.wifi.Connect()) {
return;
}
SyncNTPTime(); SyncNTPTime();
DisconnectWiFi(); m_features.wifi.Disconnect();
m_RTC.Resync();
if (m_inMenu) { if (m_inMenu) {
m_inMenu = false; m_inMenu = false;
m_menu.Reset(); m_menu.Reset();
DrawWatchFace(false); DrawWatchFace(false);
} }
m_features.rtc.Resync();
} }
void WatchFace::MenuTimeZoneSelected(int tzOffset) void WatchFace::MenuTimeZoneSelected(int tzOffset)
{ {
m_tzOffset = tzOffset; m_features.storage.SetTzOffset(tzOffset);
if (m_inMenu) { if (m_inMenu) {
m_inMenu = false; m_inMenu = false;
@ -291,7 +272,7 @@ void WatchFace::MenuTimeZoneSelected(int tzOffset)
void WatchFace::MenuConfirmResetSteps() void WatchFace::MenuConfirmResetSteps()
{ {
ResetSteps(); m_features.stepCounter.ResetSteps();
if (m_inMenu) { if (m_inMenu) {
m_inMenu = false; m_inMenu = false;

View file

@ -2,21 +2,28 @@
#include "Watchy.h" #include "Watchy.h"
#include "Menu.h" #include "Menu.h"
#include "WatchFacePages/Clock.h"
#include "WatchFacePages/Weather.h"
#include <memory>
class WatchFace : public Watchy class WatchFace : public Watchy
{ {
public: public:
void Setup() override; // Called after hardware is set up void InitBoot() override; // Called once when the watch starts up
void InitWake() override; // Called every time the watch wakes from sleep
void HandleButtonPress(uint64_t buttonMask) override; void HandleButtonPress(uint64_t buttonMask) override;
void HandleDoubleTap() override; void HandleDoubleTap() override;
void HandleTilt() override; void HandleTilt() override;
void DrawWatchFace(bool partialRefresh = false) override; void DrawWatchFace(bool partialRefresh = false) override;
private: private:
RTC_DATA_ATTR static bool m_menuSetup;
RTC_DATA_ATTR static bool m_inMenu; RTC_DATA_ATTR static bool m_inMenu;
RTC_DATA_ATTR static Menu m_menu; RTC_DATA_ATTR static Menu m_menu;
RTC_DATA_ATTR static int m_tzOffset; RTC_DATA_ATTR static uint8_t m_watchFacePage;
std::vector<std::shared_ptr<WatchFacePages::Page>> m_pages = {
std::make_shared<WatchFacePages::Clock>(m_display, m_features),
std::make_shared<WatchFacePages::Weather>(m_display, m_features)
};
void SetupVolatileMenuStuff(); void SetupVolatileMenuStuff();
void DrawBatteryIcon(); void DrawBatteryIcon();

View file

@ -0,0 +1,111 @@
#include "Clock.h"
#include "../SevenSegment.h"
#include "../Icons.h"
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <sstream>
WatchFacePages::Clock::Clock(WatchyDisplay & display, WatchFeatures::WatchFeatures & features)
: m_display(display), m_features(features)
{
}
void WatchFacePages::Clock::InitBoot()
{
}
void WatchFacePages::Clock::InitWake()
{
}
void WatchFacePages::Clock::DrawPage(bool partialRefresh)
{
m_display.setFullWindow();
m_display.fillScreen(GxEPD_WHITE);
DrawBatteryIcon();
// Get current time and offset by timezone
tmElements_t currentTime;
m_features.rtc.Get(currentTime);
std::string date = m_features.rtc.GetDateString();
int tzOffset = m_features.storage.GetTzOffset();
m_features.rtc.OffsetTime(currentTime, tzOffset);
SevenSegment sevenSegment(30, 60, 6, 5, 5);
if (currentTime.Hour < 10) {
sevenSegment.DrawDigit(m_display, 0, 15, 65, GxEPD_BLACK);
} else {
sevenSegment.DrawDigit(m_display, currentTime.Hour / 10, 20, 65, GxEPD_BLACK);
}
sevenSegment.DrawDigit(m_display, currentTime.Hour % 10, 60, 65, GxEPD_BLACK);
if (currentTime.Minute < 10) {
sevenSegment.DrawDigit(m_display, 0, 110, 65, GxEPD_BLACK);
} else {
sevenSegment.DrawDigit(m_display, currentTime.Minute / 10, 110, 65, GxEPD_BLACK);
}
sevenSegment.DrawDigit(m_display, currentTime.Minute % 10, 150, 65, GxEPD_BLACK);
m_display.fillRect(97, 80, 5, 5, GxEPD_BLACK);
m_display.fillRect(97, 100, 5, 5, GxEPD_BLACK);
// Print day and date
const char * weekDays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
const char * dow = weekDays[currentTime.Wday - 1];
m_display.setFont(&FreeSans9pt7b);
m_display.setTextColor(GxEPD_BLACK);
m_display.setCursor(15, 150);
m_display.print(dow);
int16_t x, y;
uint16_t w, h;
m_display.getTextBounds(date.c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(180 - w, 150);
m_display.print(m_features.rtc.GetDateString().c_str());
// Seperator
m_display.fillRect(15, 127, 170, 2, GxEPD_BLACK);
// Steps
m_display.setFont(&FreeSans12pt7b);
m_display.setTextColor(GxEPD_BLACK);
m_display.drawBitmap(10, 177, Icons::steps, 19, 23, GxEPD_BLACK);
m_display.setCursor(40, 195);
m_display.print(m_features.stepCounter.GetSteps());
m_display.display(partialRefresh);
}
void WatchFacePages::Clock::DrawBatteryIcon()
{
m_display.fillRect(200 - 48, 5, 43, 23, GxEPD_BLACK);
m_display.fillRect(200 - 46, 7, 39, 19, GxEPD_WHITE);
int intLevel = m_features.battery.GetPercentage();
float level = intLevel / 100.0f;
m_display.fillRect(200 - 44, 9, (int)std::round(35.0f * level), 15, GxEPD_BLACK);
int16_t x = 200 - 85;
if (intLevel == 100) {
x -= 13;
}
std::ostringstream oss;
oss << intLevel << "%";
m_display.setFont(&FreeSans9pt7b);
m_display.setTextColor(GxEPD_BLACK);
int16_t y;
uint16_t w, h;
m_display.getTextBounds(oss.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(142 - w, 22);
m_display.print(oss.str().c_str());
}

View file

@ -0,0 +1,25 @@
#pragma once
#include "Page.h"
#include "../WatchyDisplay.h"
#include "../WatchFeatures/WatchFeatures.h"
namespace WatchFacePages
{
class Clock;
}
class WatchFacePages::Clock : public WatchFacePages::Page
{
public:
Clock(WatchyDisplay & display, WatchFeatures::WatchFeatures & features);
void InitBoot() override;
void InitWake() override;
void DrawPage(bool partialRefresh = false) override;
private:
void DrawBatteryIcon();
WatchyDisplay & m_display;
WatchFeatures::WatchFeatures & m_features;
};

15
src/WatchFacePages/Page.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
namespace WatchFacePages
{
class Page;
}
class WatchFacePages::Page
{
public:
Page() {};
virtual void InitBoot() {};
virtual void InitWake() {};
virtual void DrawPage(bool partialRefresh = false) = 0;
};

View file

@ -0,0 +1,432 @@
#include "Weather.h"
#include "../SevenSegment.h"
#include "../Icons.h"
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <HTTPClient.h>
#include <math.h>
#include <sstream>
#include <iomanip>
RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncTime = 0;
RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncAttemptTime = 0;
RTC_DATA_ATTR int WatchFacePages::Weather::m_lastCalculatedDay = 0XFFFFFFFF;
RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLat = DEFAULT_LATITUDE;
RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLon = DEFAULT_LONGITUDE;
RTC_DATA_ATTR char WatchFacePages::Weather::m_locationCity[128];
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseMinute;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonMinute;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetMinute;
RTC_DATA_ATTR double WatchFacePages::Weather::m_moonPhase;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_moonBitmap[8 * 64];
RTC_DATA_ATTR double WatchFacePages::Weather::m_currentTemperature;
RTC_DATA_ATTR float WatchFacePages::Weather::m_currentWindSpeed;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_currentHumidity;
RTC_DATA_ATTR char WatchFacePages::Weather::m_currentWeatherCondition[4];
WatchFacePages::Weather::Weather(WatchyDisplay & display, WatchFeatures::WatchFeatures & features)
: m_display(display), m_features(features)
{
}
void WatchFacePages::Weather::InitBoot()
{
memcpy(m_locationCity, DEFAULT_CITY_NAME, sizeof(DEFAULT_CITY_NAME));
}
void WatchFacePages::Weather::InitWake()
{
}
void WatchFacePages::Weather::DrawPage(bool partialRefresh)
{
Resync();
Recalc();
m_display.setFullWindow();
m_display.fillScreen(GxEPD_WHITE);
m_display.setTextColor(GxEPD_BLACK);
bool weatherOutdated = false;
// If it hasn't synced in 24 hours, assume weather is outdated
if (m_lastSyncTime == 0 || m_features.rtc.GetTimestamp() - m_lastSyncTime > 86400) {
weatherOutdated = true;
}
// City name and icon
m_display.setFont(&FreeSans9pt7b);
int16_t x, y;
uint16_t w, h;
m_display.drawBitmap(5, 5, Icons::city, 29, 23, GxEPD_BLACK);
m_display.setCursor(39, 22);
m_display.print(m_locationCity);
// Separator
m_display.fillRect(10, 35, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK);
// Moon
m_display.drawBitmap(200 - 64 - 10, 45, m_moonBitmap, 64, 64, GxEPD_BLACK);
m_display.drawCircle(200 - 64 - 10 + 32, 45 + 32, 32, GxEPD_BLACK);
// Sunrise, solar noon and sunset
m_display.drawBitmap(10, 38, Icons::Weather::sunrise, 30, 30, GxEPD_BLACK);
std::ostringstream sunrise;
sunrise << std::setfill('0') << std::setw(2) << (int)m_sunriseHour << ":" << std::setw(2) << (int)m_sunriseMinute;
m_display.setFont(&FreeSans9pt7b);
m_display.setCursor(45, 56);
m_display.setTextColor(GxEPD_BLACK);
m_display.print(sunrise.str().c_str());
m_display.drawBitmap(10, 62, Icons::Weather::noon, 30, 30, GxEPD_BLACK);
std::ostringstream noon;
noon << std::setfill('0') << std::setw(2) << (int)m_noonHour << ":" << std::setw(2) << (int)m_noonMinute;
m_display.setCursor(45, 80);
m_display.print(noon.str().c_str());
m_display.drawBitmap(10, 87, Icons::Weather::sunset, 30, 30, GxEPD_BLACK);
std::ostringstream sunset;
sunset << std::setfill('0') << std::setw(2) << (int)m_sunsetHour << ":" << std::setw(2) << (int)m_sunsetMinute;
m_display.setFont(&FreeSans9pt7b);
m_display.setCursor(45, 106);
m_display.print(sunset.str().c_str());
// Temperature and weather icon
// Separator
m_display.fillRect(10, 116, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK);
if (weatherOutdated) {
m_display.setFont(&FreeSans9pt7b);
m_display.getTextBounds("No Weather Data", 0, 0, &x, &y, &w, &h);
m_display.setCursor(DISPLAY_WIDTH / 2 - w / 2, 162);
m_display.print("No Weather Data");
} else {
// Unknown, Cloudy, Fog, Heavy Rain, Heavy Showers, Heavy Snow, Heavy Snow Showers, Light Rain, Light Showers, Light Sleet, Light Sleet Showers, Light Snow, Light Snow Showers, Partly Cloudy, Sunny, Thundery Heavy Rain, Thundery Showers, Thundery Snow Showers, Very Cloudy
std::vector<std::string> iconLookupCondition = {"?", "mm", "=", "///", "//", "**", "/*/", "/", ".", "x", "x/", "*", "*/", "m", "o", "/!/", "!/", "*!*", "mmm"};
std::string iconName = m_currentWeatherCondition;
int iconNum = 0;
for (int i = 0; i < iconLookupCondition.size(); i++) {
if (iconName == iconLookupCondition[i]) {
iconNum = i;
break;
}
}
// Get current time to see if it's day or night
tmElements_t currentTime;
m_features.rtc.Get(currentTime);
m_features.rtc.OffsetTime(currentTime, m_features.storage.GetTzOffset());
const unsigned char * iconBitmap = nullptr;
if (currentTime.Hour >= m_sunriseHour && currentTime.Hour < m_sunsetHour) {
const std::vector<const unsigned char * PROGMEM> dayIconLookupBitmap = {
Icons::Weather::alien,
Icons::Weather::day_cloudy,
Icons::Weather::day_fog,
Icons::Weather::day_rain_wind,
Icons::Weather::day_rain_mix,
Icons::Weather::day_snow,
Icons::Weather::day_snow_wind,
Icons::Weather::day_rain,
Icons::Weather::day_showers,
Icons::Weather::day_sleet,
Icons::Weather::day_sleet,
Icons::Weather::day_snow,
Icons::Weather::day_snow,
Icons::Weather::day_cloudy,
Icons::Weather::day_sunny,
Icons::Weather::day_thunderstorm,
Icons::Weather::day_thunderstorm,
Icons::Weather::day_snow_thunderstorm,
Icons::Weather::cloudy
};
iconBitmap = dayIconLookupBitmap[iconNum];
} else {
const std::vector<const unsigned char * PROGMEM> nightIconLookupBitmap = {
Icons::Weather::alien,
Icons::Weather::night_cloudy,
Icons::Weather::night_fog,
Icons::Weather::night_rain_wind,
Icons::Weather::night_rain_mix,
Icons::Weather::night_snow,
Icons::Weather::night_snow_wind,
Icons::Weather::night_rain,
Icons::Weather::night_showers,
Icons::Weather::night_sleet,
Icons::Weather::night_sleet,
Icons::Weather::night_snow,
Icons::Weather::night_snow,
Icons::Weather::night_cloudy,
Icons::Weather::night_clear,
Icons::Weather::night_thunderstorm,
Icons::Weather::night_thunderstorm,
Icons::Weather::night_snow_thunderstorm,
Icons::Weather::cloudy
};
iconBitmap = nightIconLookupBitmap[iconNum];
}
m_display.drawBitmap(5, 125, iconBitmap, 75, 75, GxEPD_BLACK);
m_display.setFont(&FreeSans12pt7b);
std::ostringstream temperature;
temperature << (int)m_currentTemperature << " C";
m_display.getTextBounds(temperature.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(190 - w, 142);
m_display.print(temperature.str().c_str());
// Hacky degree symbol
m_display.fillCircle(190 - 23, 129, 3, GxEPD_BLACK);
m_display.fillCircle(190 - 23, 129, 1, GxEPD_WHITE);
// Wind speed
m_display.drawBitmap(165, 145, Icons::Weather::wind, 30, 30, GxEPD_BLACK);
std::ostringstream windSpeed;
windSpeed << std::fixed << std::setprecision(1) << m_currentWindSpeed << " m/s";
m_display.setFont(&FreeSans9pt7b);
m_display.getTextBounds(windSpeed.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(160 - w, 165);
m_display.print(windSpeed.str().c_str());
// Humidity
m_display.drawBitmap(165, 170, Icons::Weather::humidity, 30, 30, GxEPD_BLACK);
std::ostringstream humidity;
humidity << (int)m_currentHumidity;
m_display.getTextBounds(humidity.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(160 - w, 190);
m_display.print(humidity.str().c_str());
}
m_display.display(partialRefresh);
}
void WatchFacePages::Weather::Resync()
{
uint64_t currentTime = m_features.rtc.GetTimestamp();
if(m_lastSyncTime > 0 && currentTime - m_lastSyncTime < WEATHER_UPDATE_INTERVAL) {
return;
}
if (m_lastSyncTime > 0 && currentTime - m_lastSyncAttemptTime < WEATHER_UPDATE_BACKOFF) {
return;
}
if(!m_features.wifi.Connect()) {
m_lastSyncAttemptTime = currentTime;
return;
}
HTTPClient client;
client.setConnectTimeout(3000); // 3 second max timeout
// Get geolocation from IP
if (DO_GEOLOCATION) {
client.begin("http://ip-api.com/line/");
int httpCode = client.GET();
if (httpCode != 200) {
m_features.wifi.Disconnect();
m_lastSyncAttemptTime = currentTime;
return;
}
String payload = client.getString();
client.end();
std::string reponse(payload.c_str());
// Split into lines
std::vector<std::string> lines;
std::string::size_type pos = 0;
std::string::size_type prev = 0;
char delimiter = '\n';
while ((pos = reponse.find(delimiter, prev)) != std::string::npos) {
lines.push_back(reponse.substr(prev, pos - prev));
prev = pos + 1;
}
// Check if we got enough lines
if (lines.size() < 14) {
m_features.wifi.Disconnect();
m_lastSyncAttemptTime = currentTime;
return;
}
std::string location = lines[5];
location = location.substr(0, 127);
m_locationCity[location.length()] = '\0';
memcpy(m_locationCity, location.c_str(), location.length());
m_locationLat = std::stof(lines[7]);
m_locationLon = std::stof(lines[8]);
// Force recalculation next time
m_lastCalculatedDay = 0xFFFFFFFF;
}
std::ostringstream url;
url << "http://wttr.in/" << m_locationLat << "," << m_locationLon << "?0Q&format=%x:%t:%h:%w";
// Grr. wttr.in does a redirect to HTTPS if your agent isn't curl
client.setUserAgent("curl/8.0.1");
client.begin(url.str().c_str());
int httpCode = client.GET();
m_features.wifi.Disconnect();
if (httpCode != 200) {
m_lastSyncAttemptTime = currentTime;
return;
}
String payload = client.getString();
client.end();
std::string reponse(payload.c_str());
// Split on :
std::vector<std::string> parts;
std::string::size_type pos = 0;
std::string::size_type prev = 0;
char delimiter = ':';
while ((pos = reponse.find(delimiter, prev)) != std::string::npos) {
parts.push_back(reponse.substr(prev, pos - prev));
prev = pos + 1;
}
parts.push_back(reponse.substr(prev));
if (parts.size() != 4) {
m_lastSyncAttemptTime = currentTime;
return;
}
memcpy(m_currentWeatherCondition, parts[0].c_str(), parts[0].length());
std::string temperature = parts[2].substr(0, parts[2].length() - 2);
m_currentTemperature = std::stoi(temperature);
std::string humidity = parts[2].substr(0, parts[2].length() - 1);
m_currentHumidity = std::stoi(humidity);
std::string windSpeed = parts[3].substr(3, parts[3].length() - 7);
float windSpeedKmh = std::stof(windSpeed);
// Convert to m/s
m_currentWindSpeed = std::round(windSpeedKmh * 0.277778);
m_features.wifi.Disconnect();
m_lastSyncTime = currentTime;
}
void WatchFacePages::Weather::Recalc()
{
tmElements_t tm;
m_features.rtc.Get(tm);
if (m_lastCalculatedDay == tm.Day) {
return;
}
unsigned int dayOfYear = m_features.rtc.GetDayOfYear(tm);
double denominator = tm.Year / 4 ? 366.0 : 365.0;
double fractionalYear = (2.0 * M_PI / denominator) * (dayOfYear - 1 + ((0 - 12.0) / 24.0));
double eqtime = 229.18 * (0.000075 + 0.001868 * cos(fractionalYear) - 0.032077 * sin(fractionalYear) - 0.014615 * cos(2.0 * fractionalYear) - 0.040849 * sin(2.0 * fractionalYear));
double declination = 0.006918 - 0.399912 * cos(fractionalYear) + 0.070257 * sin(fractionalYear) - 0.006758 * cos(2.0 * fractionalYear) + 0.000907 * sin(2.0 * fractionalYear) - 0.002697 * cos(3.0 * fractionalYear) + 0.00148 * sin(3.0 * fractionalYear);
const double latitudeRadians = (double)m_locationLat * M_PI / 180.0;
const double longitudeRadians = (double)m_locationLon * M_PI / 180.0;
const double sunriseSunsetAngle = 1.5853349200000815;
double timeOffset = eqtime + 4.0 * (double)m_locationLon - 60.0;
double trueSolarTime = tm.Hour * 60.0 + tm.Minute + tm.Second / 60.0 + timeOffset;
double hourAngle = acos(cos(sunriseSunsetAngle) / (cos(latitudeRadians) * cos(declination)) - tan(latitudeRadians) * tan(declination));
hourAngle = hourAngle * 180.0 / M_PI;
double sunrise = 720 - 4.0 * ((double)m_locationLon + hourAngle) - eqtime;
double sunset = 720 - 4.0 * ((double)m_locationLon - hourAngle) - eqtime;
double noon = 720 - 4.0 * ((double)m_locationLon) - eqtime;
sunrise += m_features.storage.GetTzOffset() / 60;
sunset += m_features.storage.GetTzOffset() / 60;
noon += m_features.storage.GetTzOffset() / 60;
m_sunriseHour = sunrise / 60;
m_sunriseMinute = sunrise - m_sunriseHour * 60;
m_sunsetHour = sunset / 60;
m_sunsetMinute = sunset - m_sunsetHour * 60;
m_noonHour = noon / 60;
m_noonMinute = noon - m_noonHour * 60;
tmElements_t newMoon;
newMoon.Year = 2023 - 1970;
newMoon.Month = 5;
newMoon.Day = 19;
unsigned int daysDifference = m_features.rtc.DaysDifference(newMoon, tm);
m_moonPhase = fmod((double)daysDifference, 29.530588853) / 29.530588853;
// Draw a 1-bit bitmap with the moon phase
bool pixels[64 * 64];
for (int i = 0; i < 64 * 64; i++) {
pixels[i] = false;
}
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
float distance = sqrt(powf(fabsf(x - 32), 2) + powf(fabsf(y - 32), 2));
if (distance < 32) {
float inclination = asinf(((float)y - 32.0f) / 32.0f);
float azimuth = acosf((float)(x - 32) / (-32.0f * cosf(inclination)));
azimuth += M_PI;
azimuth += (m_moonPhase) * M_PI * 2.0f;
while (azimuth > M_PI * 2.0f) {
azimuth -= M_PI * 2.0f;
}
while (azimuth < 0) {
azimuth += M_PI * 2.0f;
}
if (azimuth > M_PI) {
pixels[x + y * 64] = true;
}
} else {
pixels[x + y * 64] = false;
}
}
}
// Needs to be split into two parts because pio does too aggressive optimisation
for (unsigned int y = 0; y < 32; y++) {
for (unsigned int x = 0; x < 8; x++) {
uint8_t value = 0;
for (unsigned int i = 0; i < 8; i++) {
value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i);
}
m_moonBitmap[x + y * 8] = value;
}
}
for (unsigned int y = 32; y < 64; y++) {
for (unsigned int x = 0; x < 8; x++) {
uint8_t value = 0;
for (unsigned int i = 0; i < 8; i++) {
value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i);
}
m_moonBitmap[x + y * 8] = value;
}
}
m_lastCalculatedDay = tm.Day;
}

View file

@ -0,0 +1,48 @@
#pragma once
#pragma once
#include "Page.h"
#include "../WatchyDisplay.h"
#include "../WatchFeatures/WatchFeatures.h"
#include <string>
namespace WatchFacePages
{
class Weather;
}
class WatchFacePages::Weather : public WatchFacePages::Page
{
public:
Weather(WatchyDisplay & display, WatchFeatures::WatchFeatures & features);
void InitBoot() override;
void InitWake() override;
void DrawPage(bool partialRefresh = false) override;
void Resync();
private:
void Recalc(); // Offline daily ephemeris like sunrise/sunset
WatchyDisplay & m_display;
WatchFeatures::WatchFeatures & m_features;
static RTC_DATA_ATTR uint64_t m_lastSyncTime;
static RTC_DATA_ATTR uint64_t m_lastSyncAttemptTime;
static RTC_DATA_ATTR int m_lastCalculatedDay;
static RTC_DATA_ATTR char m_locationCity[128];
static RTC_DATA_ATTR double m_locationLat, m_locationLon;
static RTC_DATA_ATTR uint8_t m_sunriseHour;
static RTC_DATA_ATTR uint8_t m_sunriseMinute;
static RTC_DATA_ATTR uint8_t m_noonHour;
static RTC_DATA_ATTR uint8_t m_noonMinute;
static RTC_DATA_ATTR uint8_t m_sunsetHour;
static RTC_DATA_ATTR uint8_t m_sunsetMinute;
static RTC_DATA_ATTR double m_moonPhase;
static RTC_DATA_ATTR uint8_t m_moonBitmap[8 * 64];
static RTC_DATA_ATTR double m_currentTemperature;
static RTC_DATA_ATTR float m_currentWindSpeed;
static RTC_DATA_ATTR uint8_t m_currentHumidity;
static RTC_DATA_ATTR char m_currentWeatherCondition[4];
};

View file

@ -0,0 +1,40 @@
#include "../config.h"
#include "Battery.h"
#include <Wire.h>
#include <Arduino.h>
#include <limits>
float WatchFeatures::Battery::m_previousVoltage = std::numeric_limits<float>::infinity();
WatchFeatures::Battery::Battery()
{
}
float WatchFeatures::Battery::GetVoltage()
{
float voltage = analogReadMilliVolts(BATT_ADC_PIN) / 1000.0f * 2.0f;
if (m_previousVoltage == std::numeric_limits<float>::infinity()) {
m_previousVoltage = voltage;
}
float averageVoltage = (m_previousVoltage + voltage) / 2.0f;
m_previousVoltage = voltage;
return averageVoltage;
}
uint8_t WatchFeatures::Battery::GetPercentage()
{
float voltage = GetVoltage();
float level = (GetVoltage() - 3.6f) / 0.6f;
if (level < 0.0f) {
level = 0.0f;
} else if (level > 1.0f) {
level = 1.0f;
}
level = level * level * (3.0f - 2.0f * level);
return std::round(level * 100.0f);
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <stdint.h>
namespace WatchFeatures
{
class Battery;
}
class WatchFeatures::Battery
{
public:
Battery();
float GetVoltage();
uint8_t GetPercentage();
private:
static float m_previousVoltage;
};

View file

@ -1,49 +1,46 @@
#include "WatchyRTC.h" #include "RTC.h"
#include <EEPROM.h>
#if (UPDATE_INTERVAL > 255) #if (UPDATE_INTERVAL > 255)
#error "UPDATE_INTERVAL must be either a multiple of 60, or less than 256 seconds" #error "UPDATE_INTERVAL must be either a multiple of 60, or less than 256 seconds"
#endif #endif
RTC_DATA_ATTR bool WatchyRTC::m_timerSet = false; RTC_DATA_ATTR bool WatchFeatures::RTC::m_timerSet = false;
RTC_DATA_ATTR bool WatchyRTC::m_initialTimer = true; RTC_DATA_ATTR bool WatchFeatures::RTC::m_initialTimer = true;
WatchyRTC::WatchyRTC() void WatchFeatures::RTC::Get(tmElements_t & tm)
{ {
m_rtcPcf.getDateTime();
tm.Year = y2kYearToTm(m_rtcPcf.getYear());
tm.Month = m_rtcPcf.getMonth();
tm.Day = m_rtcPcf.getDay();
tm.Wday = m_rtcPcf.getWeekday() + 1; // TimeLib has Wday range of 1-7, but PCF8563 stores day of week in 0-6 range
tm.Hour = m_rtcPcf.getHour();
tm.Minute = m_rtcPcf.getMinute();
tm.Second = m_rtcPcf.getSecond();
} }
void WatchyRTC::Init() void WatchFeatures::RTC::Set(tmElements_t tm)
{
}
void WatchyRTC::Get(tmElements_t & tm, int offsetInSeconds)
{
rtc_pcf.getDateTime();
tm.Year = y2kYearToTm(rtc_pcf.getYear());
tm.Month = rtc_pcf.getMonth();
tm.Day = rtc_pcf.getDay();
tm.Wday = rtc_pcf.getWeekday() + 1; // TimeLib 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();
OffsetTime(tm, offsetInSeconds);
}
void WatchyRTC::Set(tmElements_t tm)
{ {
time_t t = makeTime(tm); // make and break to calculate tm.Wday time_t t = makeTime(tm); // make and break to calculate tm.Wday
breakTime(t, tm); breakTime(t, tm);
// day, weekday, month, century(1=1900, 0=2000), year(0-99) // day, weekday, month, century(1=1900, 0=2000), year(0-99)
rtc_pcf.setDateTime(tm.Day, tm.Wday - 1, tm.Month, 0, tmYearToY2k(tm.Year), tm.Hour, tm.Minute, tm.Second); m_rtcPcf.setDateTime(tm.Day, tm.Wday - 1, tm.Month, 0, tmYearToY2k(tm.Year), tm.Hour, tm.Minute, tm.Second);
} }
void WatchyRTC::SetTimer() uint64_t WatchFeatures::RTC::GetTimestamp()
{
return m_rtcPcf.getTimestamp64();
}
void WatchFeatures::RTC::SetTimer()
{ {
if (!m_timerSet) { if (!m_timerSet) {
Resync(); Resync();
} }
} }
void WatchyRTC::OffsetTime(tmElements_t & tm, int offsetInSeconds) void WatchFeatures::RTC::OffsetTime(tmElements_t & tm, int offsetInSeconds)
{ {
int year = tm.Year; int year = tm.Year;
int month = tm.Month; int month = tm.Month;
@ -105,10 +102,55 @@ void WatchyRTC::OffsetTime(tmElements_t & tm, int offsetInSeconds)
tm.Hour = hour; tm.Hour = hour;
tm.Minute = minute; tm.Minute = minute;
tm.Second = second; tm.Second = second;
time_t t = makeTime(tm); // make and break to calculate tm.Wday
breakTime(t, tm);
} }
unsigned int WatchFeatures::RTC::GetDayOfYear(tmElements_t & tm)
{
unsigned int dayOfYear = 1;
for (int i = 1; i < tm.Month; i++) {
switch (i) {
case 2:
dayOfYear += (tm.Year % 4 == 0) ? 29 : 28;
break;
case 4:
case 6:
case 9:
case 11:
dayOfYear += 30;
break;
default:
dayOfYear += 31;
break;
}
}
dayOfYear += tm.Day;
return dayOfYear;
}
unsigned int WatchFeatures::RTC::DaysDifference(tmElements_t & tm1, tmElements_t & tm2)
{
unsigned int dayOfYear1 = GetDayOfYear(tm1);
unsigned int dayOfYear2 = GetDayOfYear(tm2);
unsigned int days = 0;
if (tm1.Year == tm2.Year) {
days = dayOfYear2 - dayOfYear1;
} else {
days = 365 - dayOfYear1 + dayOfYear2;
for (int i = tm1.Year + 1; i < tm2.Year; i++) {
days += (i % 4 == 0) ? 366 : 365;
}
}
return days;
}
// TODO: implement more advanced wakeup logic, i.e. > 255 seconds that are not a multiple of 60 // TODO: implement more advanced wakeup logic, i.e. > 255 seconds that are not a multiple of 60
bool WatchyRTC::CheckWakeup() bool WatchFeatures::RTC::CheckWakeup()
{ {
if(m_initialTimer) { if(m_initialTimer) {
m_initialTimer = false; m_initialTimer = false;
@ -123,8 +165,8 @@ bool WatchyRTC::CheckWakeup()
} }
// Timer doesn't work reliably unless it's cleared first // Timer doesn't work reliably unless it's cleared first
rtc_pcf.clearTimer(); m_rtcPcf.clearTimer();
rtc_pcf.setTimer(interval, frequency, true); m_rtcPcf.setTimer(interval, frequency, true);
return true; return true;
} }
@ -132,18 +174,23 @@ bool WatchyRTC::CheckWakeup()
return true; return true;
} }
void WatchyRTC::Resync() std::string WatchFeatures::RTC::GetDateString()
{ {
rtc_pcf.getDateTime(); return std::string(m_rtcPcf.formatDate(RTCC_DATE_ISO8601));
}
void WatchFeatures::RTC::Resync()
{
m_rtcPcf.getDateTime();
// Sleep just long enough to get to a multiple of the update interval, makes updates happen on exact turn of the minute // Sleep just long enough to get to a multiple of the update interval, makes updates happen on exact turn of the minute
int seconds = UPDATE_INTERVAL - (rtc_pcf.getSecond() % UPDATE_INTERVAL); int seconds = UPDATE_INTERVAL - (m_rtcPcf.getSecond() % UPDATE_INTERVAL);
if (seconds < 0) { if (seconds < 0) {
seconds = 0; seconds = 0;
} }
// Timer doesn't work reliably unless it's cleared first // Timer doesn't work reliably unless it's cleared first
rtc_pcf.clearTimer(); m_rtcPcf.clearTimer();
if (seconds == 0) { if (seconds == 0) {
// If there's no time to wait, just call CheckWakeup() immediately and it'll set the repeating timer // If there's no time to wait, just call CheckWakeup() immediately and it'll set the repeating timer
@ -151,7 +198,7 @@ void WatchyRTC::Resync()
m_initialTimer = true; m_initialTimer = true;
CheckWakeup(); CheckWakeup();
} else { } else {
rtc_pcf.setTimer(seconds, TMR_1Hz, false); m_rtcPcf.setTimer(seconds, TMR_1Hz, false);
m_timerSet = true; m_timerSet = true;
m_initialTimer = true; m_initialTimer = true;

View file

@ -6,25 +6,27 @@
#include <TimeLib.h> #include <TimeLib.h>
#include <Rtc_Pcf8563.h> #include <Rtc_Pcf8563.h>
#define RTC_PCF_ADDR 0x51 namespace WatchFeatures
#define YEAR_OFFSET_PCF 2000 {
class RTC;
}
class WatchyRTC { class WatchFeatures::RTC {
public: public:
Rtc_Pcf8563 rtc_pcf; void Get(tmElements_t & tm);
public:
WatchyRTC();
void Init();
void Get(tmElements_t & tm, int offsetInSeconds = 0);
void Set(tmElements_t tm); void Set(tmElements_t tm);
uint64_t GetTimestamp();
std::string GetDateString();
void SetTimer(); void SetTimer();
bool CheckWakeup(); // Checks to really wake up or not, also resets the timer after the initial sleep bool CheckWakeup(); // Checks to really wake up or not, also resets the timer after the initial sleep
void Resync(); // Resync the timer cycle, both initially and after RTC resync void Resync(); // Resync the timer cycle, both initially and after RTC resync
static void OffsetTime(tmElements_t & tm, int offsetInSeconds); static void OffsetTime(tmElements_t & tm, int offsetInSeconds);
static unsigned int GetDayOfYear(tmElements_t & tm);
static unsigned int DaysDifference(tmElements_t & tm1, tmElements_t & tm2);
private: private:
Rtc_Pcf8563 m_rtcPcf;
static RTC_DATA_ATTR bool m_timerSet, m_initialTimer; static RTC_DATA_ATTR bool m_timerSet, m_initialTimer;
}; };

View file

@ -0,0 +1,16 @@
#include "StepCounter.h"
WatchFeatures::StepCounter::StepCounter(BMA423 & sensor) :
m_sensor(sensor)
{
}
uint64_t WatchFeatures::StepCounter::GetSteps()
{
return m_sensor.getCounter();
}
void WatchFeatures::StepCounter::ResetSteps()
{
m_sensor.resetStepCounter();
}

View file

@ -0,0 +1,20 @@
#pragma once
#include <stdint.h>
#include "../bma.h"
namespace WatchFeatures
{
class StepCounter;
}
class WatchFeatures::StepCounter
{
public:
StepCounter(BMA423 & sensor);
uint64_t GetSteps();
void ResetSteps();
private:
BMA423 & m_sensor;
};

View file

@ -0,0 +1,58 @@
#include "../config.h"
#include "Storage.h"
#include <EEPROM.h>
WatchFeatures::Storage::Storage()
{
}
void WatchFeatures::Storage::InitBoot()
{
EEPROM.begin(512);
if (EEPROM.read(EEPROM_LOCATION_MAGIC) != EEPROM_MAGIC1
|| EEPROM.read(EEPROM_LOCATION_MAGIC + 1) != EEPROM_MAGIC2)
{
Serial.println("Initializing EEPROM");
EEPROM.write(EEPROM_LOCATION_MAGIC, EEPROM_MAGIC1);
EEPROM.write(EEPROM_LOCATION_MAGIC + 1, EEPROM_MAGIC2);
uint16_t version = EEPROM_VERSION;
EEPROM.write(EEPROM_LOCATION_VERSION, version & 0xFF);
EEPROM.write(EEPROM_LOCATION_VERSION + 1, version >> 8);
int offset = DEFAULT_TZ_OFFSET;
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET, offset & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 1, (offset >> 8) & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 2, (offset >> 16) & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 3, (offset >> 24) & 0xFF);
EEPROM.commit();
}
uint16_t version = EEPROM.read(EEPROM_LOCATION_VERSION) | (EEPROM.read(EEPROM_LOCATION_VERSION + 1) << 8);
if (version != EEPROM_VERSION)
{
// TODO: Handle version mismatch
}
}
void WatchFeatures::Storage::InitWake()
{
EEPROM.begin(512);
}
int32_t WatchFeatures::Storage::GetTzOffset()
{
return EEPROM.read(EEPROM_LOCATION_TZ_OFFSET)
| (EEPROM.read(EEPROM_LOCATION_TZ_OFFSET + 1) << 8)
| (EEPROM.read(EEPROM_LOCATION_TZ_OFFSET + 2) << 16)
| (EEPROM.read(EEPROM_LOCATION_TZ_OFFSET + 3) << 24);
}
void WatchFeatures::Storage::SetTzOffset(int offset)
{
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET, offset & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 1, (offset >> 8) & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 2, (offset >> 16) & 0xFF);
EEPROM.write(EEPROM_LOCATION_TZ_OFFSET + 3, (offset >> 24) & 0xFF);
EEPROM.commit();
}

View file

@ -0,0 +1,16 @@
#pragma once
namespace WatchFeatures
{
class Storage;
}
class WatchFeatures::Storage
{
public:
Storage();
void InitBoot();
void InitWake();
int GetTzOffset();
void SetTzOffset(int offset);
};

View file

@ -0,0 +1,26 @@
#pragma once
#include "Battery.h"
#include "StepCounter.h"
#include "RTC.h"
#include "Storage.h"
#include "Wifi.h"
namespace WatchFeatures
{
struct WatchFeatures;
}
struct WatchFeatures::WatchFeatures
{
WatchFeatures(BMA423 & sensor)
: battery(), stepCounter(sensor), rtc()
{
}
Battery battery;
StepCounter stepCounter;
RTC rtc;
Storage storage;
Wifi wifi;
};

View file

@ -0,0 +1,28 @@
#include <WiFi.h>
#include "Wifi.h"
#include "config.h"
bool WatchFeatures::Wifi::Connect()
{
if (WiFi.begin(WIFI_SSID, WIFI_PASS) == WL_CONNECT_FAILED) {
WiFi.mode(WIFI_OFF);
return false;
}
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
WiFi.mode(WIFI_OFF);
return false;
}
return true;
}
void WatchFeatures::Wifi::Disconnect()
{
}
bool WatchFeatures::Wifi::IsConnected()
{
return true;
}

12
src/WatchFeatures/Wifi.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
namespace WatchFeatures
{
class Wifi
{
public:
bool Connect();
void Disconnect();
bool IsConnected();
};
}

View file

@ -3,44 +3,41 @@
WatchyDisplayBase Watchy::m_displayBase; WatchyDisplayBase Watchy::m_displayBase;
WatchyDisplay Watchy::m_display(Watchy::m_displayBase); WatchyDisplay Watchy::m_display(Watchy::m_displayBase);
WatchyRTC Watchy::m_RTC;
RTC_DATA_ATTR BMA423 Watchy::m_sensor; RTC_DATA_ATTR BMA423 Watchy::m_sensor;
RTC_DATA_ATTR bool g_displayFullInit = true; RTC_DATA_ATTR bool g_displayFullInit = true;
Watchy::Watchy() Watchy::Watchy()
: m_features(m_sensor)
{ {
} }
void Watchy::Init() void Watchy::Wake()
{ {
esp_sleep_wakeup_cause_t wakeup_reason; esp_sleep_wakeup_cause_t wakeup_reason;
wakeup_reason = esp_sleep_get_wakeup_cause(); wakeup_reason = esp_sleep_get_wakeup_cause();
if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) { if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) {
if(!m_RTC.CheckWakeup()) { if(!m_features.rtc.CheckWakeup()) {
DeepSleep(); DeepSleep();
} }
} }
Wire.begin(SDA, SCL);
m_display.epd2.selectSPI(SPI, SPISettings(20000000, MSBFIRST, SPI_MODE0));
m_display.init(0, g_displayFullInit, 10, true);
SetBusyCallback();
Setup();
switch(wakeup_reason) { switch(wakeup_reason) {
case ESP_SLEEP_WAKEUP_EXT0: case ESP_SLEEP_WAKEUP_EXT0:
{ {
// RTC interrupt // RTC interrupt
InitWakeInternal();
ShowWatchFace(true); ShowWatchFace(true);
break; break;
} }
case ESP_SLEEP_WAKEUP_EXT1: case ESP_SLEEP_WAKEUP_EXT1:
{ {
// Button press or accelerometer interrupt
InitWakeInternal();
uint64_t wakeupBit = esp_sleep_get_ext1_wakeup_status(); uint64_t wakeupBit = esp_sleep_get_ext1_wakeup_status();
if (wakeupBit & ACC_INT_MASK) { if (wakeupBit & ACC_INT_MASK) {
// Accelerometer interrupt
m_sensor.getINT(); m_sensor.getINT();
uint8_t irqMask = m_sensor.getIRQMASK(); uint8_t irqMask = m_sensor.getIRQMASK();
@ -54,24 +51,14 @@ void Watchy::Init()
m_sensor.getINT(); m_sensor.getINT();
} else if (wakeupBit & BTN_PIN_MASK) { } else if (wakeupBit & BTN_PIN_MASK) {
// Button press // Button press
HandleButtonPress(wakeupBit); HandleButtonPress(wakeupBit);
break;
} }
// Button press
HandleButtonPress(wakeupBit);
break; break;
} }
default: default:
{ {
BmaConfig(); InitBootInternal();
m_RTC.Init();
ConnectWiFi();
SyncNTPTime();
DisconnectWiFi();
ShowWatchFace(false); ShowWatchFace(false);
break; break;
} }
@ -102,7 +89,7 @@ void Watchy::DeepSleep()
BTN_PIN_MASK | ACC_INT_MASK, BTN_PIN_MASK | ACC_INT_MASK,
ESP_EXT1_WAKEUP_ANY_HIGH); ESP_EXT1_WAKEUP_ANY_HIGH);
m_RTC.SetTimer(); m_features.rtc.SetTimer();
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
@ -125,58 +112,23 @@ void Watchy::VibeMotor(uint8_t intervalMs, uint8_t length)
} }
} }
float Watchy::GetBatteryVoltage()
{
return analogReadMilliVolts(BATT_ADC_PIN) / 1000.0f * 2.0f;
}
uint64_t Watchy::GetSteps()
{
return m_sensor.getCounter();
}
void Watchy::ResetSteps()
{
m_sensor.resetStepCounter();
}
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() void Watchy::SyncNTPTime()
{ {
WiFiUDP ntpUDP; WiFiUDP ntpUDP;
// GMT offset should be 0, RTC class will adjust to local time // GMT offset should be 0, since RTC is set to UTC
NTPClient timeClient(ntpUDP, NTP_SERVER, 0); NTPClient timeClient(ntpUDP, NTP_SERVER, 0);
timeClient.begin(); timeClient.begin();
bool success = timeClient.forceUpdate(); bool success = timeClient.forceUpdate();
if (success) { if (success) {
tmElements_t tm; tmElements_t tm;
breakTime((time_t)timeClient.getEpochTime(), tm); breakTime((time_t)timeClient.getEpochTime(), tm);
m_RTC.Set(tm); m_features.rtc.Set(tm);
} else { } else {
Serial.begin(9600); Serial.begin(9600);
Serial.println("Failed to get NTP time"); Serial.println("Failed to get NTP time");
} }
} }
void Watchy::DisconnectWiFi()
{
WiFi.mode(WIFI_OFF);
}
void Watchy::ShowWatchFace(bool partialRefresh) void Watchy::ShowWatchFace(bool partialRefresh)
{ {
DrawWatchFace(partialRefresh); DrawWatchFace(partialRefresh);
@ -187,6 +139,29 @@ void Watchy::ClearBusyCallback()
m_display.epd2.setBusyCallback(nullptr); m_display.epd2.setBusyCallback(nullptr);
} }
void Watchy::InitBootInternal()
{
Wire.begin(SDA, SCL);
m_display.epd2.selectSPI(SPI, SPISettings(20000000, MSBFIRST, SPI_MODE0));
m_display.init(0, g_displayFullInit, 10, true);
SetBusyCallback();
BmaConfig();
m_features.storage.InitBoot();
InitBoot();
}
void Watchy::InitWakeInternal()
{
Wire.begin(SDA, SCL);
m_display.epd2.selectSPI(SPI, SPISettings(20000000, MSBFIRST, SPI_MODE0));
m_display.init(0, g_displayFullInit, 10, true);
SetBusyCallback();
m_features.storage.InitWake();
InitWake();
}
void Watchy::SetBusyCallback() void Watchy::SetBusyCallback()
{ {
m_display.epd2.setBusyCallback(DisplayBusyCallback); m_display.epd2.setBusyCallback(DisplayBusyCallback);
@ -309,3 +284,4 @@ void Watchy::BmaConfig()
m_sensor.enableWakeupInterrupt(); m_sensor.enableWakeupInterrupt();
} }
} }

View file

@ -2,7 +2,7 @@
#include "config.h" #include "config.h"
#include "WatchyDisplay.h" #include "WatchyDisplay.h"
#include "WatchyRTC.h" #include "WatchFeatures/WatchFeatures.h"
#include <WiFi.h> #include <WiFi.h>
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <NTPClient.h> #include <NTPClient.h>
@ -12,12 +12,10 @@ class Watchy
{ {
public: public:
Watchy(); Watchy();
void Init(); void Wake();
void DeepSleep(); void DeepSleep();
void VibeMotor(uint8_t intervalMs = 100, uint8_t length = 20); void VibeMotor(uint8_t intervalMs = 100, uint8_t length = 20);
float GetBatteryVoltage(); float GetBatteryVoltage();
uint64_t GetSteps();
void ResetSteps();
void ConnectWiFi(); void ConnectWiFi();
void SyncNTPTime(); void SyncNTPTime();
void DisconnectWiFi(); void DisconnectWiFi();
@ -26,9 +24,8 @@ public:
void ClearBusyCallback(); void ClearBusyCallback();
void SetBusyCallback(); void SetBusyCallback();
// Called after hardware is setup virtual void InitBoot() {}; // Called on first boot
virtual void Setup() = 0; virtual void InitWake() {}; // Called every time the watch wakes from sleep
virtual void HandleButtonPress(uint64_t buttonMask) = 0; virtual void HandleButtonPress(uint64_t buttonMask) = 0;
virtual void HandleDoubleTap() {} virtual void HandleDoubleTap() {}
virtual void HandleTilt() {} virtual void HandleTilt() {}
@ -36,13 +33,15 @@ public:
protected: protected:
void InitBootInternal();
void InitWakeInternal();
static void DisplayBusyCallback(const void *); static void DisplayBusyCallback(const void *);
static WatchyDisplayBase m_displayBase; static WatchyDisplayBase m_displayBase;
static WatchyDisplay m_display; static WatchyDisplay m_display;
static WatchyRTC m_RTC;
static RTC_DATA_ATTR BMA423 m_sensor; static RTC_DATA_ATTR BMA423 m_sensor;
WatchFeatures::WatchFeatures m_features;
private: private:
void BmaConfig(); void BmaConfig();

1469
src/WeatherIcons.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@
#define ACC_INT_2_PIN 12 #define ACC_INT_2_PIN 12
#define VIB_MOTOR_PIN 13 #define VIB_MOTOR_PIN 13
#define RTC_INT_PIN 27 #define RTC_INT_PIN 27
#define RTC_PCF_ADDR 0x51
#define YEAR_OFFSET_PCF 2000
#define MENU_BTN_MASK GPIO_SEL_26 #define MENU_BTN_MASK GPIO_SEL_26
#define BACK_BTN_MASK GPIO_SEL_25 #define BACK_BTN_MASK GPIO_SEL_25
@ -24,11 +26,30 @@
#define DISPLAY_WIDTH 200 #define DISPLAY_WIDTH 200
#define DISPLAY_HEIGHT 200 #define DISPLAY_HEIGHT 200
#define WIFI_SSID "<ssid>" #define EEPROM_LOCATION_MAGIC 0
#define WIFI_PASS "<pass>" #define EEPROM_LOCATION_VERSION 2
#define TZ_OFFSET 3600 * 3 #define EEPROM_LOCATION_TZ_OFFSET 4
#define EEPROM_MAGIC1 0xf0
#define EEPROM_MAGIC2 0x0d
#define EEPROM_VERSION 1
#define DEFAULT_TZ_OFFSET 3600 * 3
#define NTP_SERVER "pool.ntp.org" #define NTP_SERVER "pool.ntp.org"
#define UPDATE_INTERVAL 60 // seconds #define UPDATE_INTERVAL 60 // seconds
#define WAKE_ON_ACCEL_EVENTS false // useful if saving battery by not updating every minute #define WAKE_ON_ACCEL_EVENTS false // useful if saving battery by not updating every minute
#define WEATHER_UPDATE_INTERVAL 3600 // seconds
#define WEATHER_UPDATE_BACKOFF 900 // If weather update fails, how long to wait before trying again
#define DO_GEOLOCATION true // if false then use defaults below
#define DEFAULT_LATITUDE 60.170833
#define DEFAULT_LONGITUDE 24.9375
#define DEFAULT_CITY_NAME "Helsinki"
#include "secrets.h"
#if !defined(WIFI_SSID) || !defined(WIFI_PASS)
#error "Please define WIFI_SSID and WIFI_PASS in secrets.h"
#endif