From 9c65ef10e938ea4473b647a8671725399f4ed915 Mon Sep 17 00:00:00 2001 From: Chris Liebman Date: Fri, 3 Nov 2017 19:03:05 -0700 Subject: [PATCH] Initial checkin, hacky code, using DS3231 instead of GPS unit. --- .gitignore | 12 ++ DS3231.cpp | 231 +++++++++++++++++++++ DS3231.h | 101 ++++++++++ DS3231DateTime.cpp | 214 ++++++++++++++++++++ DS3231DateTime.h | 60 ++++++ ESPNTPServer.cpp | 279 ++++++++++++++++++++++++++ ESPNTPServer.h | 93 +++++++++ Logger.cpp | 159 +++++++++++++++ Logger.h | 91 +++++++++ README.md | 4 + TimeUtils.cpp | 491 +++++++++++++++++++++++++++++++++++++++++++++ TimeUtils.h | 55 +++++ WireUtils.cpp | 112 +++++++++++ WireUtils.h | 35 ++++ 14 files changed, 1937 insertions(+) create mode 100644 .gitignore create mode 100644 DS3231.cpp create mode 100644 DS3231.h create mode 100644 DS3231DateTime.cpp create mode 100644 DS3231DateTime.h create mode 100644 ESPNTPServer.cpp create mode 100644 ESPNTPServer.h create mode 100644 Logger.cpp create mode 100644 Logger.h create mode 100644 TimeUtils.cpp create mode 100644 TimeUtils.h create mode 100644 WireUtils.cpp create mode 100644 WireUtils.h 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_ */