=NTP_TIMEOUT) ntpGoing = 0; //time out
+ return false;
+ }
+ // We've received a packet, read the data from it
+ ntpSyncLast = millis(); if(!ntpSyncLast) ntpSyncLast = -1; //never let it be zero
+ unsigned int requestTime = ntpSyncLast-ntpStartLast;
+ Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
+
+ //https://forum.arduino.cc/index.php?topic=526792.0
+ //epoch in earlier bits? needed after 2038
+ //TODO leap second notification in earlier bits?
+ ntpTime = (packetBuffer[40] << 24) | (packetBuffer[41] << 16) | (packetBuffer[42] << 8) | packetBuffer[43];
+ unsigned long ntpFrac = (packetBuffer[44] << 24) | (packetBuffer[45] << 16) | (packetBuffer[46] << 8) | packetBuffer[47];
+ ntpMils = (int32_t)(((float)ntpFrac / UINT32_MAX) * 1000);
+
+ //Account for the request time
+ ntpMils += requestTime/2;
+ if(ntpMils>=1000) { ntpMils -= 1000; ntpTime++; }
+
+ // Serial.print(F("NTP time: "));
+ // Serial.print(ntpTime,DEC);
+ // Serial.print(F("."));
+ // Serial.print(ntpMils,DEC);
+ // Serial.print(F(" ±"));
+ // Serial.print(requestTime,DEC);
+
+ //Unless the mils are bang on, we'll wait to set the clock until the next full second.
+ if(ntpMils>0) ntpTime++;
+
+ // Serial.print(F(" - set to "));
+ // Serial.print(ntpTime,DEC);
+ // if(ntpMils==0) Serial.print(F(" immediately"));
+ // else { Serial.print(F(" after ")); Serial.print(1000-ntpMils,DEC); }
+ // Serial.println();
+
+ Udp.flush(); //in case of extraneous(?) data
+ //Udp.stop() was formerly here
+ ntpGoing = 0; //next if{} block will handle this
+ }
+ if(!ntpGoing){
+ //If we are waiting to start, do it (asynchronously)
+ if(ntpCued){ startNTP(false); ntpCued=false; return false; }
+ //If we are not waiting to set, do nothing
+ if(!ntpTime) return false;
+ //If we are waiting to set, but it's not time, wait for the next cycle
+ //but return true since we successfully got a time to set to
+ if(ntpMils!=0 && (unsigned long)(millis()-ntpSyncLast)<(1000-ntpMils)) return true;
+ //else it's time!
+ //Serial.print(millis(),DEC); Serial.println(F("NTP complete"));
+
+ //Convert unix timestamp to UTC date/time
+ //TODO this assumes epoch 0, which is only good til 2038, I think!
+ ntpTime -= 3155673600; //from 1900 to 2000, assuming epoch 0
+ unsigned long ntpPart = ntpTime;
+ int y = 2000;
+ while(1){ //iterate to find year
+ unsigned long yearSecs = daysInYear(y)*86400;
+ if(ntpPart > yearSecs){
+ ntpPart-=yearSecs; y++;
+ } else break;
+ }
+ byte m = 1;
+ while(1){ //iterate to find month
+ unsigned long monthSecs = daysInMonth(y,m)*86400;
+ if(ntpPart > monthSecs){
+ ntpPart-=monthSecs; m++;
+ } else break;
+ }
+ byte d = 1+(ntpPart/86400); ntpPart %= 86400;
+ int hm = ntpPart/60; //mins from midnight
+ byte s = ntpPart%60;
+
+ //Take UTC date/time and apply standard offset
+ //which involves checking for date rollover
+ //eeprom loc 14 is UTC offset in quarter-hours plus 100 - range is 52 (-12h or -48qh, US Minor Outlying Islands) to 156 (+14h or +56qh, Kiribati)
+ int utcohm = (readEEPROM(14,false)-100)*15; //utc offset in mins from midnight
+ if(hm+utcohm<0){ //date rolls backward
+ hm = hm+utcohm+1440; //e.g. -1 to 1439 which is 23:59
+ d--; if(d<1){ m--; if(m<1){ y--; m=12; } d=daysInMonth(y,m); } //month or year rolls backward
+ } else if(hm+utcohm>1439){ //date rolls forward
+ hm = (hm+utcohm)%1440; //e.g. 1441 to 1 which is 00:01
+ d++; if(d>daysInMonth(y,m)){ m++; if(m>12){ y++; m=1; } d=1; } //month or year rolls forward
+ } else hm += utcohm;
+
+ //then check DST at that time (setting DST flag), and add an hour if necessary
+ //which involves checking for date rollover again (forward only)
+ //TODO this may behave unpredictably from 1–2am on fallback day since that occurs twice - check to see whether it has been applied already per the difference from utc
+ if(isDSTByHour(y,m,d,hm/60,true)){
+ if(hm+60>1439){ //date rolls forward
+ hm = (hm+60)%1440; //e.g. 1441 to 1 which is 00:01
+ d++; if(d>daysInMonth(y,m)){ m++; if(m>12){ y++; m=1; } d=1; } //month or year rolls forward
+ } else hm += 60;
+ }
+
+ //finally set the rtc
+ rtcSetDate(y, m, d, dayOfWeek(y,m,d));
+ rtcSetTime(hm/60,hm%60,s);
+ calcSun();
+
+ // Serial.print(F("RTC set to "));
+ // Serial.print(rtcGetYear(),DEC); Serial.print(F("-"));
+ // if(rtcGetMonth()<10) Serial.print(F("0")); Serial.print(rtcGetMonth(),DEC); Serial.print(F("-"));
+ // if(rtcGetDate()<10) Serial.print(F("0")); Serial.print(rtcGetDate(),DEC); Serial.print(F(" "));
+ // if(rtcGetHour()<10) Serial.print(F("0")); Serial.print(rtcGetHour(),DEC); Serial.print(F(":"));
+ // if(rtcGetMinute()<10) Serial.print(F("0")); Serial.print(rtcGetMinute(),DEC); Serial.print(F(":"));
+ // if(rtcGetSecond()<10) Serial.print(F("0")); Serial.print(rtcGetSecond(),DEC);
+ // Serial.println();
+
+ ntpTime = 0; ntpMils = 0; //no longer waiting to set
+ updateDisplay();
+ return true; //successfully got a time and set to it
+ }
+} //end fn checkNTP
+
+void clearNTPSyncLast(){
+ //called when other code divorces displayed time from NTP sync
+ ntpSyncLast = 0;
+}
+
+unsigned long adminInputLast = 0; //for noticing when the admin page hasn't been interacted with in 2 minutes, so we can time it (and AP if applicable) out
+
+void networkStartAdmin(){
+ adminInputLast = millis();
+ if(WiFi.status()!=WL_CONNECTED){
+ networkStartAP();
+ tempDisplay(7777); //display to user
+ //Serial.println(F("Admin started at 7.7.7.7"));
+ } else { //use existing wifi
+ IPAddress theip = WiFi.localIP();
+ tempDisplay(theip[0],theip[1],theip[2],theip[3]); //display to user
+ //Serial.print(F("Admin started at "));
+ //Serial.println(theip);
+ }
+ updateDisplay();
+}
+void networkStopAdmin(){
+ //Serial.println(F("stopping admin"));
+ adminInputLast = 0; //TODO use a different flag from adminInputLast
+ if(WiFi.status()!=WL_CONNECTED) networkStartWiFi();
+}
+
+//unsigned long debugLast = 0;
+void checkClients(){
+ // if((unsigned long)(millis()-debugLast)>=1000) { debugLast = millis();
+ // Serial.print("Hello ");
+ // Serial.println(WiFi.status());
+ // }
+ //if(WiFi.status()!=WL_CONNECTED && WiFi.status()!=WL_AP_CONNECTED) return;
+ if(adminInputLast && (unsigned long)(millis()-adminInputLast)>=ADMIN_TIMEOUT) networkStopAdmin();
+ WiFiClient client = server.available();
+ if(client) {
+ if(adminInputLast==0) {
+ client.flush(); client.stop();
+ //Serial.print(F("Got a client but ditched it because last admin input was over ")); Serial.print(ADMIN_TIMEOUT); Serial.println(F("ms ago."));
+ return;
+ }
+ else {
+ //Serial.print(F("Last admin input was ")); Serial.print(millis()-adminInputLast); Serial.print(F("ms ago which is under the limit of ")); Serial.print(ADMIN_TIMEOUT); Serial.println(F("ms."));
+ }
+
+ adminInputLast = millis();
+
+ String currentLine = ""; //we'll read the data from the client one line at a time
+ int requestType = 0;
+ bool newlineSeen = false;
+
+ if(client.connected()){
+ while(client.available()){ //if there's bytes to read from the client
+ char c = client.read();
+ //Serial.write(c); //DEBUG
+
+ if(c=='\n') newlineSeen = true;
+ else {
+ if(newlineSeen){ currentLine = ""; newlineSeen = false; } //if we see a newline and then something else: clear current line
+ currentLine += c;
+ }
+
+ //Find the request type and path from the first line.
+ if(!requestType){
+ if(currentLine=="GET / ") { requestType = 1; break; } //Read no more. We'll render out the page.
+ if(currentLine=="POST / ") requestType = 2; //We'll keep reading til the last line.
+ if(c=='\n') break; //End of first line without matching the above: invalid request, return nothing.
+ }
+
+ } //end whie client available
+ } //end if client connected
+
+ if(requestType){
+ // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
+ // and a content-type so the client knows what's coming, then a blank line:
+ client.println("HTTP/1.1 200 OK");
+ client.println("Content-type:text/html");
+ client.println("Access-Control-Allow-Origin:*");
+ client.println();
+ if(requestType==1){ //get
+
+ //Re hiding irrelevant settings/options, see also fnOptScroll in main code
+
+ client.print(F("Clock SettingsClock Settings
Loading…
If page doesn't appear in a few seconds, refresh.
"));
+ //client.print(F(""));
+ } //end get
+ else { //requestType==2 - handle what was POSTed
+ bool clientReturn = false; //Mark true when sending an error. If none, "ok" is sent at end. If nothing sent (crash), client displays generic error.
+ //client.print(currentLine);
+ //syncfreq=hr
+ //syncfreq=min
+ if(currentLine.startsWith(F("wssid="))){ //wifi change
+ //e.g. wssid=Network Name&wpass=qwertyuiop&wki=1
+ //TODO since the values are not html-entitied (due to the difficulty of de-entiting here), this will fail if the ssid contains "&wpass=" or pass contains "&wki="
+ int startPos = 6;
+ int endPos = currentLine.indexOf(F("&wpass="),startPos);
+ wssid = currentLine.substring(startPos,endPos);
+ startPos = endPos+7;
+ endPos = currentLine.indexOf(F("&wki="),startPos);
+ wpass = currentLine.substring(startPos,endPos);
+ startPos = endPos+5;
+ wki = currentLine.substring(startPos).toInt();
+ //Persistent storage - see wssid/wpass definitions above
+ for(byte i=0; i<97; i++) writeEEPROM(55+i,0,false,false); //Clear out the old values (32+64+1)
+ for(byte i=0; i-1? currentLine.substring(startPos,endPos).toInt(): currentLine.substring(startPos).toInt());
+ ntpip[i] = octet; startPos = endPos+1;
+ if(ntpip[i]!=octet){ parseOK = false; break; }
+ }
+ if(!parseOK) { clientReturn = true; client.print(F("Error: invalid format")); }
+ else for(byte i=0; i<4; i++) writeEEPROM(51+i,ntpip[i],false);
+ //Serial.print(F("IP should be ")); Serial.print(ntpip[0],DEC); Serial.print(F(".")); Serial.print(ntpip[1],DEC); Serial.print(F(".")); Serial.print(ntpip[2],DEC); Serial.print(F(".")); Serial.println(ntpip[3],DEC);
+ //Serial.print(F("IP saved as ")); Serial.print(readEEPROM(51,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(52,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(53,false),DEC); Serial.print(F(".")); Serial.println(readEEPROM(54,false),DEC);
+ } else if(currentLine.startsWith(F("syncnow"))){
+ //TODO this doesn't seem to return properly if the wifi was changed after the clock was booted - it syncs, but just hangs
+ int ntpCode = startNTP(true);
+ switch(ntpCode){
+ case -1: client.print(F("Error: no Wi-Fi credentials.")); break;
+ case -2: client.print(F("Error: not connected to Wi-Fi.")); break;
+ case -3: client.print(F("Error: NTP response pending. Please try again shortly.")); break; //should never see this one on the web since it's synchronous and the client blocks
+ case -4: client.print(F("Error: too many sync requests in the last ")); client.print(NTP_MINFREQ/1000,DEC); client.print(F(" seconds. Please try again shortly.")); break;
+ case -5: client.print(F("Error: no NTP response received. Please confirm server.")); break;
+ case 0: client.print(F("synced")); break;
+ default: client.print(F("Error: unhandled NTP code")); break;
+ }
+ clientReturn = true;
+ } else if(currentLine.startsWith(F("curtod"))){
+ int tod = currentLine.substring(7).toInt();
+ rtcSetTime(tod/60,tod%60,0);
+ ntpSyncLast = 0;
+ goToFn(FN_TOD);
+ } else if(currentLine.startsWith(F("curdatey"))){
+ rtcSetDate(currentLine.substring(9).toInt(), rtcGetMonth(), rtcGetDate(), dayOfWeek(currentLine.substring(9).toInt(), rtcGetMonth(), rtcGetDate())); //TODO what about month exceed
+ ntpSyncLast = 0;
+ goToFn(FN_CAL,254);
+ } else if(currentLine.startsWith(F("curdatem"))){
+ rtcSetDate(rtcGetYear(), currentLine.substring(9).toInt(), rtcGetDate(), dayOfWeek(rtcGetYear(), currentLine.substring(9).toInt(), rtcGetDate())); //TODO what about month exceed
+ goToFn(FN_CAL,254);
+ } else if(currentLine.startsWith(F("curdated"))){
+ rtcSetDate(rtcGetYear(), rtcGetMonth(), currentLine.substring(9).toInt(), dayOfWeek(rtcGetYear(), rtcGetMonth(), currentLine.substring(9).toInt())); //TODO what about month exceed
+ goToFn(FN_CAL,254);
+ } else if(currentLine.startsWith(F("almtime"))){
+ writeEEPROM(0,currentLine.substring(7).toInt(),true);
+ goToFn(FN_ALARM);
+ } else if(currentLine.startsWith(F("alm"))){ //two settings (alarm on, alarm skip) with one control. Compare to switchAlarmState()
+ setAlarmState(currentLine.substring(4).toInt());
+ goToFn(FN_ALARM);
+ } else if(currentLine.startsWith(F("runout"))){
+ char runout = currentLine.substring(7).toInt();
+ setTimerState(3,runout/2); //chrono bit
+ setTimerState(2,runout%2); //restart bit
+ } else if(currentLine.startsWith(F("nighttod"))){
+ writeEEPROM(28,currentLine.substring(9).toInt(),true);
+ } else if(currentLine.startsWith(F("morntod"))){
+ writeEEPROM(30,currentLine.substring(8).toInt(),true);
+ } else if(currentLine.startsWith(F("worktod"))){
+ writeEEPROM(35,currentLine.substring(8).toInt(),true);
+ } else if(currentLine.startsWith(F("hometod"))){
+ writeEEPROM(37,currentLine.substring(8).toInt(),true);
+ } else {
+ //standard eeprom saves by type/loc
+ bool isInt = currentLine.startsWith(F("i")); //or b for byte
+ int eqPos = currentLine.indexOf(F("="));
+ int key = currentLine.substring(1,eqPos).toInt();
+ int val = currentLine.substring(eqPos+1).toInt();
+ writeEEPROM(key,val,isInt);
+ //do special stuff for some of them
+ switch(key){
+ case 4: case 5: case 6: //day counter
+ //in lieu of actually switching to FN_CAL, so that only this value is seen - compare to ino
+ if(readEEPROM(4,false)) tempDisplay(dateComp(rtcGetYear(),rtcGetMonth(),rtcGetDate(), readEEPROM(5,false),readEEPROM(6,false),readEEPROM(4,false)-1));
+ findFnAndPageNumbers(); //to include or exclude the day counter from the calendar function
+ break;
+ case 14: //utc offset
+ cueNTP(); break;
+ case 17: //date format
+ goToFn(FN_CAL,254); break;
+ case 22: //auto dst
+ isDSTByHour(rtcGetYear(),rtcGetMonth(),rtcGetDate(),rtcGetHour(),true); break;
+ case 39: case 47: //alarm pitch/pattern
+ goToFn(FN_ALARM); break;
+ case 40: case 48: //timer pitch/pattern
+ goToFn(FN_TIMER); break;
+ case 41: case 49: //strike pitch/pattern
+ goToFn(FN_TOD); break;
+ default: break;
+ }
+ if(key==39 || key==40 || key==41){ //play beeper pitch sample - compare to updateDisplay()
+ quickBeep(val);
+ }
+ if(key==47 || key==48 || key==49){ //play beeper pattern sample - compare to updateDisplay()
+ quickBeepPattern((key==49?FN_TOD:(key==48?FN_TIMER:FN_ALARM)),val);
+ }
+ }
+ updateDisplay();
+ if(!clientReturn) client.print(F("ok"));
+ } //end post
+ } //end if requestType
+
+ client.stop();
+ //Serial.println("");
+ //Serial.println("client disconnected");
+ delay(500); //for client to get the message TODO why is this necessary
+
+ if(requestType==3) { //wifi was changed - restart the admin
+ networkStartWiFi(); //try to connect to wifi with new settings
+ networkStartAdmin(); //will set up AP if wifi isn't connected
+ }
+ }
+}
+
+#else
+
+bool networkSupported(){ return false; }
+
+#endif //__AVR__ (network supported)
\ No newline at end of file
diff --git a/arduino-clock/network.h b/arduino-clock/network.h
new file mode 100644
index 0000000..b193aad
--- /dev/null
+++ b/arduino-clock/network.h
@@ -0,0 +1,20 @@
+#ifndef NETWORK_H
+#define NETWORK_H
+
+bool networkSupported();
+void checkForWiFiStatusChange();
+void networkStartWiFi();
+void networkStartAP();
+void networkDisconnectWiFi();
+unsigned long ntpSyncAgo();
+void cueNTP();
+int startNTP(bool synchronous);
+bool checkNTP();
+void clearNTPSyncLast();
+void networkStartAdmin();
+void networkStopAdmin();
+void checkClients();
+void initNetwork();
+void cycleNetwork();
+
+#endif //NETWORK_H
\ No newline at end of file
diff --git a/arduino-clock/rtcDS3231.cpp b/arduino-clock/rtcDS3231.cpp
new file mode 100644
index 0000000..11fcd93
--- /dev/null
+++ b/arduino-clock/rtcDS3231.cpp
@@ -0,0 +1,51 @@
+#include
+#include "arduino-clock.h"
+
+#ifdef RTC_DS3231 //see arduino-clock.ino Includes section
+
+#include "rtcDS3231.h"
+#include //Arduino - GNU LPGL - for I2C access to DS3231
+#include //NorthernWidget - The Unlicense - install in your Arduino IDE
+
+//RTC objects
+DS3231 ds3231; //an object to access the ds3231 specifically (temp, etc)
+RTClib rtc; //an object to access a snapshot of the ds3231 via rtc.now()
+DateTime tod; //stores the rtc.now() snapshot for several functions to use
+byte todW; //stores the day of week (read separately from ds3231 dow counter)
+
+void rtcInit(){
+ Wire.begin();
+}
+void rtcSetTime(byte h, byte m, byte s){
+ ds3231.setHour(h);
+ ds3231.setMinute(m);
+ ds3231.setSecond(s);
+ millisReset();
+}
+void rtcSetDate(int y, byte m, byte d, byte w){
+ ds3231.setYear(y%100); //TODO: should we store century on our end? Per ds3231 docs, "The century bit (bit 7 of the month register) is toggled when the years register overflows from 99 to 00."
+ ds3231.setMonth(m);
+ ds3231.setDate(d);
+ ds3231.setDoW(w+1); //ds3231 weekday is 1-index
+}
+void rtcSetHour(byte h){ //used for DST forward/backward
+ ds3231.setHour(h);
+}
+
+void rtcTakeSnap(){
+ //rtcGet functions pull from this snapshot - to ensure that code works off the same timestamp
+ tod = rtc.now();
+ todW = ds3231.getDoW()-1; //ds3231 weekday is 1-index
+}
+int rtcGetYear(){ return tod.year(); }
+byte rtcGetMonth(){ return tod.month(); }
+byte rtcGetDate(){ return tod.day(); }
+byte rtcGetWeekday(){ return todW; }
+int rtcGetTOD(){ return tod.hour()*60+tod.minute(); }
+byte rtcGetHour(){ return tod.hour(); }
+byte rtcGetMinute(){ return tod.minute(); }
+byte rtcGetSecond(){ return tod.second(); }
+
+byte rtcGetTemp(){ return ds3231.getTemperature()*100; }
+
+#endif //RTC_DS3231
\ No newline at end of file
diff --git a/arduino-clock/rtcDS3231.h b/arduino-clock/rtcDS3231.h
new file mode 100644
index 0000000..85f842d
--- /dev/null
+++ b/arduino-clock/rtcDS3231.h
@@ -0,0 +1,24 @@
+#ifndef RTC_DS3231_H
+#define RTC_DS3231_H
+
+//Mutually exclusive with other rtc options
+
+void rtcInit();
+void rtcSetTime(byte h, byte m, byte s);
+void rtcSetDate(int y, byte m, byte d, byte w);
+void rtcSetHour(byte h);
+
+void rtcTakeSnap();
+
+int rtcGetYear();
+byte rtcGetMonth();
+byte rtcGetDate();
+byte rtcGetWeekday();
+int rtcGetTOD();
+byte rtcGetHour();
+byte rtcGetMinute();
+byte rtcGetSecond();
+
+byte rtcGetTemp();
+
+#endif
\ No newline at end of file
diff --git a/arduino-clock/rtcMillis.cpp b/arduino-clock/rtcMillis.cpp
new file mode 100644
index 0000000..0f3363e
--- /dev/null
+++ b/arduino-clock/rtcMillis.cpp
@@ -0,0 +1,67 @@
+#include
+#include "arduino-clock.h"
+
+#ifdef RTC_MILLIS //see arduino-clock.ino Includes section
+
+#include "rtcMillis.h"
+
+////////// FAKE RTC using millis //////////
+
+//snapshot of time of day
+unsigned long todMils = 0; //time of day in milliseconds
+int todY = 2020;
+byte todM = 1;
+byte todD = 1;
+byte todW = 0;
+unsigned long millisAtTOD = 0; //reflects millis at snapshot
+
+void rtcInit(){}
+void rtcSetTime(byte h, byte m, byte s){
+ todMils = (h*3600000)+(m*60000)+(s*1000);
+ millisAtTOD = millis();
+ millisReset();
+}
+void rtcSetDate(int y, byte m, byte d, byte w){
+ todY = y; todM = m; todD = d; todW = w;
+}
+void rtcSetHour(byte h){
+ todMils = (h*3600000)+(todMils%60000);
+ millisAtTOD = millis();
+}
+
+void rtcTakeSnap(){
+ unsigned long millisNow = millis();
+ //Increment todMils per the change in millis
+ todMils += millisNow-millisAtTOD;
+
+ //Apply anti-drift
+ unsigned long drift = (abs(ANTI_DRIFT) * (millisNow-millisAtTOD))/1000;
+ if(ANTI_DRIFT>0) todMils += drift;
+ if(ANTI_DRIFT<0) todMils -= drift;
+ //any issues with data types/truncation here?
+
+ //Update the millis snap
+ millisAtTOD = millisNow;
+ //Handle midnight rollover
+ //This may behave erratically if rtcTakeSnap() is not called for long enough that todMils rolls over,
+ //but this should not happen, because except for short hangs (eg wifi connect) we should be calling it at least 1/sec
+ if(todMils >= 86400000){
+ while(todMils >= 86400000) todMils = todMils - 86400000; //while is just to ensure it's below 86400000
+ if(todD==daysInMonth(todY,todM)){ todD = 1; todM++; if(todM==13){ todM==1; todY++; } }
+ else todD++;
+ todW++; if(todW>6) todW=0;
+ }
+}
+
+int rtcGetYear(){ return todY; }
+byte rtcGetMonth(){ return todM; }
+byte rtcGetDate(){ return todD; }
+byte rtcGetWeekday(){ return todW; }
+int rtcGetTOD(){ return (todMils/1000)/60; }
+byte rtcGetHour(){ return (todMils/1000)/3600; }
+byte rtcGetMinute(){ return ((todMils/1000)/60)%60; }
+byte rtcGetSecond(){ return (todMils/1000)%60; }
+
+byte rtcGetTemp(){ return 1000; } //a fake response - ten degrees (1000 hundredths) forever
+
+#endif //RTC_MILLIS
\ No newline at end of file
diff --git a/arduino-clock/rtcMillis.h b/arduino-clock/rtcMillis.h
new file mode 100644
index 0000000..169cbe5
--- /dev/null
+++ b/arduino-clock/rtcMillis.h
@@ -0,0 +1,24 @@
+#ifndef RTC_MILLIS_H
+#define RTC_MILLIS_H
+
+//Mutually exclusive with other rtc options
+
+void rtcInit();
+void rtcSetTime(byte h, byte m, byte s);
+void rtcSetDate(int y, byte m, byte d, byte w);
+void rtcSetHour(byte h);
+
+void rtcTakeSnap();
+
+int rtcGetYear();
+byte rtcGetMonth();
+byte rtcGetDate();
+byte rtcGetWeekday();
+int rtcGetTOD();
+byte rtcGetHour();
+byte rtcGetMinute();
+byte rtcGetSecond();
+
+byte rtcGetTemp();
+
+#endif
\ No newline at end of file
diff --git a/arduino-clock/storage.cpp b/arduino-clock/storage.cpp
new file mode 100644
index 0000000..d5642fe
--- /dev/null
+++ b/arduino-clock/storage.cpp
@@ -0,0 +1,84 @@
+// Persistent storage
+
+// This project was originally written to use the AVR Arduino's EEPROM for persistent storage, and would frequently read it directly during runtime (not just at startup). I wanted to abstract that away, partly to add support for SAMD flash memory, and partly to protect against runtime errors due to EEPROM/flash failure.
+// This code serves those values out of a volatile array of bytes, which are backed by EEPROM/flash for the sole purpose of recovery after a power failure. It reads them from EEPROM/flash at startup, and sets them when changed.
+// Flash support is via cmaglie's FlashStorage library. It offers an EEPROM emulation mode, which I'm currently using, but I don't like that it writes the entire "EEPROM" data to flash (not just the value being updated) with every commit() – I think it will wear out unnecessarily. TODO address this.
+// Note that flash data is necessarily wiped out when the sketch is (re)uploaded.
+
+#include
+#include "arduino-clock.h"
+
+#include "storage.h"
+
+#ifdef __AVR__
+ #include //Arduino - GNU LPGL
+#else //SAMD - is there a better way to detect EEPROM availability? TODO
+ #define FLASH_AS_EEPROM
+ //cmaglie's FlashStorage library - https://github.com/cmaglie/FlashStorage/
+ #include //EEPROM mode
+ //#include //regular mode
+#endif
+
+#define STORAGE_SPACE 152 //number of bytes
+byte storageBytes[STORAGE_SPACE]; //the volatile array of bytes
+#define COMMIT_TO_EEPROM 1 //1 for production
+
+void initStorage(){
+ //If this is SAMD, write starting values if unused
+ #ifdef FLASH_AS_EEPROM
+ //Serial.println(F("Hello world from storage.cpp"));
+ //Serial.print(F("valid=")); Serial.println(EEPROM.isValid(),DEC);
+ //Serial.print(F("16=")); Serial.println(EEPROM.read(16));
+ if(!EEPROM.isValid() || EEPROM.read(16)==0 || EEPROM.read(16)==255){ //invalid eeprom, wipe it out
+ for(byte i=0; i