diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0db0561 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.metadata +.DS_Store +.ino.cpp +.project +.cproject +.settings +Release +Debug +libraries +core +*.bak +*-bak diff --git a/DS3231.cpp b/DS3231.cpp new file mode 100644 index 0000000..0f6448d --- /dev/null +++ b/DS3231.cpp @@ -0,0 +1,231 @@ +/* + * DS3231.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 21, 2017 + * Author: chris.l + */ + +#include "DS3231.h" + +//#define DEBUG +#include "Logger.h" + +DS3231::DS3231() +{ +} + +int DS3231::begin() +{ + dbprintln("DS3231::begin()"); + + delay(2500); // DS3231 may need this on powerup. + + uint8_t ctrl = 0b00000000; // enable osc/no BBS/no CONV/1hz/SQWV on/no ALRM + int err = write(DS3231_CONTROL_REG, ctrl); //CONTROL Register Address + if (err) + { + dbprintf("DS3231(): write(DS3231_CONTROL_REG) failed: %d\n", err); + return -1; + } + + delay(100); + + // set the clock to 24hr format + uint8_t hr; + if (read(DS3231_HOUR_REG, &hr)) + { + dbprintf("DS3231::begin(): read(DS3231_HOUR_REG) failed!\n"); + return -1; + } + + // switch to 24hr mode if its not already + if (hr & _BV(DS3231_AMPM)) + { + hr &= ~_BV(DS3231_AMPM); + + if(write(DS3231_HOUR_REG, hr)) + { + dbprintf("DS3231::begin(): write(DS3231_HOUR_REG, 0x%02x) failed!\n", hr); + return -1; + } + } + + return 0; +} + +uint8_t DS3231::fromBCD(uint8_t val) +{ + return val - 6 * (val >> 4); +} + +uint8_t DS3231::toBCD(uint8_t val) +{ + return val + 6 * (val / 10); +} + +int DS3231::readTime(DS3231DateTime& dt) +{ + + uint8_t count = setupRead(DS3231_SEC_REG, 7); + if (count != 7) + { + dbprintf("DS3231::readTime: setupRead failed! count:%u = 7\n", count); + Wire.clearWriteError(); + Wire.flush(); + return -1; + } + + uint8_t seconds = Wire.read(); + uint8_t minutes = Wire.read(); + uint8_t hours = Wire.read(); + uint8_t day = Wire.read(); + uint8_t date = Wire.read(); + uint8_t month = Wire.read(); + uint8_t year = Wire.read(); + + dbprintf("DS3231::readTime raw month: 0x%02x\n", month); + + dt.seconds = fromBCD(seconds); + dt.minutes = fromBCD(minutes); + dt.hours = fromBCD(hours & DS3231_24HR_MASK); + dt.day = fromBCD(day); + dt.date = fromBCD(date); + dt.month = fromBCD(month & DS3231_MONTH_MASK); + dt.year = fromBCD(year); + dt.century = month&DS3231_CENTURY? 1 : 0; + + if (!dt.isValid()) + { + dbprintf("DS3231::readTime: result not valid!!!\n"); + Wire.clearWriteError(); + Wire.flush(); + return -1; + } + + dbprintf("DS3231::readTime %04u-%02u-%02u %02u:%02u:%02u day:%u century:%u\n", + dt.year, + dt.month, + dt.date, + dt.hours, + dt.minutes, + dt.seconds, + dt.day, + dt.century); + return 0; +} + +int DS3231::writeTime(DS3231DateTime &dt) +{ + dbprintf("DS3231::writeTime %04u-%02u-%02u %02u:%02u:%02u day:%u century:%u\n", + dt.year, + dt.month, + dt.date, + dt.hours, + dt.minutes, + dt.seconds, + dt.day, + dt.century); + + Wire.beginTransmission(DS3231_ADDRESS); + if (Wire.write(DS3231_SEC_REG) != 1) + { + Wire.endTransmission(); + dbprintln("DS3231::writeTime: Wire.write(reg=DS3231_SEC_REG) failed!"); + return -1; + } + + int count; + count = Wire.write(toBCD(dt.seconds)); + count += Wire.write(toBCD(dt.minutes)); + count += Wire.write(toBCD(dt.hours)); + count += Wire.write(toBCD(dt.day)); + count += Wire.write(toBCD(dt.date)); + count += Wire.write(toBCD(dt.month) | (dt.century ? _BV(DS3231_CENTURY) : 0)); + count += Wire.write(toBCD(dt.year)); + if (count != 7) + { + dbprintf("DS3231::writeTime: Wire.write() all fields, expected 7 got %u\n", count); + return -1; + } + int err = Wire.endTransmission(); + if (err) + { + dbprintf("DS3231::writeTime: Wire.endTransmission() returned: %d\n", err); + return -1; + } + return(0); +} + +int DS3231::setupRead(uint8_t reg, uint8_t size) +{ + Wire.beginTransmission(DS3231_ADDRESS); + if (Wire.write(reg) != 1) + { + Wire.endTransmission(); + dbprintln("DS3231::setupRead: Wire.write(DS3231_CONTROL_REG) failed!"); + return -1; + } + int err = Wire.endTransmission(); + + if (err) + { + dbprintf("DS3231::setupRead: Wire.endTransmission() returned: %d\n", err); + return -1; + } + + return Wire.requestFrom(DS3231_ADDRESS, (size_t)size); +} + +int DS3231::read(uint8_t reg, uint8_t *value) +{ + size_t count = setupRead(reg, 1); + count = Wire.requestFrom(DS3231_ADDRESS, (size_t)1); + *value = Wire.read(); + if (count != 1) + { + dbprintf("DS3231::read: Wire.requestFrom() returns %u, expected 1\n", count); + return -1; + } + return 0; +} + + +int DS3231::write(uint8_t reg, uint8_t value) +{ + Wire.beginTransmission(DS3231_ADDRESS); + if (Wire.write(reg) != 1) + { + Wire.endTransmission(); + dbprintf("DS3231::write: Wire.write(reg=%d, value=%d) failed!\n", reg, value); + return -1; + } + size_t count; + count = Wire.write(value); + if (count != 1) + { + dbprintf("DS3231::write: Wire.write(value=%u) returns %u\n", value, count); + return -1; + } + int err = Wire.endTransmission(); + if (err) + { + dbprintf("DS3231::write: Wire.endTransmission() returned: %d\n", err); + return -1; + } + return(0); +} diff --git a/DS3231.h b/DS3231.h new file mode 100644 index 0000000..bd08c12 --- /dev/null +++ b/DS3231.h @@ -0,0 +1,101 @@ +/* + * DS3231.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 21, 2017 + * Author: chris.l + */ + +#ifndef DS3231_H_ +#define DS3231_H_ +#include +#include +#include "WireUtils.h" +#include "DS3231DateTime.h" + +// I2C address +const uint8_t DS3231_ADDRESS = 0x68; + +// Registers +const uint8_t DS3231_SEC_REG = 0x00; +const uint8_t DS3231_MIN_REG = 0x01; +const uint8_t DS3231_HOUR_REG = 0x02; +const uint8_t DS3231_WDAY_REG = 0x03; +const uint8_t DS3231_MDAY_REG = 0x04; +const uint8_t DS3231_MONTH_REG = 0x05; +const uint8_t DS3231_YEAR_REG = 0x06; + +const uint8_t DS3231_A1S_REG = 0x07; +const uint8_t DS3231_A1M_REG = 0x08; +const uint8_t DS3231_A1H_REG = 0x09; +const uint8_t DS3231_A1W_REG = 0x0A; + +const uint8_t DS3231_AL2M_REG = 0x0B; +const uint8_t DS3231_AL2H_REG = 0x0C; +const uint8_t DS3231_AL2W_REG = 0x0D; + +const uint8_t DS3231_CONTROL_REG = 0x0E; +const uint8_t DS3231_STATUS_REG = 0x0F; +const uint8_t DS3231_AGING_REG = 0x0F; +const uint8_t DS3231_TEMP_UP_REG = 0x11; +const uint8_t DS3231_TEMP_LOW_REG = 0x12; + +// DS3231 Control Register Bits +const uint8_t DS3231_CTL_A1IE = 0; +const uint8_t DS3231_CTL_A2IE = 1; +const uint8_t DS3231_CTL_INTCN = 2; +const uint8_t DS3231_CTL_RS1 = 3; +const uint8_t DS3231_CTL_RS2 = 4; +const uint8_t DS3231_CTL_CONV = 5; +const uint8_t DS3231_CTL_BBSQW = 6; +const uint8_t DS3231_CTL_EOSC = 7; + +// DS3231 Status Register Bits +const uint8_t DS3231_STS_A1F = 0; +const uint8_t DS3231_STS_A2F = 1; +const uint8_t DS3231_STS_BSY = 2; +const uint8_t DS3231_STS_EN32KHZ = 3; +const uint8_t DS3231_STS_OSF = 7; + +// DS3231 Hour register +const uint8_t DS3231_AMPM = 6; // AMPM bit +const uint8_t DS3231_24HR_MASK = 0x3f; // mask for 24 hour value + +// DS3231 Month register +const uint8_t DS3231_CENTURY = 7; +const uint8_t DS3231_MONTH_MASK = 0x1f; + +#define RTC_POSITION_ERROR 0xffff + + +class DS3231 +{ +public: + DS3231(); + int begin(); + int readTime(DS3231DateTime& dt); // return 0 if ok + int writeTime(DS3231DateTime& dt); // return 0 if ok + +private: + uint8_t fromBCD(uint8_t val); + uint8_t toBCD(uint8_t val); + int setupRead(uint8_t reg, uint8_t size); + int write(uint8_t reg, uint8_t value); + int read(uint8_t reg, uint8_t* value); +}; + +#endif /* DS3231_H_ */ diff --git a/DS3231DateTime.cpp b/DS3231DateTime.cpp new file mode 100644 index 0000000..82a95f7 --- /dev/null +++ b/DS3231DateTime.cpp @@ -0,0 +1,214 @@ +/* + * DS3231DateTime.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 22, 2017 + * Author: chris.l + */ + +#include "DS3231DateTime.h" + + +//#define DEBUG +#include "Logger.h" + +#if defined(DEBUG) +#define dbvalue(prefix) { \ + dbprintf("%s position:%u (%04u-%02u-%02u %02u:%02u:%02u) weekday:%u century:%d unix:%lu\n", \ + prefix, \ + getPosition(), \ + year+1900+100, \ + month, \ + date, \ + hours, \ + minutes, \ + seconds, \ + day, \ + century, \ + getUnixTime());} +#else +#define dbvalue(prefix) +#endif + + +DS3231DateTime::DS3231DateTime() +{ + seconds = 0; + minutes = 0; + hours = 0; + date = 0; + day = 0; + month = 0; + year = 0; + century = 0; +} + +boolean DS3231DateTime::isValid() +{ + dbvalue("DS3231DateTime::isValid"); + + if (seconds > 59) + { + dbprintf("invalid seconds %d\n", seconds); + return false; + } + + if (minutes > 59) + { + dbprintf("invalid minutes %d\n", minutes); + return false; + } + + if (hours > 23) + { + dbprintf("invalid hours %d\n", hours); + return false; + } + + if (date > 31) + { + dbprintf("invalid hours %d\n", hours); + return false; + } + + if ((month > 12) || (month < 1)) + { + dbprintf("invalid month %d\n", month); + return false; + } + + if (year > 99) + { + dbprintf("invalid year %d\n", year); + return false; + } + + return true; +} + +void DS3231DateTime::setUnixTime(unsigned long time) +{ + struct tm tm; + TimeUtils::gmtime_r((time_t*)&time, &tm); + + dbprintf("DS3231DateTime::setUnixTime month: %d\n", tm.tm_mon); + + seconds = tm.tm_sec; + minutes = tm.tm_min; + hours = tm.tm_hour; + day = tm.tm_wday; + date = tm.tm_mday; + month = tm.tm_mon + 1; + year = tm.tm_year - 100; + century = 0; + dbvalue("setUnixTime new value:"); +} + +unsigned long DS3231DateTime::getUnixTime() +{ + struct tm tm; + tm.tm_sec = seconds; + tm.tm_min = minutes; + tm.tm_hour = hours; + tm.tm_mday = date; + tm.tm_mon = month - 1; + tm.tm_year = year + 100; + tm.tm_isdst = 0; + tm.tm_wday = 0; + tm.tm_yday = 0; + unsigned long unix = TimeUtils::mktime(&tm); + dbprintf("returning unix time: %lu\n", unix); + return unix; +} + +void DS3231DateTime::applyOffset(int offset) +{ + unsigned long now = getUnixTime(); + now += offset; + setUnixTime(now); +} + +uint16_t DS3231DateTime::getPosition() +{ + int h = hours; + if (h > 12) + { + h -= 12; + } + return h*3600 + minutes*60 + seconds; +} + +uint16_t DS3231DateTime::getPosition(int offset) +{ + int signed_position = getPosition(); + dbprintf("position before offset: %d\n", signed_position); + signed_position += offset; + dbprintf("position after offset: %d\n", signed_position); + if (signed_position < 0) + { + signed_position += MAX_POSITION; + dbprintf("position corrected+: %d\n", signed_position); + } + else if (signed_position >= MAX_POSITION) + { + signed_position -= MAX_POSITION; + dbprintf("position corrected-: %d\n", signed_position); + } + uint16_t position = (uint16_t) signed_position; + return position; +} + +char value_buf[128]; + +const char* DS3231DateTime::string() +{ + snprintf(value_buf, 127, + "%04u-%02u-%02u %02u:%02u:%02u", + year+1900+100, + month, + date, + hours, + minutes, + seconds); + return value_buf; +} + +uint8_t DS3231DateTime::getDay() +{ + return day; +} + +uint8_t DS3231DateTime::getDate() +{ + return date; +} + +uint8_t DS3231DateTime::getHour() +{ + return hours; +} + +uint8_t DS3231DateTime::getMonth() +{ + return month; +} + +uint16_t DS3231DateTime::getYear() +{ + return year+2000; +} + diff --git a/DS3231DateTime.h b/DS3231DateTime.h new file mode 100644 index 0000000..28658c5 --- /dev/null +++ b/DS3231DateTime.h @@ -0,0 +1,60 @@ +/* + * DS3231DateTime.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 22, 2017 + * Author: chris.l + */ + +#ifndef DS3231DATETIME_H_ +#define DS3231DATETIME_H_ +#include +#include "TimeUtils.h" + +#define MAX_POSITION 43200 // seconds in 12 hours + +class DS3231DateTime +{ +public: + DS3231DateTime(); + boolean isValid(); + void setUnixTime(unsigned long time); + unsigned long getUnixTime(); + void applyOffset(int offset); + uint16_t getPosition(); + uint16_t getPosition(int offset); + const char* string(); + + uint8_t getDay(); + uint8_t getDate(); + uint8_t getHour(); + uint8_t getMonth(); + uint16_t getYear(); + + friend class DS3231; +protected: + uint8_t seconds; + uint8_t minutes; + uint8_t hours; + uint8_t date; + uint8_t day; + uint8_t month; + uint8_t year; + uint8_t century; +}; + +#endif /* DS3231DATETIME_H_ */ diff --git a/ESPNTPServer.cpp b/ESPNTPServer.cpp new file mode 100644 index 0000000..40f4aec --- /dev/null +++ b/ESPNTPServer.cpp @@ -0,0 +1,279 @@ +/* + * ESPNTPServer.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: Oct 29, 2017 + * Author: liebman + */ + +#include "ESPNTPServer.h" +#define DEBUG +#include "Logger.h" +#include // htonl() & ntohl() + +volatile uint32_t seconds; +volatile uint32_t cycles; +volatile uint32_t last_cpu_cycles; +volatile uint32_t min_cycles; +volatile uint32_t max_cycles; + +AsyncUDP udp; +DS3231 rtc; // real time clock on i2c interface + +#ifdef DEBUG +void dumpNTPPacket(NTPPacket* ntp) +{ + dbprintf("size: %u\n", sizeof(*ntp)); + dbprintf("firstbyte: 0x%02x\n", *(uint8_t*)ntp); + dbprintf("li: %u\n", getLI(ntp->flags)); + dbprintf("version: %u\n", getVERS(ntp->flags)); + dbprintf("mode: %u\n", getMODE(ntp->flags)); + dbprintf("stratum: %u\n", ntp->stratum); + dbprintf("poll: %u\n", ntp->poll); + dbprintf("precision: %d\n", ntp->precision); + dbprintf("delay: %u\n", ntp->delay); + dbprintf("dispersion: %u\n", ntp->dispersion); + dbprintf("ref_id: %02x:%02x:%02x:%02x\n", ntp->ref_id[0], ntp->ref_id[1], ntp->ref_id[2], ntp->ref_id[3]); + dbprintf("ref_time: %08x:%08x\n", ntp->ref_time.seconds, ntp->ref_time.fraction); + dbprintf("orig_time: %08x:%08x\n", ntp->orig_time.seconds, ntp->orig_time.fraction); + dbprintf("recv_time: %08x:%08x\n", ntp->recv_time.seconds, ntp->recv_time.fraction); + dbprintf("xmit_time: %08x:%08x\n", ntp->xmit_time.seconds, ntp->xmit_time.fraction); +} +#else +#define dumpNTPPacket(x) +#endif + +void oneSecondInterrupt() +{ + uint32_t cpu_cycles = ESP.getCycleCount(); + // + // the first time around we just initialize the last value + // + if (last_cpu_cycles == 0) + { + last_cpu_cycles = cpu_cycles; + return; + } + + cycles = cpu_cycles - last_cpu_cycles; + last_cpu_cycles = cpu_cycles; + seconds += 1; + + if (min_cycles == 0 || cycles < min_cycles) + { + min_cycles = cycles; + } + + if (cycles > max_cycles) + { + max_cycles = cycles; + } + +#if defined(DEBUG) + digitalWrite(LED_PIN, digitalRead(LED_PIN) ? LOW : HIGH); +#endif +} + +void waitForEdge(int edge) +{ + while (digitalRead(SYNC_PIN) == edge) + { + delay(0); + } + while (digitalRead(SYNC_PIN) != edge) + { + delay(0); + } +} + +void getNTPTime(NTPTime *time) +{ + time->seconds = toNTP(seconds); + uint32_t cpu_cycles = ESP.getCycleCount(); + uint32_t cycles_delta = cpu_cycles - last_cpu_cycles; + + // + // if cycles_delta is at or bigger than cycles then + // use the max fraction. + // + if (cycles_delta >= cycles) + { + time->fraction = 0xffffffff; + return; + } + + double percent = (double)cycles_delta / (double)cycles; + //dbprintf("cycles_delta: %lu cycles: %lu percent: %lf\n", cycles_delta, cycles, percent); + time->fraction = (uint32_t)(percent * (double)4294967296L); +} + +void recievePacket(AsyncUDPPacket aup) +{ + NTPTime recv_time; + getNTPTime(&recv_time); + + if (aup.length() != sizeof(NTPPacket)) + { + dbprintf("recievePacket: ignoring packet with bad length: %d < %d\n", aup.length(), sizeof(NTPPacket)); + return; + } + NTPPacket ntp; + memcpy(&ntp, aup.data(), sizeof(NTPPacket)); + ntp.delay = ntohl(ntp.delay); + ntp.dispersion = ntohl(ntp.dispersion); + ntp.orig_time.seconds = ntohl(ntp.orig_time.seconds); + ntp.orig_time.fraction = ntohl(ntp.orig_time.fraction); + ntp.ref_time.seconds = ntohl(ntp.ref_time.seconds); + ntp.ref_time.fraction = ntohl(ntp.ref_time.fraction); + ntp.recv_time.seconds = ntohl(ntp.recv_time.seconds); + ntp.recv_time.fraction = ntohl(ntp.recv_time.fraction); + ntp.xmit_time.seconds = ntohl(ntp.xmit_time.seconds); + ntp.xmit_time.fraction = ntohl(ntp.xmit_time.fraction); + dumpNTPPacket(&ntp); + + // + // Build the response + // + ntp.flags = setLI(LI_NONE) | setVERS(NTP_VERSION) | setMODE(MODE_SERVER); + ntp.stratum = 1; + ntp.precision = -18; + // TODO: root delay, and root dispersion + strncpy((char*)ntp.ref_id, REF_ID, sizeof(ntp.ref_id)); + ntp.orig_time = ntp.xmit_time; + ntp.recv_time = recv_time; + getNTPTime(&(ntp.ref_time)); + dumpNTPPacket(&ntp); + ntp.delay = htonl(ntp.delay); + ntp.dispersion = htonl(ntp.dispersion); + ntp.orig_time.seconds = htonl(ntp.orig_time.seconds); + ntp.orig_time.fraction = htonl(ntp.orig_time.fraction); + ntp.ref_time.seconds = htonl(ntp.ref_time.seconds); + ntp.ref_time.fraction = htonl(ntp.ref_time.fraction); + ntp.recv_time.seconds = htonl(ntp.recv_time.seconds); + ntp.recv_time.fraction = htonl(ntp.recv_time.fraction); + getNTPTime(&(ntp.xmit_time)); + ntp.xmit_time.seconds = htonl(ntp.xmit_time.seconds); + ntp.xmit_time.fraction = htonl(ntp.xmit_time.fraction); + aup.write((uint8_t*)&ntp, sizeof(ntp)); +} + +void setup() +{ + dbbegin(115200); + dbprintln(""); + dbprintln("Startup!"); + + pinMode(SYNC_PIN, INPUT); + pinMode(LED_PIN, OUTPUT); + + seconds = 0; + cycles = 0; + max_cycles = 0; + min_cycles = 0; + last_cpu_cycles = 0; + + attachInterrupt(SYNC_PIN, &oneSecondInterrupt, FALLING); + dbprintf("delay 2 seconds to make sure we have a clean cycle count\n"); + delay(2000); + + Wire.begin(); + Wire.setClockStretchLimit(1500); + while (rtc.begin()) + { + dbprintln("RTC begin failed! Attempting recovery..."); + + while (WireUtils.clearBus()) + { + delay(10000); + dbprintln("lets try that again..."); + } + delay(1000); + } + + waitForEdge(SYNC_EDGE_FALLING); + delay(2); // for some reason we get errors if we read too soon after the falling edge. + DS3231DateTime dt; + while (rtc.readTime(dt)) + { + dbprintln("setup: FAILED to read RTC"); + while (WireUtils.clearBus()) + { + delay(10000); + dbprintln("lets try that again..."); + } + delay(1000); + } + seconds = dt.getUnixTime(); + + WiFiManager wifi; + //wifi.setDebugOutput(false); + String ssid = "SynchroClock" + String(ESP.getChipId()); + wifi.autoConnect(ssid.c_str(), NULL); + + // + // initialize UDP handler + // + while(!udp.listen(NTP_PORT)) { + dbprintf("setup: failed to listen on port %d! Will retry in a bit...\n", NTP_PORT); + delay(1000); + dbprintf("setup: retrying!\n"); + } + + udp.onPacket(recievePacket); +} + + + +void loop() +{ + static int last_sync_level; + + // + // insure seconds is correct after each falling edge + // + int sync_level = digitalRead(SYNC_PIN); + if (sync_level == 0 && sync_level != last_sync_level) + { + DS3231DateTime dt; + delay(2); // for some reason we get errors if we read too soon after the falling edge. + if (rtc.readTime(dt)) + { + dbprintln("loop: FAILED to read RTC, clearing bus & not checking seconds!"); + WireUtils.clearBus(); + } + else + { + uint32_t old_seconds = seconds; + uint32_t now = dt.getUnixTime(); + if (now != seconds) + { + seconds = now; + dbprintf("loop: updated seconds from %lu to %lu\n", old_seconds, now); + } + } + } + last_sync_level = sync_level; + +#if 1 + static uint32_t last_seconds; + if (seconds != last_seconds && (seconds % 300) == 0) + { + dbprintf("min_cycles:%lu max_cycles:%lu cycles:%lu\n", min_cycles, max_cycles, cycles); + } + last_seconds = seconds; +#endif +} diff --git a/ESPNTPServer.h b/ESPNTPServer.h new file mode 100644 index 0000000..20c50ad --- /dev/null +++ b/ESPNTPServer.h @@ -0,0 +1,93 @@ +/* + * ESPNTPServer.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: Oct 29, 2017 + * Author: liebman + */ + +#ifndef _ESPNTPServer_H_ +#define _ESPNTPServer_H_ +#include "Arduino.h" +#include "Wire.h" +#include "DS3231.h" +#include "ESPAsyncUDP.h" +#include "WiFiManager.h" + + +// pin definitions +#define LED_PIN D7 // (GPIO13) LED on pin, active low +#define SYNC_PIN D5 // (GPIO14) pin tied to 1hz square wave from RTC +#define CONFIG_PIN D6 // (GPIO12) button tied to pin + +#define NTP_PORT 123 + +#define SYNC_EDGE_RISING 1 +#define SYNC_EDGE_FALLING 0 + +typedef struct ntp_time +{ + uint32_t seconds; + uint32_t fraction; +} NTPTime; + +typedef struct ntp_packet +{ + uint8_t flags; + uint8_t stratum; + uint8_t poll; + int8_t precision; + uint32_t delay; + uint32_t dispersion; + uint8_t ref_id[4]; + NTPTime ref_time; + NTPTime orig_time; + NTPTime recv_time; + NTPTime xmit_time; +} NTPPacket; + +#define LI_NONE 0 +#define LI_SIXTY_ONE 1 +#define LI_FIFTY_NINE 2 +#define LI_NOSYNC 3 + +#define MODE_RESERVED 0 +#define MODE_ACTIVE 1 +#define MODE_PASSIVE 2 +#define MODE_CLIENT 3 +#define MODE_SERVER 4 +#define MODE_BROADCAST 5 +#define MODE_CONTROL 6 +#define MODE_PRIVATE 7 + +#define NTP_VERSION 4 + +#define REF_ID "PPS" // "GPS" when we have one! + +#define setLI(value) ((value&0x03)<<6) +#define setVERS(value) ((value&0x07)<<3) +#define setMODE(value) ((value&0x07)) + +#define getLI(value) ((value>>6)&0x03) +#define getVERS(value) ((value>>3)&0x07) +#define getMODE(value) (value&0x07) + +#define SEVENTY_YEARS 2208988800L +#define toEPOCH(t) ((uint32_t)t-SEVENTY_YEARS) +#define toNTP(t) ((uint32_t)t+SEVENTY_YEARS) + +#endif /* _ESPNTPServer_H_ */ diff --git a/Logger.cpp b/Logger.cpp new file mode 100644 index 0000000..41cabd0 --- /dev/null +++ b/Logger.cpp @@ -0,0 +1,159 @@ +/* + * Logger.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 31, 2017 + * Author: liebman + */ + +#include "Logger.h" + +#define DEBUG + +#ifdef DEBUG + +#undef dbprintf +#undef dbprint +#undef dbprintln +#undef dbflush + +#define DBP_BUF_SIZE 256 +extern unsigned int snprintf(char*, unsigned int, ...); +#define dbprintf(...) {char dbp_buf[DBP_BUF_SIZE]; snprintf(dbp_buf, DBP_BUF_SIZE-1, __VA_ARGS__); Serial.print(dbp_buf);} +#define dbprintln(x) Serial.println(x) +#define dbflush() Serial.flush() +#endif + +Logger::Logger() +{ + _host = NULL; + _port = 0; + _failed = 0; +#if defined(USE_NETWORK) && defined(USE_TCP) + _client.setTimeout(1000); // 1 second connect timeout +#endif +} + +void Logger::begin() +{ + begin(LOGGER_DEFAULT_BAUD); +} + +void Logger::begin(long int baud) +{ + Serial.begin(baud); +} + +void Logger::end() +{ +#if defined(USE_NETWORK) && defined(USE_TCP) + _client.stop(); +#endif +} + +void Logger::setNetworkLogger(const char* host, uint16_t port) +{ + _host = host; + _port = port; +} + +void Logger::println(const char* message) +{ + snprintf(_buffer, LOGGER_BUFFER_SIZE-1, "%s\n", message); + Serial.print(_buffer); + Serial.flush(); + send(_buffer); +} + +extern int vsnprintf(char* buffer, size_t size, const char* fmt, va_list args); + +void Logger::printf(const char* fmt, ...) +{ + va_list argp; + va_start(argp, fmt); + + vsnprintf(_buffer, LOGGER_BUFFER_SIZE-1, fmt, argp); + va_end(argp); + + Serial.print(_buffer); + Serial.flush(); + send(_buffer); +} + +void Logger::flush() +{ + Serial.flush(); +#if defined(USE_NETWORK) && defined(USE_TCP) + if (_client.connected()) + { + _client.flush(); + } +#endif +} + +void Logger::send(const char* message) +{ +#if defined(USE_NETWORK) + // if we are not configured for TCP then just return + if (_host == NULL) + { + return; + } + + IPAddress ip; + + if (!WiFi.isConnected()) + { + dbprintln("Logger::log: not connected!"); + return; + } + + if (!WiFi.hostByName(_host, ip)) + { + dbprintf("Logger::log failed to resolve address for:%s\n", _host); + return; + } + +#if defined(USE_TCP) + if (!_client.connected() && _failed < 3) // try 3 times at most to connect then give up. + { + if(!_client.connect(ip, _port) && _failed < 3) + { + _failed++; + dbprintf("Logger::send failed to connect!\n"); + return; + } + } + + if (_client.write(message) != strlen(message)) + { + dbprintf("Logger::write failed to write message: %s\n", message); + return; + } + _client.flush(); +#else + _udp.beginPacket(ip, _port); + _udp.write(message); + if (!_udp.endPacket()) + { + dbprintln("Logger::log failed to send packet!"); + } +#endif +#endif +} + +Logger logger; diff --git a/Logger.h b/Logger.h new file mode 100644 index 0000000..1d42e7b --- /dev/null +++ b/Logger.h @@ -0,0 +1,91 @@ +/* + * Logger.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 31, 2017 + * Author: liebman + */ + +#ifndef LOGGER_H_ +#define LOGGER_H_ + +#include +#include + +#define USE_TCP +#define USE_NETWORK + +#ifdef USE_NETWORK +#ifdef USE_TCP +#include +#else +#include +#endif +#endif + +#define LOGGER_DEFAULT_BAUD 115200L +#define LOGGER_BUFFER_SIZE 256 + +class Logger { +public: + Logger(); + void begin(); + void begin(long int baud); + void end(); + void setNetworkLogger(const char* host, uint16_t port); + void println(const char*message); + void printf(const char*message, ...); + void flush(); + +private: +#ifdef USE_NETWORK +#ifdef USE_TCP + WiFiClient _client; +#else + WiFiUDP _udp; +#endif +#endif + const char* _host; + uint16_t _port; + uint16_t _failed; + char _buffer[LOGGER_BUFFER_SIZE]; + + void send(const char* message); +}; + +extern Logger logger; + +#ifdef DEBUG +#define dbbegin(x) logger.begin(x) +#define dbnetlog(h, p) {if (strlen(h) && p) logger.setNetworkLogger(h, p);} +#define dbend() logger.end() +#define dbprintf(...) logger.printf(__VA_ARGS__) +#define dbprintln(x) logger.println(x) +#define dbprint64(l,v) logger.printf("%s %08x:%08x (%Lf)\n", l, (uint32_t)(v>>32), (uint32_t)(v & 0xffffffff), ((long double)v / 4294967296.)) +#define dbprint64s(l,v) logger.printf("%s %08x:%08x (%Lf)\n", l, (int32_t)(v>>32), (uint32_t)(v & 0xffffffff), ((long double)v / 4294967296.)) +#define dbflush() logger.flush() +#else +#define dbbegin(x) +#define dbnetlog(h, p) +#define dbend() +#define dbprintf(...) +#define dbprintln(x) +#define dbprint64(l,v) +#define dbprint64s(l,v) +#define dbflush() +#endif +#endif /* LOGGER_H_ */ diff --git a/README.md b/README.md index 5e5ae50..0d04cd6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # ESPNTPServer GPS fueled ESP8266 based NTP Server + +Don't have a GPS unit yet so I'm using a DS3231 as a PPS source to workout +some of the software. I'm actually using one of my [AnalogClock](https://github.com/liebman/AnalogClock) boards as it has the DS3231 1hz routed to an ESP8266 GPIO pin. + diff --git a/TimeUtils.cpp b/TimeUtils.cpp new file mode 100644 index 0000000..d59fb5a --- /dev/null +++ b/TimeUtils.cpp @@ -0,0 +1,491 @@ +/* + * TimeUtils.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: Jun 7, 2017 + * Author: liebman + */ + +#include "TimeUtils.h" + +//#define DEBUG +#include "Logger.h" + +uint8_t TimeUtils::parseSmallDuration(const char* value) +{ + int i = atoi(value); + if (i < 0 || i > 255) + { + dbprintf("TimeUtils::parseSmallDuration: invalid value %s: using 32 instead!\n", value); + i = 32; + } + return (uint8_t) i; +} + +uint8_t TimeUtils::parseOccurrence(const char* occurrence_string) +{ + int i = atoi(occurrence_string); + if (i < -5 || i == 0 || i > 5) + { + dbprintf("TimeUtils::parseOccurrence: invalid value %s: using 1 instead!\n", occurrence_string); + i = 1; + } + return (uint8_t) i; +} + +uint8_t TimeUtils::parseDayOfWeek(const char* dow_string) +{ + int i = atoi(dow_string); + if (i < 0 || i > 6) + { + dbprintf("TimeUtils::parseDayOfWeek: invalid value %s: using 0 (Sunday) instead!\n", dow_string); + i = 1; + } + return (uint8_t) i; +} + +uint8_t TimeUtils::parseMonth(const char* month_string) +{ + int i = atoi(month_string); + if (i < 0 || i > 12) + { + dbprintf("TimeUtils::parseMonth: invalid value '%s': using 3 (Mar) instead!\n", month_string); + i = 1; + } + return (uint8_t) i; +} + +uint8_t TimeUtils::parseHour(const char* hour_string) +{ + int i = atoi(hour_string); + if (i < 0 || i > 23) + { + dbprintf("TimeUtils::parseMonth: invalid value '%s': using 2 instead!\n", hour_string); + i = 1; + } + return (uint8_t) i; +} + +int TimeUtils::parseOffset(const char* offset_string) +{ + int result = 0; + char value[11]; + strncpy(value, offset_string, 10); + if (strchr(value, ':') != NULL) + { + int sign = 1; + char* s; + + if (value[0] == '-') + { + sign = -1; + s = strtok(&(value[1]), ":"); + } + else + { + s = strtok(value, ":"); + } + if (s != NULL) + { + int h = atoi(s); + while (h > 11) + { + h -= 12; + } + + result += h * 3600; // hours to seconds + s = strtok(NULL, ":"); + } + if (s != NULL) + { + result += atoi(s) * 60; // minutes to seconds + s = strtok(NULL, ":"); + } + if (s != NULL) + { + result += atoi(s); + } + // apply sign + result *= sign; + } + else + { + result = atoi(value); + if (result < -43199 || result > 43199) + { + result = 0; + } + } + return result; +} + +uint16_t TimeUtils::parsePosition(const char* position_string) +{ + int result = 0; + char value[10]; + strncpy(value, position_string, 9); + if (strchr(value, ':') != NULL) + { + char* s = strtok(value, ":"); + + if (s != NULL) + { + int h = atoi(s); + while (h > 11) + { + h -= 12; + } + + result += h * 3600; // hours to seconds + s = strtok(NULL, ":"); + } + if (s != NULL) + { + result += atoi(s) * 60; // minutes to seconds + s = strtok(NULL, ":"); + } + if (s != NULL) + { + result += atoi(s); + } + } + else + { + result = atoi(value); + if (result < 0 || result > 43199) + { + result = 0; + } + } + return result; +} + + +// +// Modified code from: http://www.jbox.dk/sanos/source/lib/time.c.html +// + +#define YEAR0 1900 +#define EPOCH_YR 1970 +#define SECS_DAY (24L * 60L * 60L) +#define LEAPYEAR(year) (!((year) % 4) && (((year) % 100) || !((year) % 400))) +#define YEARSIZE(year) (LEAPYEAR(year) ? 366 : 365) +#define TIME_MAX 2147483647L + +const int _ytab[2][12] = +{ +{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, +{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } }; + +// esp version is broken :-( +time_t TimeUtils::mktime(struct tm *tmbuf) +{ + long day, year; + int tm_year; + int yday, month; + /*unsigned*/long seconds; + int overflow; + + tmbuf->tm_min += tmbuf->tm_sec / 60; + tmbuf->tm_sec %= 60; + if (tmbuf->tm_sec < 0) + { + tmbuf->tm_sec += 60; + tmbuf->tm_min--; + } + tmbuf->tm_hour += tmbuf->tm_min / 60; + tmbuf->tm_min = tmbuf->tm_min % 60; + if (tmbuf->tm_min < 0) + { + tmbuf->tm_min += 60; + tmbuf->tm_hour--; + } + day = tmbuf->tm_hour / 24; + tmbuf->tm_hour = tmbuf->tm_hour % 24; + if (tmbuf->tm_hour < 0) + { + tmbuf->tm_hour += 24; + day--; + } + tmbuf->tm_year += tmbuf->tm_mon / 12; + tmbuf->tm_mon %= 12; + if (tmbuf->tm_mon < 0) + { + tmbuf->tm_mon += 12; + tmbuf->tm_year--; + } + day += (tmbuf->tm_mday - 1); + while (day < 0) + { + if (--tmbuf->tm_mon < 0) + { + tmbuf->tm_year--; + tmbuf->tm_mon = 11; + } + day += _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]; + } + while (day >= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]) + { + day -= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]; + if (++(tmbuf->tm_mon) == 12) + { + tmbuf->tm_mon = 0; + tmbuf->tm_year++; + } + } + tmbuf->tm_mday = day + 1; + year = EPOCH_YR; + if (tmbuf->tm_year < year - YEAR0) + return (time_t) -1; + seconds = 0; + day = 0; // Means days since day 0 now + overflow = 0; + + // Assume that when day becomes negative, there will certainly + // be overflow on seconds. + // The check for overflow needs not to be done for leapyears + // divisible by 400. + // The code only works when year (1970) is not a leapyear. + tm_year = tmbuf->tm_year + YEAR0; + + if (TIME_MAX / 365 < tm_year - year) + overflow++; + day = (tm_year - year) * 365; + if (TIME_MAX - day < (tm_year - year) / 4 + 1) + overflow++; + day += (tm_year - year) / 4 + ((tm_year % 4) && tm_year % 4 < year % 4); + day -= (tm_year - year) / 100 + + ((tm_year % 100) && tm_year % 100 < year % 100); + day += (tm_year - year) / 400 + + ((tm_year % 400) && tm_year % 400 < year % 400); + + yday = month = 0; + while (month < tmbuf->tm_mon) + { + yday += _ytab[LEAPYEAR(tm_year)][month]; + month++; + } + yday += (tmbuf->tm_mday - 1); + if (day + yday < 0) + overflow++; + day += yday; + + tmbuf->tm_yday = yday; + tmbuf->tm_wday = (day + 4) % 7; // Day 0 was thursday (4) + + seconds = ((tmbuf->tm_hour * 60L) + tmbuf->tm_min) * 60L + tmbuf->tm_sec; + + if ((TIME_MAX - seconds) / SECS_DAY < day) + overflow++; + seconds += day * SECS_DAY; + + if (overflow) + return (time_t) -1; + + if ((time_t) seconds != seconds) + return (time_t) -1; + return (time_t) seconds; +} + +struct tm *TimeUtils::gmtime_r(const time_t *timer, struct tm *tmbuf) +{ + time_t time = *timer; + unsigned long dayclock, dayno; + int year = EPOCH_YR; + + dayclock = (unsigned long) time % SECS_DAY; + dayno = (unsigned long) time / SECS_DAY; + + tmbuf->tm_sec = dayclock % 60; + tmbuf->tm_min = (dayclock % 3600) / 60; + tmbuf->tm_hour = dayclock / 3600; + tmbuf->tm_wday = (dayno + 4) % 7; // Day 0 was a thursday + while (dayno >= (unsigned long) YEARSIZE(year)) { + dayno -= YEARSIZE(year); + year++; + } + tmbuf->tm_year = year - YEAR0; + tmbuf->tm_yday = dayno; + tmbuf->tm_mon = 0; + while (dayno >= (unsigned long) _ytab[LEAPYEAR(year)][tmbuf->tm_mon]) { + dayno -= _ytab[LEAPYEAR(year)][tmbuf->tm_mon]; + tmbuf->tm_mon++; + } + tmbuf->tm_mday = dayno + 1; + tmbuf->tm_isdst = 0; + return tmbuf; +} + +// +// The functions findDOW & findNthDate are from: +// +// http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects +// + +/*-------------------------------------------------------------------------- + FUNC: 6/11/11 - Returns day of week for any given date + PARAMS: year, month, date + RETURNS: day of week (0-7 is Sun-Sat) + NOTES: Sakamoto's Algorithm + http://en.wikipedia.org/wiki/Calculating_the_day_of_the_week#Sakamoto.27s_methods + Altered to use char when possible to save microcontroller ram +--------------------------------------------------------------------------*/ +uint8_t TimeUtils::findDOW(uint16_t y, uint8_t m, uint8_t d) +{ + static char t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + y -= m < 3; + return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; +} + +/*-------------------------------------------------------------------------- + http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects + FUNC: 6/11/11 - Returns the date for Nth day of month. For instance, + it will return the numeric date for the 2nd Sunday of April + PARAMS: year, month, day of week, Nth occurrence of that day in that month + RETURNS: date + NOTES: There is no error checking for invalid inputs. +--------------------------------------------------------------------------*/ +uint8_t TimeUtils::findNthDate(uint16_t year, uint8_t month, uint8_t dow, uint8_t nthWeek) +{ + dbprintf("findNthDate: year:%u month:%u, dow:%u nthWeek:%d\n", year, month, dow, nthWeek); + + uint8_t targetDate = 1; + uint8_t firstDOW = findDOW(year,month,targetDate); + while (firstDOW != dow) { + firstDOW = (firstDOW+1)%7; + targetDate++; + } + //Adjust for weeks + targetDate += (nthWeek-1)*7; + return targetDate; +} + +uint8_t TimeUtils::daysInMonth(uint16_t year, uint8_t month) +{ + uint8_t days = 31; + + switch(month) + { + case 2: + days = 28; + if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) + { + days = 29; + } + break; + case 4: + case 6: + case 9: + case 11: + days = 30; + break; + } + return days; +} + +uint8_t TimeUtils::findDateForWeek(uint16_t year, uint8_t month, uint8_t dow, int8_t week) +{ + dbprintf("findDateForWeek: year:%u month:%u, dow:%u week:%d\n", year, month, dow, week); + + uint8_t weeks[5]; + uint8_t max_day = daysInMonth(year, month); + int last = 0; + + if (week >= 0) + { + return findNthDate(year, month, dow, week); + } + + // + // find all times this weekday shows up in the month + // Note that 'last' will end up pointing 1 past the last + // valid occurrence. -1 will give the last one. + // + for(last = 0; last <= 5; ++last) + { + weeks[last] = findNthDate(year, month, dow, last+1); + + dbprintf("findDateForWeek: last:%d date:%u\n", last, weeks[last]); + + if (weeks[last] > max_day) + { + break; + } + } + + return weeks[last+week]; +} + +int TimeUtils::computeUTCOffset(time_t now, int tz_offset, TimeChange* tc, int tc_count) +{ + struct tm tm; + + // + // get the current year + // + gmtime_r(&now, &tm); + int year = tm.tm_year; + + // + // pre-set the offset to the last timechange of the year + // + int offset = tc[tc_count-1].tz_offset; + + // + // loop thru each time change entry, converting it to the time in seconds for the + // current year. If now is greater/equal to the time change the use the new offset. + // We return the last offset that is greater/equal now. + // + for(int i = 0; i < tc_count; ++i) + { + dbprintf("TimeUtils::computeUTCOffset: index:%d offset:%d month:%u dow:%u occurrence:%d hour:%u day_offset:%d\n", + i, + tc[i].tz_offset, + tc[i].month, + tc[i].day_of_week, + tc[i].occurrence, + tc[i].hour, + tc[i].day_offset); + + tm.tm_sec = 0; + tm.tm_min = 0; + tm.tm_hour = tc[i].hour; + tm.tm_mday = TimeUtils::findDateForWeek(year+1900, tc[i].month, tc[i].day_of_week, tc[i].occurrence); + tm.tm_mon = tc[i].month-1; + tm.tm_year = year; + + dbprintf("computeUTCOffset: tm: %04d/%02d/%02d %02d:%02d:%02d + %d days\n", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tc[i].day_offset); + + // convert to seconds + time_t tc_time = mktime(&tm); + // convert to UTC + tc_time -= tz_offset; + dbprintf("computeUTCOffset: tc_time: %ld (UTC)\n", tc_time); + // add in days offset + tc_time += tc[i].day_offset*86400; + + dbprintf("computeUTCOffset: now: %ld tc_time: %ld\n", now, tc_time); + + if (now >= tc_time) + { + offset = tc[i].tz_offset; + dbprintf("computeUTCOffset: now > tc_time, offset: %d\n", offset); + } + } + + return offset; +} diff --git a/TimeUtils.h b/TimeUtils.h new file mode 100644 index 0000000..4c97873 --- /dev/null +++ b/TimeUtils.h @@ -0,0 +1,55 @@ +/* + * TimeUtils.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: Jun 7, 2017 + * Author: liebman + */ + +#ifndef TIMEUTILS_H_ +#define TIMEUTILS_H_ +#include +#include +typedef struct +{ + int tz_offset; // seconds offset from UTC + uint8_t month; // starting month that this takes effect. + int8_t occurrence; // 2 is second occurrence of the given day, -1 is last + uint8_t day_of_week; // 0 = Sunday + uint8_t hour; + int8_t day_offset; // +/- days (for Friday before last Sunday type) +} TimeChange; + +class TimeUtils +{ +public: + static uint8_t parseSmallDuration(const char* value); + static int parseOffset(const char* offset_string); + static uint16_t parsePosition(const char* position_string); + static uint8_t parseOccurrence(const char* occurrence_string); + static uint8_t parseDayOfWeek(const char* dow_string); + static uint8_t parseMonth(const char* month_string); + static uint8_t parseHour(const char* hour_string); + static time_t mktime(struct tm *tmbuf); + static struct tm* gmtime_r(const time_t *timer, struct tm *tmbuf); + static int computeUTCOffset(time_t now, int tz_offset, TimeChange* tc, int tc_count); + static uint8_t findDOW(uint16_t y, uint8_t m, uint8_t d); + static uint8_t findNthDate(uint16_t year, uint8_t month, uint8_t dow, uint8_t nthWeek); + static uint8_t daysInMonth(uint16_t year, uint8_t month); + static uint8_t findDateForWeek(uint16_t year, uint8_t month, uint8_t dow, int8_t nthWeek); +}; +#endif /* TIMEUTILS_H_ */ diff --git a/WireUtils.cpp b/WireUtils.cpp new file mode 100644 index 0000000..295c2fc --- /dev/null +++ b/WireUtils.cpp @@ -0,0 +1,112 @@ +/* + * WireUtils.cpp + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 26, 2017 + * Author: liebman + */ + +#include "WireUtils.h" + +#define DEBUG +#include "Logger.h" + +WireUtilsC::WireUtilsC() +{ +} + +/** + * This routine turns off the I2C bus and clears it + * on return SCA and SCL pins are tri-state inputs. + * You need to call Wire.begin() after this to re-enable I2C + * This routine does NOT use the Wire library at all. + * + * returns 0 if bus cleared + * 1 if SCL held low. + * 2 if SDA held low by slave clock stretch for > 2sec + * 3 if SDA held low after 20 clocks. + */ +int WireUtilsC::clearBus() +{ + dbprintln("WireUtils::ClearBus: attempting to clean i2c bus"); +#if defined(TWCR) && defined(TWEN) + TWCR &= ~(_BV(TWEN)); //Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly +#endif + pinMode(SDA, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup. + pinMode(SCL, INPUT_PULLUP); + + boolean SCL_LOW = (digitalRead(SCL) == LOW); // Check is SCL is Low. + if (SCL_LOW) + { //If it is held low Arduno cannot become the I2C master. + dbprintln("WireUtils::ClearBus: Failed! SCL held low!"); + return 1; //I2C bus error. Could not clear SCL clock line held low + } + + boolean SDA_LOW = (digitalRead(SDA) == LOW); // vi. Check SDA input. + int clockCount = 20; // > 2x9 clock + + while (SDA_LOW && (clockCount > 0)) + { // vii. If SDA is Low, + clockCount--; + // Note: I2C bus is open collector so do NOT drive SCL or SDA high. + pinMode(SCL, INPUT); // release SCL pullup so that when made output it will be LOW + pinMode(SCL, OUTPUT); // then clock SCL Low + delayMicroseconds(10); // for >5uS + pinMode(SCL, INPUT); // release SCL LOW + pinMode(SCL, INPUT_PULLUP); // turn on pullup resistors again + // do not force high as slave may be holding it low for clock stretching. + delayMicroseconds(10); // for >5uS + // The >5uS is so that even the slowest I2C devices are handled. + SCL_LOW = (digitalRead(SCL) == LOW); // Check if SCL is Low. + int counter = 20; + while (SCL_LOW && (counter > 0)) + { // loop waiting for SCL to become High only wait 2sec. + counter--; + delay(100); + SCL_LOW = (digitalRead(SCL) == LOW); + } + if (SCL_LOW) + { // still low after 2 sec error + dbprintln("WireUtils::ClearBus: Failed! SCL clock line held low by slave clock stretch for >2sec"); + return 2; // I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec + } + SDA_LOW = (digitalRead(SDA) == LOW); // and check SDA input again and loop + } + if (SDA_LOW) + { // still low + dbprintln("WireUtils::ClearBus: Failed! SDA data line still held low"); + return 3; // I2C bus error. Could not clear. SDA data line held low + } + + // else pull SDA line low for Start or Repeated Start + pinMode(SDA, INPUT); // remove pullup. + pinMode(SDA, OUTPUT); // and then make it LOW i.e. send an I2C Start or Repeated start control. + // When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus. + /// A Repeat Start is a Start occurring after a Start with no intervening Stop. + delayMicroseconds(10); // wait >5uS + pinMode(SDA, INPUT); // remove output low + pinMode(SDA, INPUT_PULLUP); // and make SDA high i.e. send I2C STOP control. + delayMicroseconds(10); // x. wait >5uS + pinMode(SDA, INPUT); // and reset pins as tri-state inputs which is the default state on reset + pinMode(SCL, INPUT); + dbprintln("WireUtils::ClearBus: Success!"); + return 0; // all ok +} + +// act like wire library (even though I don't like it) +WireUtilsC WireUtils = WireUtilsC(); + diff --git a/WireUtils.h b/WireUtils.h new file mode 100644 index 0000000..f20dd54 --- /dev/null +++ b/WireUtils.h @@ -0,0 +1,35 @@ +/* + * WireUtils.h + * + * Copyright 2017 Christopher B. Liebman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Created on: May 26, 2017 + * Author: liebman + */ + +#ifndef WIREUTILS_H_ +#define WIREUTILS_H_ +#include + +class WireUtilsC { +public: + WireUtilsC(); + int clearBus(); +}; + +extern WireUtilsC WireUtils; + +#endif /* WIREUTILS_H_ */