diff --git a/software/wordclock/platformio.ini b/software/wordclock/platformio.ini index df650d6..09b9a24 100644 --- a/software/wordclock/platformio.ini +++ b/software/wordclock/platformio.ini @@ -30,6 +30,17 @@ lib_deps = makuna/NeoPixelBus@^2.7.7 256dpi/MQTT@^2.5.1 +[env:ring] +platform = espressif32 +board = esp32-c3-devkitm-1 +framework = arduino +monitor_dtr = 0 +build_flags = -DDEBUG -DRING +lib_deps = + https://github.com/adafruit/RTClib.git + https://github.com/Makuna/NeoPixelBus.git + https://github.com/prampec/IotWebConf.git + [env:nodo1] platform = espressif32 board = pico32 diff --git a/software/wordclock/src/src/ClockFace.cpp b/software/wordclock/src/src/ClockFace.cpp index a77825e..7a44e70 100644 --- a/software/wordclock/src/src/ClockFace.cpp +++ b/software/wordclock/src/src/ClockFace.cpp @@ -968,3 +968,26 @@ case 0: } return true; } + + + + + +bool RingClockFace::stateForTime(int hour, int minute, int second, bool show_ampm) +{ + if (hour == _hour && minute == _minute) + { + return false; + } + _hour = hour; + _minute = minute; + + DLOGLN("update state"); + clearDisplay(); + int h = (hour%12)*2+((minute >= 30)?1:0); + updateSegment(h,0,1); + updateSegment(24+minute,0,1); + + + return true; +} \ No newline at end of file diff --git a/software/wordclock/src/src/ClockFace.h b/software/wordclock/src/src/ClockFace.h index c0aee54..7e9ad6f 100644 --- a/software/wordclock/src/src/ClockFace.h +++ b/software/wordclock/src/src/ClockFace.h @@ -29,7 +29,7 @@ class ClockFace const std::vector &getState() { return _state; }; // Returns the index of the LED in the strip given a position on the grid. - uint16_t map(int16_t x, int16_t y); + virtual uint16_t map(int16_t x, int16_t y); // The first four LED are the corner ones, counting minutes. They are assumed // to be wired in clockwise order, starting from the light sensor position. @@ -93,3 +93,13 @@ class ItalianClockFace : public ClockFace virtual bool stateForTime(int hour, int minute, int second, bool show_ampm); }; + + +class RingClockFace : public ClockFace +{ +public: + RingClockFace(LightSensorPosition position) : ClockFace(position){}; + + virtual bool stateForTime(int hour, int minute, int second, bool show_ampm); + virtual uint16_t map(int16_t x, int16_t y) {return x;} +}; \ No newline at end of file diff --git a/software/wordclock/src/src/Iot.cpp b/software/wordclock/src/src/Iot.cpp index d5ba716..de8ea72 100644 --- a/software/wordclock/src/src/Iot.cpp +++ b/software/wordclock/src/src/Iot.cpp @@ -1,3 +1,6 @@ +extern "C" { +#include +} #include "logging.h" #include "Iot.h" #include @@ -25,8 +28,6 @@ #define HTTP_OK 200 // NTP server. #define NTP_SERVER "pool.ntp.org" -// When NTP is enabled, define how often to update the RTC. -#define NTP_POLL_DELAY_SECONDS 86400 // Once a day is plenty enough. namespace { @@ -223,7 +224,11 @@ body > div > div:last-child {\ { DLOG("[INFO] Could not parse number value \""); DLOG(str); - DLOGLN("\"."); + DLOG("\" "); + DLOG(min_value); + DLOG(" <, >"); + DLOG(max_value); + DLOGLN("."); return default_value; } return parsed_value; @@ -287,6 +292,9 @@ Iot::Iot(Display *display, RTC_DS3231 *rtc) "style='border-width: 1px; padding: 1px;'"), time_group_("time_group", "Time"), + ntp_interval_param_( + "Refresh interval in hours for ntp synchronization (requires WiFi)", "ntp_interval", ntp_interval_value_, + IOT_CONFIG_VALUE_LENGTH, "24", 1, 24,1, "data-controlledby='ntp_enabled' data-showon='1'"), ntp_enabled_param_( "Use network time (requires WiFi)", "ntp_enabled", ntp_enabled_value_, IOT_CONFIG_VALUE_LENGTH, "0", 0, 1, 1, "style='width: 40px;' data-labels='Off|On'"), @@ -311,6 +319,7 @@ Iot::Iot(Display *display, RTC_DS3231 *rtc) this->ldr_sensitivity_value_[0] = '\0'; this->color_value_[0] = '\0'; this->ntp_enabled_value_[0] = '\0'; + this->ntp_interval_value_[0] = '\0'; this->timezone_value_[0] = '\0'; this->mqtt_enabled_value_[0] = '\0'; this->mqtt_server_value_[0] = '\0'; @@ -330,32 +339,37 @@ void Iot::clearTransientParams_() // workaround of representing booleans as 0 or 1 integers. void Iot::updateClockFromParams_() { - switch (parseNumberValue(clockface_language_value_, 0, 10, 0)) - { - case 1: - { - display_->setClockFace(&clockFaceNL); - DLOGLN("Language set to Dutch"); - break; - } - case 2: - { - display_->setClockFace(&clockFaceFR); - DLOGLN("Language set to French"); - break; - } - case 3: - { - display_->setClockFace(&clockFaceIT); - DLOGLN("Language set to Italian"); - break; - } - default: - { - display_->setClockFace(&clockFaceEN); - DLOGLN("Language set to English"); - break; - } + switch(parseNumberValue(clockface_language_value_, 0, 10, 0)) { + case 1: + { + display_->setClockFace(&clockFaceNL); + DLOGLN("Language set to Dutch"); + break; + } + case 2: + { + display_->setClockFace(&clockFaceFR); + DLOGLN("Language set to French"); + break; + } + case 3: + { + display_->setClockFace(&clockFaceIT); + DLOGLN("Language set to Italian"); + break; + } + case 4: + { + display_->setClockFace(&clockFaceRING); + DLOGLN("Language set to Ring"); + break; + } + default: + { + display_->setClockFace(&clockFaceEN); + DLOGLN("Language set to English"); + break; + } } display_->setColor( @@ -363,6 +377,7 @@ void Iot::updateClockFromParams_() display_->setShowAmPm(parseBooleanValue(show_ampm_value_)); display_->setSensorSentivity(parseNumberValue(ldr_sensitivity_value_, 0, 10, 5)); + this->ntp_poll_interval_ = parseNumberValue(ntp_enabled_value_, 1,24,24); if (parseBooleanValue(ntp_enabled_value_)) { ntp_poll_timer_.start(); @@ -381,7 +396,7 @@ void Iot::setup() ntp_poll_timer_.setup([this]() { maybeSetRTCfromNTP_(); }, - NTP_POLL_DELAY_SECONDS); + ntp_poll_interval_*60*60); this->show_ampm_value_[0] = '\0'; this->ldr_sensitivity_value_[0] = '\0'; @@ -407,6 +422,7 @@ void Iot::setup() iot_web_conf_.addParameterGroup(&display_group_); time_group_.addItem(&ntp_enabled_param_); + time_group_.addItem(&ntp_interval_param_); time_group_.addItem(&timezone_param_); time_group_.addItem(&manual_time_param_); iot_web_conf_.addParameterGroup(&time_group_); @@ -504,6 +520,9 @@ void Iot::maybeSetRTCfromNTP_() } int tz = parseNumberValue(timezone_value_, 0, 459, 0); +#if SNTP_GET_SERVERS_FROM_DHCP || SNTP_GET_SERVERS_FROM_DHCPV6 + sntp_servermode_dhcp(1); +#endif configTzTime(posix[tz], NTP_SERVER); struct tm timeinfo; @@ -520,7 +539,10 @@ void Iot::maybeSetRTCfromNTP_() digitalWrite(LED_PIN, HIGH); #endif - rtc_->adjust(DateTime(timeinfo.tm_year, timeinfo.tm_mon, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)); + if (get_rtc_found()) { + rtc_->adjust(DateTime(timeinfo.tm_year, timeinfo.tm_mon, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)); + } DLOG("RTC set to:"); DLOGLN(&timeinfo, "%A, %B %d %Y %H:%M:%S"); DLOG("Timezone:"); @@ -529,23 +551,49 @@ void Iot::maybeSetRTCfromNTP_() void Iot::setRTCfromConfig_() { - const DateTime now = rtc_->now(); - uint16_t year = now.year(); - uint8_t month = now.month(); - uint8_t day = now.day(); - uint8_t hour = now.hour(); - uint8_t minute = now.minute(); - uint8_t second = now.second(); - bool datetime_changed = false; - - if (manual_time_value_[0] != 0 && parseTimeValue(manual_time_value_, &hour, &minute, &second)) - { - datetime_changed = true; - } + if (get_rtc_found()) { + const DateTime now = rtc_->now(); + uint16_t year = now.year(); + uint8_t month = now.month(); + uint8_t day = now.day(); + uint8_t hour = now.hour(); + uint8_t minute = now.minute(); + uint8_t second = now.second(); + bool datetime_changed = false; + + if (manual_time_value_[0] != 0 && + parseTimeValue(manual_time_value_, &hour, &minute, &second)) { + datetime_changed = true; + } - if (datetime_changed) - { - rtc_->adjust(DateTime(year, month, day, hour, minute, second)); + if (datetime_changed) { + rtc_->adjust(DateTime(year, month, day, hour, minute, second)); + } + } else { + uint16_t year = 0; + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 0; + uint8_t minute = 0; + uint8_t second = 0; + if (manual_time_value_[0] != 0 && + parseTimeValue(manual_time_value_, &hour, &minute, &second)) { + struct tm timeinfo = {0, }; + if (!getLocalTime(&timeinfo)) + { + DLOG("Local time invalid. set some 'usable' data"); + timeinfo.tm_year = (2016 - 1900)+1; + timeinfo.tm_mon = 1; + } + timeinfo.tm_hour = hour; + timeinfo.tm_min = minute; + timeinfo.tm_sec = second; + time_t t = mktime(&timeinfo); + struct timeval tv; + tv.tv_usec = 0; + tv.tv_sec = t; + settimeofday(&tv, NULL); + } } } @@ -650,4 +698,4 @@ void Iot::mqttMessageReceived_(String &topic, String &payload) // color_value_, payload.c_str(), // IOT_CONFIG_VALUE_LENGTH); // iot_web_conf_.saveConfig(); -} \ No newline at end of file +} diff --git a/software/wordclock/src/src/Iot.h b/software/wordclock/src/src/Iot.h index 2aa8621..a895ee8 100644 --- a/software/wordclock/src/src/Iot.h +++ b/software/wordclock/src/src/Iot.h @@ -29,6 +29,10 @@ class Iot void setup(); void loop(); + // accessor for rtc_found + bool get_rtc_found() { return rtc_found_; } + void set_rtc_found(bool b) { rtc_found_ = b; } + private: // Clears the values of transient parameters. void clearTransientParams_(); @@ -75,8 +79,11 @@ class Iot // RTC. RTC_DS3231 *rtc_ = nullptr; + bool rtc_found_ = false; + - // NTP poll timer. + // NTP poll timer; + int ntp_poll_interval_ = 24; Timer ntp_poll_timer_; // Whether to reboot the ESP (after config updates if MQTT is enabled). @@ -145,6 +152,11 @@ class Iot // MQTT password value. char mqtt_password_value_[IOT_CONFIG_VALUE_LENGTH]; + // Enables the ntp interval. + IotRangeValueParameter ntp_interval_param_; + // Value of the ntp interval setting option. + char ntp_interval_value_[IOT_CONFIG_VALUE_LENGTH]; + // Config form groups. iotwebconf::ParameterGroup time_group_; iotwebconf::ParameterGroup display_group_; diff --git a/software/wordclock/src/src/clockfaces.h b/software/wordclock/src/src/clockfaces.h index 35e7f02..c6a54cb 100644 --- a/software/wordclock/src/src/clockfaces.h +++ b/software/wordclock/src/src/clockfaces.h @@ -10,4 +10,5 @@ auto position = ClockFace::LightSensorPosition::Bottom; EnglishClockFace clockFaceEN(position); FrenchClockFace clockFaceFR(position); DutchClockFace clockFaceNL(position); -ItalianClockFace clockFaceIT(position); \ No newline at end of file +ItalianClockFace clockFaceIT(position); +RingClockFace clockFaceRING(position); \ No newline at end of file diff --git a/software/wordclock/src/src/nodo.h b/software/wordclock/src/src/nodo.h index af36503..1f85ea7 100644 --- a/software/wordclock/src/src/nodo.h +++ b/software/wordclock/src/src/nodo.h @@ -11,4 +11,9 @@ #define LDR_PIN 1 #define NEOPIXEL_PIN 0 #endif +#else +#ifdef RING +#define NEOPIXEL_PIN 1 +#define LDR_PIN 2 +#endif // RING #endif diff --git a/software/wordclock/src/wordclock.cpp b/software/wordclock/src/wordclock.cpp index 43cf04a..d4947ae 100644 --- a/software/wordclock/src/wordclock.cpp +++ b/software/wordclock/src/wordclock.cpp @@ -20,6 +20,7 @@ #include "src/Iot.h" #include "src/nodo.h" // Nodo stuff RTC_DS3231 rtc; +boolean rtc_found = false; // TODO: refactor so we don't need to initialize another clockface here. #ifdef NODO @@ -42,12 +43,16 @@ void setup() #ifdef SDA_PIN Wire.begin(SDA_PIN, SCL_PIN); #endif - bool result = rtc.begin(); // pins for RTC swapped over on V1.0 - DCHECK(result, "RTC didn't start"); - if (rtc.lostPower()) - { - DCHECK("RTC lost power. Battery was removed ?"); + rtc_found = rtc.begin(); // pins for RTC swapped over on V1.0 + if (rtc_found) { + DLOG("RTC detected"); + if (rtc.lostPower()) { + DCHECK("RTC lost power. Battery was removed ?"); + } + } else { + DLOG("RTC not found"); } + iot.set_rtc_found(rtc_found); display.setup(); iot.setup(); @@ -58,8 +63,20 @@ void loop() { long ts_now = millis(); if ((ts_now - last_rtc_update) > 99) { - DateTime now = rtc.now(); - display.updateForTime(now.hour(), now.minute(), now.second()); + if ( rtc_found ) { + DateTime now = rtc.now(); + display.updateForTime(now.hour(), now.minute(), now.second()); + } else { + struct tm timeinfo; + // Get the local time from the esp's rtc. + if (getLocalTime(&timeinfo)) { + auto now = DateTime(timeinfo.tm_year, timeinfo.tm_mon, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + display.updateForTime(now.hour(), now.minute(), now.second()); + } else { + display.updateForTime(0, 0, 0); + } + } last_rtc_update = ts_now; }