#include "Watchy.h" WatchyRTC Watchy::RTC; GxEPD2_BW Watchy::display(GxEPD2_154_D67(CS, DC, RESET, BUSY)); RTC_DATA_ATTR int guiState; RTC_DATA_ATTR int menuIndex; RTC_DATA_ATTR BMA423 sensor; RTC_DATA_ATTR bool WIFI_CONFIGURED; RTC_DATA_ATTR bool BLE_CONFIGURED; RTC_DATA_ATTR weatherData currentWeather; RTC_DATA_ATTR int weatherIntervalCounter = WEATHER_UPDATE_INTERVAL; RTC_DATA_ATTR bool displayFullInit = true; Watchy::Watchy(){} //constructor void Watchy::init(String datetime){ esp_sleep_wakeup_cause_t wakeup_reason; wakeup_reason = esp_sleep_get_wakeup_cause(); //get wake up reason Wire.begin(SDA, SCL); //init i2c RTC.init(); // Init the display here for all cases, if unused, it will do nothing display.init(0, displayFullInit, 10, true); // 10ms by spec, and fast pulldown reset display.epd2.setBusyCallback(displayBusyCallback); switch (wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT0: //RTC Alarm if(guiState == WATCHFACE_STATE){ RTC.read(currentTime); showWatchFace(true); //partial updates on tick } break; case ESP_SLEEP_WAKEUP_EXT1: //button Press handleButtonPress(); break; default: //reset RTC.config(datetime); _bmaConfig(); RTC.read(currentTime); showWatchFace(false); //full update on reset break; } deepSleep(); } void Watchy::displayBusyCallback(const void*){ gpio_wakeup_enable((gpio_num_t)BUSY, GPIO_INTR_LOW_LEVEL); esp_sleep_enable_gpio_wakeup(); esp_light_sleep_start(); } void Watchy::deepSleep(){ display.hibernate(); displayFullInit = false; // Notify not to init it again RTC.clearAlarm(); //resets the alarm flag in the RTC // Set pins 0-39 to input to avoid power leaking out for(int i=0; i<40; i++) { pinMode(i, INPUT); } esp_sleep_enable_ext0_wakeup(RTC_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::handleButtonPress(){ uint64_t wakeupBit = esp_sleep_get_ext1_wakeup_status(); //Menu Button if (wakeupBit & MENU_BTN_MASK){ if(guiState == WATCHFACE_STATE){//enter menu state if coming from watch face showMenu(menuIndex, false); }else if(guiState == MAIN_MENU_STATE){//if already in menu, then select menu item switch(menuIndex) { case 0: showBattery(); break; case 1: showBuzz(); break; case 2: showAccelerometer(); break; case 3: setTime(); break; case 4: setupWifi(); break; case 5: showUpdateFW(); break; case 6: showSyncNTP(); break; default: break; } }else if(guiState == FW_UPDATE_STATE){ updateFWBegin(); } } //Back Button else if (wakeupBit & BACK_BTN_MASK){ if(guiState == MAIN_MENU_STATE){//exit to watch face if already in menu RTC.read(currentTime); showWatchFace(false); }else if(guiState == APP_STATE){ showMenu(menuIndex, false);//exit to menu if already in app }else if(guiState == FW_UPDATE_STATE){ showMenu(menuIndex, false);//exit to menu if already in app }else if(guiState == WATCHFACE_STATE){ return; } } //Up Button else if (wakeupBit & UP_BTN_MASK){ if(guiState == MAIN_MENU_STATE){//increment menu index menuIndex--; if(menuIndex < 0){ menuIndex = MENU_LENGTH - 1; } showMenu(menuIndex, true); }else if(guiState == WATCHFACE_STATE){ return; } } //Down Button else if (wakeupBit & DOWN_BTN_MASK){ if(guiState == MAIN_MENU_STATE){//decrement menu index menuIndex++; if(menuIndex > MENU_LENGTH - 1){ menuIndex = 0; } showMenu(menuIndex, true); }else if(guiState == WATCHFACE_STATE){ return; } } /***************** fast menu *****************/ bool timeout = false; long lastTimeout = millis(); pinMode(MENU_BTN_PIN, INPUT); pinMode(BACK_BTN_PIN, INPUT); pinMode(UP_BTN_PIN, INPUT); pinMode(DOWN_BTN_PIN, INPUT); while(!timeout){ if(millis() - lastTimeout > 5000){ timeout = true; }else{ if(digitalRead(MENU_BTN_PIN) == 1){ lastTimeout = millis(); if(guiState == MAIN_MENU_STATE){//if already in menu, then select menu item switch(menuIndex) { case 0: showBattery(); break; case 1: showBuzz(); break; case 2: showAccelerometer(); break; case 3: setTime(); break; case 4: setupWifi(); break; case 5: showUpdateFW(); break; case 6: showSyncNTP(); break; default: break; } }else if(guiState == FW_UPDATE_STATE){ updateFWBegin(); } }else if(digitalRead(BACK_BTN_PIN) == 1){ lastTimeout = millis(); if(guiState == MAIN_MENU_STATE){//exit to watch face if already in menu RTC.read(currentTime); showWatchFace(false); break; //leave loop }else if(guiState == APP_STATE){ showMenu(menuIndex, false);//exit to menu if already in app }else if(guiState == FW_UPDATE_STATE){ showMenu(menuIndex, false);//exit to menu if already in app } }else if(digitalRead(UP_BTN_PIN) == 1){ lastTimeout = millis(); if(guiState == MAIN_MENU_STATE){//increment menu index menuIndex--; if(menuIndex < 0){ menuIndex = MENU_LENGTH - 1; } showFastMenu(menuIndex); } }else if(digitalRead(DOWN_BTN_PIN) == 1){ lastTimeout = millis(); if(guiState == MAIN_MENU_STATE){//decrement menu index menuIndex++; if(menuIndex > MENU_LENGTH - 1){ menuIndex = 0; } showFastMenu(menuIndex); } } } } } void Watchy::showMenu(byte menuIndex, bool partialRefresh){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); int16_t x1, y1; uint16_t w, h; int16_t yPos; const char *menuItems[] = {"Check Battery", "Vibrate Motor", "Show Accelerometer", "Set Time", "Setup WiFi", "Update Firmware", "Sync NTP"}; for(int i=0; i SET_DAY){ break; } } if(digitalRead(BACK_BTN_PIN) == 1){ if(setIndex != SET_HOUR){ setIndex--; } } blink = 1 - blink; if(digitalRead(DOWN_BTN_PIN) == 1){ blink = 1; switch(setIndex){ case SET_HOUR: hour == 23 ? (hour = 0) : hour++; break; case SET_MINUTE: minute == 59 ? (minute = 0) : minute++; break; case SET_YEAR: year == 99 ? (year = 0) : year++; break; case SET_MONTH: month == 12 ? (month = 1) : month++; break; case SET_DAY: day == 31 ? (day = 1) : day++; break; default: break; } } if(digitalRead(UP_BTN_PIN) == 1){ blink = 1; switch(setIndex){ case SET_HOUR: hour == 0 ? (hour = 23) : hour--; break; case SET_MINUTE: minute == 0 ? (minute = 59) : minute--; break; case SET_YEAR: year == 0 ? (year = 99) : year--; break; case SET_MONTH: month == 1 ? (month = 12) : month--; break; case SET_DAY: day == 1 ? (day = 31) : day--; break; default: break; } } display.fillScreen(GxEPD_BLACK); display.setTextColor(GxEPD_WHITE); display.setFont(&DSEG7_Classic_Bold_53); display.setCursor(5, 80); if(setIndex == SET_HOUR){//blink hour digits display.setTextColor(blink ? GxEPD_WHITE : GxEPD_BLACK); } if(hour < 10){ display.print("0"); } display.print(hour); display.setTextColor(GxEPD_WHITE); display.print(":"); display.setCursor(108, 80); if(setIndex == SET_MINUTE){//blink minute digits display.setTextColor(blink ? GxEPD_WHITE : GxEPD_BLACK); } if(minute < 10){ display.print("0"); } display.print(minute); display.setTextColor(GxEPD_WHITE); display.setFont(&FreeMonoBold9pt7b); display.setCursor(45, 150); if(setIndex == SET_YEAR){//blink minute digits display.setTextColor(blink ? GxEPD_WHITE : GxEPD_BLACK); } display.print(2000+year); display.setTextColor(GxEPD_WHITE); display.print("/"); if(setIndex == SET_MONTH){//blink minute digits display.setTextColor(blink ? GxEPD_WHITE : GxEPD_BLACK); } if(month < 10){ display.print("0"); } display.print(month); display.setTextColor(GxEPD_WHITE); display.print("/"); if(setIndex == SET_DAY){//blink minute digits display.setTextColor(blink ? GxEPD_WHITE : GxEPD_BLACK); } if(day < 10){ display.print("0"); } display.print(day); display.display(true); //partial refresh } tmElements_t tm; tm.Month = month; tm.Day = day; tm.Year = y2kYearToTm(year); tm.Hour = hour; tm.Minute = minute; tm.Second = 0; RTC.set(tm); showMenu(menuIndex, false); } void Watchy::showAccelerometer(){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); Accel acc; long previousMillis = 0; long interval = 200; guiState = APP_STATE; pinMode(BACK_BTN_PIN, INPUT); while(1){ unsigned long currentMillis = millis(); if(digitalRead(BACK_BTN_PIN) == 1){ break; } if(currentMillis - previousMillis > interval){ previousMillis = currentMillis; // Get acceleration data bool res = sensor.getAccel(acc); uint8_t direction = sensor.getDirection(); display.fillScreen(GxEPD_BLACK); display.setCursor(0, 30); if(res == false) { display.println("getAccel FAIL"); }else{ display.print(" X:"); display.println(acc.x); display.print(" Y:"); display.println(acc.y); display.print(" Z:"); display.println(acc.z); display.setCursor(30, 130); switch(direction){ case DIRECTION_DISP_DOWN: display.println("FACE DOWN"); break; case DIRECTION_DISP_UP: display.println("FACE UP"); break; case DIRECTION_BOTTOM_EDGE: display.println("BOTTOM EDGE"); break; case DIRECTION_TOP_EDGE: display.println("TOP EDGE"); break; case DIRECTION_RIGHT_EDGE: display.println("RIGHT EDGE"); break; case DIRECTION_LEFT_EDGE: display.println("LEFT EDGE"); break; default: display.println("ERROR!!!"); break; } } display.display(true); //full refresh } } showMenu(menuIndex, false); } void Watchy::showWatchFace(bool partialRefresh){ display.setFullWindow(); drawWatchFace(); display.display(partialRefresh); //partial refresh guiState = WATCHFACE_STATE; } void Watchy::drawWatchFace(){ display.setFont(&DSEG7_Classic_Bold_53); display.setCursor(5, 53+60); if(currentTime.Hour < 10){ display.print("0"); } display.print(currentTime.Hour); display.print(":"); if(currentTime.Minute < 10){ display.print("0"); } display.println(currentTime.Minute); } weatherData Watchy::getWeatherData(){ if(weatherIntervalCounter >= WEATHER_UPDATE_INTERVAL){ //only update if WEATHER_UPDATE_INTERVAL has elapsed i.e. 30 minutes if(connectWiFi()){ HTTPClient http; //Use Weather API for live data if WiFi is connected http.setConnectTimeout(3000);//3 second max timeout String weatherQueryURL = String(OPENWEATHERMAP_URL) + String(CITY_NAME) + String(",") + String(COUNTRY_CODE) + String("&units=") + String(TEMP_UNIT) + String("&appid=") + String(OPENWEATHERMAP_APIKEY); http.begin(weatherQueryURL.c_str()); int httpResponseCode = http.GET(); if(httpResponseCode == 200) { String payload = http.getString(); JSONVar responseObject = JSON.parse(payload); currentWeather.temperature = int(responseObject["main"]["temp"]); currentWeather.weatherConditionCode = int(responseObject["weather"][0]["id"]); }else{ //http error } http.end(); //turn off radios WiFi.mode(WIFI_OFF); btStop(); }else{//No WiFi, use internal temperature sensor uint8_t temperature = sensor.readTemperature(); //celsius if(strcmp(TEMP_UNIT, "imperial") == 0){ temperature = temperature * 9. / 5. + 32.; //fahrenheit } currentWeather.temperature = temperature; currentWeather.weatherConditionCode = 800; } weatherIntervalCounter = 0; }else{ weatherIntervalCounter++; } return currentWeather; } float Watchy::getBatteryVoltage(){ if(RTC.rtcType == DS3231){ return analogReadMilliVolts(V10_ADC_PIN) / 1000.0f * 2.0f; // Battery voltage goes through a 1/2 divider. }else{ return analogReadMilliVolts(V15_ADC_PIN) / 1000.0f * 2.0f; } } uint16_t Watchy::_readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint16_t len) { Wire.beginTransmission(address); Wire.write(reg); Wire.endTransmission(); Wire.requestFrom((uint8_t)address, (uint8_t)len); uint8_t i = 0; while (Wire.available()) { data[i++] = Wire.read(); } return 0; } uint16_t Watchy::_writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint16_t len) { Wire.beginTransmission(address); Wire.write(reg); Wire.write(data, len); return (0 != Wire.endTransmission()); } void Watchy::_bmaConfig(){ if (sensor.begin(_readRegister, _writeRegister, delay) == false) { //fail to init BMA return; } // Accel parameter structure Acfg cfg; /*! Output data rate in Hz, Optional parameters: - BMA4_OUTPUT_DATA_RATE_0_78HZ - BMA4_OUTPUT_DATA_RATE_1_56HZ - BMA4_OUTPUT_DATA_RATE_3_12HZ - BMA4_OUTPUT_DATA_RATE_6_25HZ - BMA4_OUTPUT_DATA_RATE_12_5HZ - BMA4_OUTPUT_DATA_RATE_25HZ - BMA4_OUTPUT_DATA_RATE_50HZ - BMA4_OUTPUT_DATA_RATE_100HZ - BMA4_OUTPUT_DATA_RATE_200HZ - BMA4_OUTPUT_DATA_RATE_400HZ - BMA4_OUTPUT_DATA_RATE_800HZ - BMA4_OUTPUT_DATA_RATE_1600HZ */ cfg.odr = BMA4_OUTPUT_DATA_RATE_100HZ; /*! G-range, Optional parameters: - BMA4_ACCEL_RANGE_2G - BMA4_ACCEL_RANGE_4G - BMA4_ACCEL_RANGE_8G - BMA4_ACCEL_RANGE_16G */ cfg.range = BMA4_ACCEL_RANGE_2G; /*! Bandwidth parameter, determines filter configuration, Optional parameters: - BMA4_ACCEL_OSR4_AVG1 - BMA4_ACCEL_OSR2_AVG2 - BMA4_ACCEL_NORMAL_AVG4 - BMA4_ACCEL_CIC_AVG8 - BMA4_ACCEL_RES_AVG16 - BMA4_ACCEL_RES_AVG32 - BMA4_ACCEL_RES_AVG64 - BMA4_ACCEL_RES_AVG128 */ cfg.bandwidth = BMA4_ACCEL_NORMAL_AVG4; /*! Filter performance mode , Optional parameters: - BMA4_CIC_AVG_MODE - BMA4_CONTINUOUS_MODE */ cfg.perf_mode = BMA4_CONTINUOUS_MODE; // Configure the BMA423 accelerometer sensor.setAccelConfig(cfg); // Enable BMA423 accelerometer // Warning : Need to use feature, you must first enable the accelerometer // Warning : Need to use feature, you must first enable the accelerometer sensor.enableAccel(); struct bma4_int_pin_config config ; config.edge_ctrl = BMA4_LEVEL_TRIGGER; config.lvl = BMA4_ACTIVE_HIGH; config.od = BMA4_PUSH_PULL; config.output_en = BMA4_OUTPUT_ENABLE; config.input_en = BMA4_INPUT_DISABLE; // The correct trigger interrupt needs to be configured as needed sensor.setINTPinConfig(config, BMA4_INTR1_MAP); struct bma423_axes_remap remap_data; remap_data.x_axis = 1; remap_data.x_axis_sign = 0xFF; remap_data.y_axis = 0; remap_data.y_axis_sign = 0xFF; remap_data.z_axis = 2; remap_data.z_axis_sign = 0xFF; // Need to raise the wrist function, need to set the correct axis sensor.setRemapAxes(&remap_data); // Enable BMA423 isStepCounter feature sensor.enableFeature(BMA423_STEP_CNTR, true); // Enable BMA423 isTilt feature sensor.enableFeature(BMA423_TILT, true); // Enable BMA423 isDoubleClick feature sensor.enableFeature(BMA423_WAKEUP, true); // Reset steps sensor.resetStepCounter(); // Turn on feature interrupt sensor.enableStepCountInterrupt(); sensor.enableTiltInterrupt(); // It corresponds to isDoubleClick interrupt sensor.enableWakeupInterrupt(); } void Watchy::setupWifi(){ display.epd2.setBusyCallback(0); //temporarily disable lightsleep on busy WiFiManager wifiManager; wifiManager.resetSettings(); wifiManager.setTimeout(WIFI_AP_TIMEOUT); wifiManager.setAPCallback(_configModeCallback); display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); if(!wifiManager.autoConnect(WIFI_AP_SSID)) {//WiFi setup failed display.println("Setup failed &"); display.println("timed out!"); }else{ display.println("Connected to"); display.println(WiFi.SSID()); } display.display(false); //full refresh //turn off radios WiFi.mode(WIFI_OFF); btStop(); display.epd2.setBusyCallback(displayBusyCallback); //enable lightsleep on busy guiState = APP_STATE; } void Watchy::_configModeCallback (WiFiManager *myWiFiManager) { display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Connect to"); display.print("SSID: "); display.println(WIFI_AP_SSID); display.print("IP: "); display.println(WiFi.softAPIP()); display.display(false); //full refresh } bool Watchy::connectWiFi(){ if(WL_CONNECT_FAILED == WiFi.begin()){//WiFi not setup, you can also use hard coded credentials with WiFi.begin(SSID,PASS); WIFI_CONFIGURED = false; }else{ if(WL_CONNECTED == WiFi.waitForConnectResult()){//attempt to connect for 10s WIFI_CONFIGURED = true; }else{//connection failed, time out WIFI_CONFIGURED = false; //turn off radios WiFi.mode(WIFI_OFF); btStop(); } } return WIFI_CONFIGURED; } void Watchy::showUpdateFW(){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Please visit"); display.println("watchy.sqfmi.com"); display.println("with a Bluetooth"); display.println("enabled device"); display.println(" "); display.println("Press menu button"); display.println("again when ready"); display.println(" "); display.println("Keep USB powered"); display.display(false); //full refresh guiState = FW_UPDATE_STATE; } void Watchy::updateFWBegin(){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Bluetooth Started"); display.println(" "); display.println("Watchy BLE OTA"); display.println(" "); display.println("Waiting for"); display.println("connection..."); display.display(false); //full refresh BLE BT; BT.begin("Watchy BLE OTA"); int prevStatus = -1; int currentStatus; while(1){ currentStatus = BT.updateStatus(); if(prevStatus != currentStatus || prevStatus == 1){ if(currentStatus == 0){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("BLE Connected!"); display.println(" "); display.println("Waiting for"); display.println("upload..."); display.display(false); //full refresh } if(currentStatus == 1){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Downloading"); display.println("firmware:"); display.println(" "); display.print(BT.howManyBytes()); display.println(" bytes"); display.display(true); //partial refresh } if(currentStatus == 2){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Download"); display.println("completed!"); display.println(" "); display.println("Rebooting..."); display.display(false); //full refresh delay(2000); esp_restart(); } if(currentStatus == 4){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("BLE Disconnected!"); display.println(" "); display.println("exiting..."); display.display(false); //full refresh delay(1000); break; } prevStatus = currentStatus; } delay(100); } //turn off radios WiFi.mode(WIFI_OFF); btStop(); showMenu(menuIndex, false); } void Watchy::showSyncNTP(){ display.setFullWindow(); display.fillScreen(GxEPD_BLACK); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_WHITE); display.setCursor(0, 30); display.println("Syncing NTP... "); display.display(false); //full refresh if(connectWiFi()){ if(syncNTP()){ display.println("NTP Sync Success"); }else{ display.println("NTP Sync Failed"); } }else{ display.println("WiFi Not Configured"); } display.display(true); //full refresh delay(1000); showMenu(menuIndex, false); } bool Watchy::syncNTP(){ //NTP sync - call after connecting to WiFi and remember to turn it back off configTime(GMT_OFFSET_SEC, DST_OFFSET_SEC, NTP_SERVER); struct tm timeinfo; if(!getLocalTime(&timeinfo)){ return false; //NTP sync failed } /**************************************************** struct tm { int tm_sec; // Seconds [0,60]. int tm_min; // Minutes [0,59]. int tm_hour; // Hour [0,23]. int tm_mday; // Day of month [1,31]. int tm_mon; // Month of year [0,11]. int tm_year; // Years since 1900. int tm_wday; // Day of week [0,6] (Sunday =0). int tm_yday; // Day of year [0,365]. int tm_isdst; // Daylight Savings flag. } ****************************************************/ tmElements_t tm; tm.Year = CalendarYrToTm(timeinfo.tm_year + 1900); tm.Month = timeinfo.tm_mon + 1; //tm.Month 1 - 12 tm.Day = timeinfo.tm_mday; tm.Hour = timeinfo.tm_hour; tm.Minute = timeinfo.tm_min; tm.Second = timeinfo.tm_sec; RTC.set(tm); return true; } // time_t compileTime() // { // const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV) // const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec"; // char compMon[3], *m; // strncpy(compMon, compDate, 3); // compMon[3] = '\0'; // m = strstr(months, compMon); // tmElements_t tm; // tm.Month = ((m - months) / 3 + 1); // tm.Day = atoi(compDate + 4); // tm.Year = atoi(compDate + 7) - YEAR_OFFSET; // offset from 1970, since year is stored in uint8_t // tm.Hour = atoi(compTime); // tm.Minute = atoi(compTime + 3); // tm.Second = atoi(compTime + 6); // time_t t = makeTime(tm); // return t + FUDGE; //add fudge factor to allow for compile time // }