Skip to content

Instantly share code, notes, and snippets.

@synox
Created March 9, 2014 18:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save synox/b96e0a47965880330122 to your computer and use it in GitHub Desktop.
Save synox/b96e0a47965880330122 to your computer and use it in GitHub Desktop.
ampel-beta.ino
// Derived from LiquidCrystal by David Mellis
// With portions adapted from Elco Jacobs OLEDFourBit
// Modified for 4-bit operation of the Winstar 16x2 Character OLED
// By W. Earl for Adafruit - 6/30/12
// Initialization sequence fixed by Technobly - 9/22/2013
#include "Adafruit_CharacterOLED.h"
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
// On power up, the display is initilaized as:
// 1. Display clear
// 2. Function set:
// DL="1": 8-bit interface data
// N="0": 1-line display
// F="0": 5 x 8 dot character font
// 3. Power turn off
// PWR=”0”
// 4. Display on/off control: D="0": Display off C="0": Cursor off B="0": Blinking off
// 5. Entry mode set
// I/D="1": Increment by 1
// S="0": No shift
// 6. Cursor/Display shift/Mode / Pwr
// S/C=”0”, R/L=”1”: Shifts cursor position to the right
// G/C=”0”: Character mode
// Pwr=”1”: Internal DCDC power on
//
// Note, however, that resetting the Arduino doesn't reset the LCD, so we
// can't assume that its in that state when a sketch starts (and the
// LiquidCrystal constructor is called).
Adafruit_CharacterOLED::Adafruit_CharacterOLED(uint8_t ver, uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7)
{
init(ver, rs, rw, enable, d4, d5, d6, d7);
}
void Adafruit_CharacterOLED::init(uint8_t ver, uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7)
{
_oled_ver = ver;
if(_oled_ver != OLED_V1 && _oled_ver != OLED_V2) {
_oled_ver = OLED_V2; // if error, default to newer version
}
_rs_pin = rs;
_rw_pin = rw;
_enable_pin = enable;
_data_pins[0] = d4;
_data_pins[1] = d5;
_data_pins[2] = d6;
_data_pins[3] = _busy_pin = d7;
pinMode(_rs_pin, OUTPUT);
pinMode(_rw_pin, OUTPUT);
pinMode(_enable_pin, OUTPUT);
_displayfunction = LCD_FUNCTIONSET | LCD_4BITMODE;
begin(16, 2);
}
void Adafruit_CharacterOLED::begin(uint8_t cols, uint8_t lines)
{
_numlines = lines;
_currline = 0;
pinMode(_rs_pin, OUTPUT);
pinMode(_rw_pin, OUTPUT);
pinMode(_enable_pin, OUTPUT);
digitalWrite(_rs_pin, LOW);
digitalWrite(_enable_pin, LOW);
digitalWrite(_rw_pin, LOW);
delayMicroseconds(50000); // give it some time to power up
// Now we pull both RS and R/W low to begin commands
for (int i = 0; i < 4; i++) {
pinMode(_data_pins[i], OUTPUT);
digitalWrite(_data_pins[i], LOW);
}
// Initialization sequence is not quite as documented by Winstar.
// Documented sequence only works on initial power-up.
// An additional step of putting back into 8-bit mode first is
// required to handle a warm-restart.
//
// In the data sheet, the timing specs are all zeros(!). These have been tested to
// reliably handle both warm & cold starts.
// 4-Bit initialization sequence from Technobly
write4bits(0x03); // Put back into 8-bit mode
delayMicroseconds(5000);
if(_oled_ver == OLED_V2) { // only run extra command for newer displays
write4bits(0x08);
delayMicroseconds(5000);
}
write4bits(0x02); // Put into 4-bit mode
delayMicroseconds(5000);
write4bits(0x02);
delayMicroseconds(5000);
write4bits(0x08);
delayMicroseconds(5000);
command(0x08); // Turn Off
delayMicroseconds(5000);
command(0x01); // Clear Display
delayMicroseconds(5000);
command(0x06); // Set Entry Mode
delayMicroseconds(5000);
command(0x02); // Home Cursor
delayMicroseconds(5000);
command(0x0C); // Turn On - enable cursor & blink
delayMicroseconds(5000);
}
/********** high level commands, for the user! */
void Adafruit_CharacterOLED::clear()
{
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
// delayMicroseconds(2000); // this command takes a long time!
}
void Adafruit_CharacterOLED::home()
{
command(LCD_RETURNHOME); // set cursor position to zero
// delayMicroseconds(2000); // this command takes a long time!
}
void Adafruit_CharacterOLED::setCursor(uint8_t col, uint8_t row)
{
uint8_t row_offsets[] = { 0x00, 0x40, 0x14, 0x54 };
if ( row >= _numlines )
{
row = 0; //write to first line if out off bounds
}
command(LCD_SETDDRAMADDR | (col + row_offsets[row]));
}
// Turn the display on/off (quickly)
void Adafruit_CharacterOLED::noDisplay()
{
_displaycontrol &= ~LCD_DISPLAYON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void Adafruit_CharacterOLED::display()
{
_displaycontrol |= LCD_DISPLAYON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
// Turns the underline cursor on/off
void Adafruit_CharacterOLED::noCursor()
{
_displaycontrol &= ~LCD_CURSORON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void Adafruit_CharacterOLED::cursor()
{
_displaycontrol |= LCD_CURSORON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
// Turn on and off the blinking cursor
void Adafruit_CharacterOLED::noBlink()
{
_displaycontrol &= ~LCD_BLINKON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void Adafruit_CharacterOLED::blink()
{
_displaycontrol |= LCD_BLINKON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
// These commands scroll the display without changing the RAM
void Adafruit_CharacterOLED::scrollDisplayLeft(void)
{
command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT);
}
void Adafruit_CharacterOLED::scrollDisplayRight(void)
{
command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT);
}
// This is for text that flows Left to Right
void Adafruit_CharacterOLED::leftToRight(void)
{
_displaymode |= LCD_ENTRYLEFT;
command(LCD_ENTRYMODESET | _displaymode);
}
// This is for text that flows Right to Left
void Adafruit_CharacterOLED::rightToLeft(void)
{
_displaymode &= ~LCD_ENTRYLEFT;
command(LCD_ENTRYMODESET | _displaymode);
}
// This will 'right justify' text from the cursor
void Adafruit_CharacterOLED::autoscroll(void)
{
_displaymode |= LCD_ENTRYSHIFTINCREMENT;
command(LCD_ENTRYMODESET | _displaymode);
}
// This will 'left justify' text from the cursor
void Adafruit_CharacterOLED::noAutoscroll(void)
{
_displaymode &= ~LCD_ENTRYSHIFTINCREMENT;
command(LCD_ENTRYMODESET | _displaymode);
}
// Allows us to fill the first 8 CGRAM locations
// with custom characters
void Adafruit_CharacterOLED::createChar(uint8_t location, uint8_t charmap[])
{
location &= 0x7; // we only have 8 locations 0-7
command(LCD_SETCGRAMADDR | (location << 3));
for (int i=0; i<8; i++)
{
write(charmap[i]);
}
}
/*********** mid level commands, for sending data/cmds */
inline void Adafruit_CharacterOLED::command(uint8_t value)
{
send(value, LOW);
waitForReady();
}
inline size_t Adafruit_CharacterOLED::write(uint8_t value)
{
send(value, HIGH);
waitForReady();
}
/************ low level data pushing commands **********/
// write either command or data
void Adafruit_CharacterOLED::send(uint8_t value, uint8_t mode)
{
digitalWrite(_rs_pin, mode);
pinMode(_rw_pin, OUTPUT);
digitalWrite(_rw_pin, LOW);
write4bits(value>>4);
write4bits(value);
}
void Adafruit_CharacterOLED::pulseEnable(void)
{
digitalWrite(_enable_pin, HIGH);
delayMicroseconds(50); // Timing Spec?
digitalWrite(_enable_pin, LOW);
}
void Adafruit_CharacterOLED::write4bits(uint8_t value)
{
for (int i = 0; i < 4; i++)
{
pinMode(_data_pins[i], OUTPUT);
digitalWrite(_data_pins[i], (value >> i) & 0x01);
}
delayMicroseconds(50); // Timing spec?
pulseEnable();
}
// Poll the busy bit until it goes LOW
void Adafruit_CharacterOLED::waitForReady(void)
{
unsigned char busy = 1;
pinMode(_busy_pin, INPUT);
digitalWrite(_rs_pin, LOW);
digitalWrite(_rw_pin, HIGH);
do
{
digitalWrite(_enable_pin, LOW);
digitalWrite(_enable_pin, HIGH);
delayMicroseconds(10);
busy = digitalRead(_busy_pin);
digitalWrite(_enable_pin, LOW);
pulseEnable(); // get remaining 4 bits, which are not used.
}
while(busy);
pinMode(_busy_pin, OUTPUT);
digitalWrite(_rw_pin, LOW);
}
#ifndef Adafruit_CharacterOLED_h
#define Adafruit_CharacterOLED_h
#include "application.h"
#include <inttypes.h>
// OLED hardware versions
#define OLED_V1 0x01
#define OLED_V2 0x02
// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x28
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80
// flags for display entry mode
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00
// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00
// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00
// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_JAPANESE 0x00
#define LCD_EUROPEAN_I 0x01
#define LCD_RUSSIAN 0x02
#define LCD_EUROPEAN_II 0x03
class Adafruit_CharacterOLED : public Print {
public:
Adafruit_CharacterOLED(uint8_t ver, uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7);
void init(uint8_t ver, uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7);
void begin(uint8_t cols, uint8_t rows);
void clear();
void home();
void noDisplay();
void display();
void noBlink();
void blink();
void noCursor();
void cursor();
void scrollDisplayLeft();
void scrollDisplayRight();
void leftToRight();
void rightToLeft();
void autoscroll();
void noAutoscroll();
void createChar(uint8_t, uint8_t[]);
void setCursor(uint8_t, uint8_t);
virtual size_t write(uint8_t);
void command(uint8_t);
private:
void send(uint8_t, uint8_t);
void write4bits(uint8_t);
void pulseEnable();
void waitForReady();
uint8_t _oled_ver; // OLED_V1 = older, OLED_V2 = newer hardware version.
uint8_t _rs_pin; // LOW: command. HIGH: character.
uint8_t _rw_pin; // LOW: write to LCD. HIGH: read from LCD.
uint8_t _enable_pin; // activated by a HIGH pulse.
uint8_t _busy_pin; // HIGH means not ready for next command
uint8_t _data_pins[4];
uint8_t _displayfunction;
uint8_t _displaycontrol;
uint8_t _displaymode;
uint8_t _initialized;
uint8_t _currline;
uint8_t _numlines;
};
#endif
// This #include statement was automatically added by the Spark IDE.
#include "http.h"
// This #include statement was automatically added by the Spark IDE.
#include "Adafruit_CharacterOLED.h"
#include "SparkTime.h"
#include "Ampel.h"
UDP ntpClient;
SparkTime rtc;
Adafruit_CharacterOLED *lcd;
int led = D7;
unsigned int nextTime = 0; // next time to contact the server
Ampel ampel;
void setup() {
Serial.begin(9600);
// setup time
rtc.begin(&ntpClient, "0.europe.pool.ntp.org");
rtc.setTimeZone(+1); // gmt offset
pinMode(led, OUTPUT);
// setup led
RGB.control(true);
RGB.brightness(255);
// setup lcd
lcd = new Adafruit_CharacterOLED(OLED_V1, D0, D1, D2, D3, D4, D5, D6);
lcd->clear();
ampel.init(lcd);
}
void loop() {
if (nextTime > millis()) {
// keep the same color while waiting
return;
}
digitalWrite(led, HIGH);
delay(20);
digitalWrite(led, LOW);
unsigned long now = rtc.now();
Serial.print("now: ");
Serial.println(now);
// if there is a problem with the time server, the rest makes no sense
if(now == 0) {
lcd->setCursor(0,0);
lcd->print("ERROR: can not ");
lcd->setCursor(0,1);
lcd->print("load timestamp! ");
// check again in 10 seconds:
nextTime = millis() + 10*1000;
return;
}
// organize connection cache
ampel.loadConnections(now);
ampel.printCache();
ampel.updateLed(now);
ampel.updateDisplay(now);
// check again in 5 seconds:
nextTime = millis() + 5000;
}
#include "Ampel.h"
// ------------- configuration ------------
// set the parametrs from= and to= (keep the fields[] as it is).
const char* query1 = "/v1/connections?from=Wabern,Gurtenbahn&to=Bern&fields[]=connections/from/departure&limit=6";
const char* connName1 = ">Bern";
Status Ampel::getStatus(int diffSeconds) {
if (diffSeconds < -60) {
// too late
return off;
} else if (diffSeconds < 90) {
return missed;
} else if (diffSeconds < 3 * 60) {
return run;
} else if (diffSeconds < 3.5 * 60 ) {
return leave_now;
} else if (diffSeconds < 5 * 60) {
return walk;
} else {
// longer, then turn led off
return off;
}
}
void Ampel::updateLED(Status status) {
RGB.control(true);
switch(status) {
case off: RGB.color(0,0,0); break;
case missed: RGB.color(255,0,0); break; // red
case run: RGB.color(255,50,0); break;// orange
case leave_now:RGB.color(255,150,0); break;// yellow
case walk: RGB.color(0,255,0); break; // green
}
}
// is a display connected?
#define DISPLAY true
// ------------- enf of configuration ------------
#define CONNECTION_CACHE_SIZE 10
unsigned long connections[CONNECTION_CACHE_SIZE];
void Ampel::init (Adafruit_CharacterOLED *lcd) {
this->lcd = lcd;
// init connection array
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
connections[i] = 0;
}
}
void Ampel::cleanupCache(unsigned long now) {
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
if(connections[i] == 0) {
// empty
} else if(connections[i] < now - 60) {
// delete old entries
connections[i] = 0;
}
if(i > 0 && connections[i-1] == 0) {
// if prev entry is empty: move current
connections[i-1] = connections[i];
connections[i] = 0;
}
}
}
unsigned int Ampel::getCacheSize() {
unsigned int count = 0;
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
if(connections[i] != 0) {
count++;
}
}
return count;
}
void Ampel::loadConnections(unsigned long now) {
cleanupCache( now);
if (getCacheSize() <=2 ) {
lcd->setCursor(0,1);
lcd->print("updating data...");
Serial.println("loading connections...");
// refresh connections
String resp = http_get("transport.opendata.ch", query1);
parseFahrplan(resp);
}
}
void Ampel::parseFahrplan(String jsonData) {
int offset = 0;
do {
offset = jsonData.indexOf("departure\":\"", offset);
if(DEBUG) Serial.print("offset: ");
if(DEBUG) Serial.println(offset);
if (offset == -1) {
break;
}
offset += 12; // move to timestamp
String str = jsonData.substring(offset, offset + 24);
if(DEBUG) Serial.print("date: ");
if(DEBUG) Serial.println(str);
if(str.length() == 0) {
continue;
}
long ts = parseDateWithTimezone(str);
addConnection(ts);
} while (offset >= 0);
}
/**
* Adds the given timestamp to the connection array. Multiple arrays might be possible.
*/
void Ampel::addConnection(unsigned long ts) {
if(DEBUG) Serial.print("fahrplan ts:");
if(DEBUG) Serial.println(ts);
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
if (connections[i] == ts) {
// already in table, ignore
return;
} else if (connections[i] == 0) {
// found empty slot, add ts. empty slots are in the end.
connections[i] = ts;
return;
}
}
}
void Ampel::printCache() {
Serial.println("conn:");
Serial.println("-----------");
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
Serial.println(connections[i]);
}
Serial.println("-------");
}
void Ampel::updateLed(unsigned long now) {
Status status = calculateStatus(now);
updateLED(status);
}
Status Ampel::calculateStatus(unsigned long now) {
// Connections might be overlapping. In case you have every 2 minutes a connection,
// you want to see green all the time. the enum Status is ordered by increasing priority.
Status bestStatus = off;
for (int i = 0; i < CONNECTION_CACHE_SIZE; i++) {
unsigned long ts = connections[i];
if(ts == 0) {
continue;
}
int diff = ts - now;
Status newColor = getStatus(diff);
if (newColor > bestStatus) {
bestStatus = newColor;
}
if(bestStatus == walk) {
// it is as good as it gets, stop evaluating
break;
}
}
return bestStatus;
}
void Ampel::updateDisplay(unsigned long now) {
//displayTime(now, 1, 0);
if(DEBUG) Serial.println("updating display...");
lcd->clear();
// assuming it is a 16 column, 2 row display
int row = 0;
int col = 0;
for (int i = 0; i < CONNECTION_CACHE_SIZE && row < 2; i++) {
long diff = connections[i] - now;
if(diff < 0 || diff > 3600 ) {
continue;
} else {
if(col == 0 && row == 0) {
// Connection name
lcd->setCursor(0,row);
lcd->print(connName1);
lcd->print(" ");
col = strlen(connName1) + 1;
}
unsigned int remaining = 16 - col;
String time(diff / 60 ); // minutes
if(time.length() <= remaining) {
// Print duration
lcd->print(time);
col += time.length();
// Space between words
if(col < 16) {
lcd->print(" ");
col++;
}
} else {
// not enough space:
// go to line 2
if (row == 0) {
row = 1;
col = 7; // skip time in row 2
lcd->setCursor(col,row);
lcd->print(time);
col += time.length();
lcd->print(" ");
col++;
} else {
// end of display, finish
return;
}
}
}
}
}
void Ampel::displayTime(unsigned long ts, int row, int col) {
/* long tmpTime = ts + timezoneOffset; // fix offset
struct tm * timeinfo;
timeinfo = gmtime (&tmpTime);
char buffer[10];
strftime (buffer, 10, "%H:%M ", timeinfo);
lcd->setCursor(col, row);
lcd->print(buffer);*/
}
// ------ date functions ------
signed int timezoneOffset = 0; // defered from time string
long parseDate(String str) {
// TODO: it assumes it is running in UTC. mktime() uses the local time (time zone) for creating timestamp.
// parse date. timegm() would be better, but is not available.
struct tm time;
strptime(str.c_str(), "%Y-%m-%dT%H:%M:%S", &time);
return (long) mktime(&time);
}
long parseTzOffset(String str) {
// strptime currently does not parse the timezone with %z, so we do it ourself:
// parse 3 digits the "+0100" which result in 1 hour.
int offsetHours = atoi(str.substring(19,22).c_str());
return offsetHours * 3600;
}
/**
* parse a string of the form "2014-01-11T17:17:59+0200" and fixes the timezone offset
*/
long parseDateWithTimezone(String str) {
long ts = parseDate(str);
timezoneOffset = parseTzOffset(str);
ts -= timezoneOffset;
if(DEBUG) Serial.print("convert ");
if(DEBUG) Serial.print(str);
if(DEBUG) Serial.print(" to ");
if(DEBUG) Serial.println(ts);
return ts;
}
#ifndef _AMPEL
#define _AMPEL
#include "Adafruit_CharacterOLED.h"
#include "http.h"
#include <time.h>
#define DEBUG 0
// possible status list:
enum Status {
// increasing priority, if there is "walk" and "run", it shows "walk".
off=1, missed=2, run=3, leave_now=4, walk=5
};
class Ampel {
public:
void init(Adafruit_CharacterOLED *lcd);
void cleanupCache(unsigned long now);
unsigned int getCacheSize();
void loadConnections(unsigned long now);
void printCache();
void updateLed(unsigned long now);
void updateDisplay(unsigned long now);
private:
Adafruit_CharacterOLED *lcd;
void parseFahrplan(String jsonData);
void addConnection(unsigned long ts);
Status calculateStatus(unsigned long now);
void updateLED(Status status);
Status getStatus(int diffSeconds);
void displayTime(unsigned long ts, int row, int col);
};
/**
* parse a string of the form "2014-01-11T17:17:59+0200" and fixes the timezone offset
*/
long parseDateWithTimezone(String str);
/**
* parse a string of the form "2014-01-11T17:17:59+0200"
*/
long parseDate(String str) ;
/**
* can parse the timezone offset in the string "2014-01-11T17:17:59+0100"
*/
long parseTzOffset(String str) ;
#endif
#include "application.h"
/**
* make http request and return body
*/
TCPClient client;
char buffer[1024];
String http_get(char const* hostname, String path) {
if (client.connect(hostname, 80)) {
client.print("GET ");
client.print(path);
client.print(" HTTP/1.0\n");
client.print("HOST: ");
client.println(hostname);
client.print("\n");
client.print("Connection: close\n\n");
client.flush();
} else {
Serial.println("connection failed");
client.stop();
return NULL;
}
uint32_t startTime = millis();
int index = 0;
while( (millis() - startTime) < 5000){
if(client.available()) {
char c = client.read();
// Serial.print(c);
if (c == -1) {
break;
}
buffer[index++] = c;
}
if(index >= 1024) {
break;
}
}
client.flush();
client.stop();
String response(buffer);
int bodyPos = response.indexOf("\r\n\r\n");
if (bodyPos == -1) {
Serial.println("can not find http reponse body");
return NULL;
}
return response.substring(bodyPos + 4);
}
String http_get(char const* hostname, String path);
/* Spark Time by Brian Ogilvie
Inspired by Arduino Time by Michael Margolis
Copyright (c) 2014 Brian Ogilvie. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
TODO: Fractional second handling for NTP and millis();
Translation for other languages
*/
#include "SparkTime.h"
SparkTime::SparkTime()
{
_UDPClient = NULL;
_timezone = -5;
_useDST = true;
_useEuroDSTRule = false;
_syncedOnce = false;
_interval = 60UL * 60UL;
_localPort = 2390;
}
void SparkTime::begin(UDP * UDPClient, const char * NTPServer) {
_UDPClient = UDPClient;
memcpy(_serverName, NTPServer, strlen(NTPServer)+1);
}
void SparkTime::begin(UDP * UDPClient) {
_UDPClient = UDPClient;
const char NTPServer[] = "pool.ntp.org";
memcpy(_serverName, NTPServer, strlen(NTPServer)+1);
}
bool SparkTime::hasSynced() {
return _syncedOnce;
}
void SparkTime::setUseDST(bool value) {
_useDST = value;
}
void SparkTime::setUseEuroDSTRule(bool value) {
_useEuroDSTRule = value;
}
uint8_t SparkTime::hour(uint32_t tnow) {
uint8_t hTemp = ((tnow+timeZoneDSTOffset(tnow)) % 86400UL)/3600UL;
return hTemp;
}
uint8_t SparkTime::minute(uint32_t tnow) {
return (((tnow+timeZoneDSTOffset(tnow)) % 3600) / 60);
}
uint8_t SparkTime::second(uint32_t tnow) {
return ((tnow+timeZoneDSTOffset(tnow)) % 60);
}
uint8_t SparkTime::dayOfWeek(uint32_t tnow) {
uint32_t dayNum = (tnow + timeZoneDSTOffset(tnow)-SPARKTIMEEPOCHSTART)/SPARKTIMESECPERDAY;
//Unix epoch day 0 was a thursday
return ((dayNum+4)%7);
}
uint8_t SparkTime::day(uint32_t tnow) {
uint32_t dayNum = (tnow+timeZoneDSTOffset(tnow)-SPARKTIMEBASESTART)/SPARKTIMESECPERDAY;
uint32_t tempYear = SPARKTIMEBASEYEAR;
uint8_t tempMonth = 0;
while(dayNum >= YEARSIZE(tempYear)) {
dayNum -= YEARSIZE(tempYear);
tempYear++;
}
while(dayNum >= _monthLength[LEAPYEAR(tempYear)][tempMonth]) {
dayNum -= _monthLength[LEAPYEAR(tempYear)][tempMonth];
tempMonth++;
}
dayNum++; // correct for zero-base
return (uint8_t)dayNum;
}
uint8_t SparkTime::month(uint32_t tnow) {
uint32_t dayNum = (tnow+timeZoneDSTOffset(tnow)-SPARKTIMEBASESTART)/SPARKTIMESECPERDAY;
uint32_t tempYear = SPARKTIMEBASEYEAR;
uint8_t tempMonth = 0;
while(dayNum >= YEARSIZE(tempYear)) {
dayNum -= YEARSIZE(tempYear);
tempYear++;
}
while(dayNum >= _monthLength[LEAPYEAR(tempYear)][tempMonth]) {
dayNum -= _monthLength[LEAPYEAR(tempYear)][tempMonth];
tempMonth++;
}
tempMonth++;
return tempMonth;
}
uint32_t SparkTime::year(uint32_t tnow) {
uint32_t dayNum = (tnow+timeZoneDSTOffset(tnow)-SPARKTIMEBASESTART)/SPARKTIMESECPERDAY;
uint32_t tempYear = SPARKTIMEBASEYEAR;
while(dayNum >= YEARSIZE(tempYear)) {
dayNum -= YEARSIZE(tempYear);
tempYear++;
}
return tempYear;
}
String SparkTime::hourString(uint32_t tnow) {
return String(_digits[hour(tnow)]);
}
String SparkTime::hour12String(uint32_t tnow) {
uint8_t tempHour = hour(tnow);
if (tempHour>12) {
tempHour -= 12;
}
if (tempHour == 0) {
tempHour = 12;
}
return String(_digits[tempHour]);
}
String SparkTime::minuteString(uint32_t tnow) {
return String(_digits[minute(tnow)]);
}
String SparkTime::secondString(uint32_t tnow) {
return String(_digits[second(tnow)]);
}
String SparkTime::AMPMString(uint32_t tnow) {
uint8_t tempHour = hour(tnow);
if (tempHour<12) {
return String("AM");
} else {
return String("PM");
}
}
String SparkTime::dayOfWeekShortString(uint32_t tnow) {
return String(_days_short[dayOfWeek(tnow)]);
}
String SparkTime::dayOfWeekString(uint32_t tnow) {
return String(_days[dayOfWeek(tnow)]);
}
String SparkTime::dayString(uint32_t tnow) {
return String(_digits[day(tnow)]);
}
String SparkTime::monthString(uint32_t tnow) {
return String(_digits[month(tnow)]);
}
String SparkTime::monthNameShortString(uint32_t tnow) {
return String(_months_short[month(tnow)-1]);
}
String SparkTime::monthNameString(uint32_t tnow) {
return String(_months[month(tnow)-1]);
}
String SparkTime::yearShortString(uint32_t tnow) {
uint32_t tempYear = year(tnow)%100;
if (tempYear<10) {
return String(_digits[tempYear]);
} else {
return String(tempYear);
}
}
String SparkTime::yearString(uint32_t tnow) {
return String(year(tnow));
}
String SparkTime::ISODateString(uint32_t tnow) {
String ISOString;
ISOString += yearString(tnow);
ISOString += "-";
ISOString += monthString(tnow);
ISOString += "-";
ISOString += dayString(tnow);
ISOString += "T";
ISOString += hourString(tnow);
ISOString += ":";
ISOString += minuteString(tnow);
ISOString += ":";
ISOString += secondString(tnow);
int32_t offset = timeZoneDSTOffset(tnow)/3600L;
// Guard against timezone problems
if (offset>-24 && offset<24) {
if (offset < 0) {
ISOString = ISOString + "-" + _digits[-offset] + "00";
} else {
ISOString = ISOString + "+" + _digits[offset] + "00";
}
}
return ISOString;
}
String SparkTime::ISODateUTCString(uint32_t tnow) {
uint32_t savedTimeZone = _timezone;
bool savedUseDST = _useDST;
_timezone = 0;
_useDST = false;
String ISOString;
ISOString += yearString(tnow);
ISOString += "-";
ISOString += monthString(tnow);
ISOString += "-";
ISOString += dayString(tnow);
ISOString += "T";
ISOString += hourString(tnow);
ISOString += ":";
ISOString += minuteString(tnow);
ISOString += ":";
ISOString += secondString(tnow);
ISOString += "Z";
_timezone = savedTimeZone;
_useDST = savedUseDST;
return ISOString;
}
uint32_t SparkTime::now() {
if (!_syncedOnce && !_isSyncing) {
updateNTPTime();
}
if (!_syncedOnce) { // fail!
return SPARKTIMEBASEYEAR; // Jan 1, 2014
}
return nowNoUpdate();
}
uint32_t SparkTime::nowNoUpdate() {
uint32_t mTime = millis();
//unsigned long mFracSec = (mTime%1000); // 0 to 999 miliseconds
//unsigned long nowFrac = _lastSyncNTPFrac + mFracSec*2^22/1000;
uint32_t nowSec = _lastSyncNTPTime + ((mTime - _lastSyncMillisTime)/1000);
if (_lastSyncMillisTime>mTime) { // has wrapped
nowSec = nowSec + SPARKTIMEWRAPSECS;
}
if (nowSec >= (_lastSyncNTPTime + _interval)) {
updateNTPTime();
}
return nowSec;
}
uint32_t SparkTime::nowEpoch() {
return (now()-SPARKTIMEEPOCHSTART);
}
uint32_t SparkTime::lastNTPTime() {
return _lastSyncNTPTime;
}
void SparkTime::setNTPInvterval(uint32_t intervalMinutes) {
const uint32_t fiveMinutes = 5UL * 60UL;
uint32_t interval = intervalMinutes * 60UL;
_interval = max(fiveMinutes, interval);
}
void SparkTime::setTimeZone(int32_t hoursOffset) {
_timezone = hoursOffset;
}
bool SparkTime::isUSDST(uint32_t tnow) {
// 2am 2nd Sunday in March to 2am 1st Sunday in November
// can't use offset here
bool result = false;
uint32_t dayNum = (tnow+_timezone*3600UL-SPARKTIMEBASESTART)/SPARKTIMESECPERDAY;
uint32_t tempYear = SPARKTIMEBASEYEAR;
uint8_t tempMonth = 0;
uint8_t tempHour = ((tnow+_timezone*3600UL) % 86400UL)/3600UL;
while(dayNum >= YEARSIZE(tempYear)) {
dayNum -= YEARSIZE(tempYear);
tempYear++;
}
while(dayNum >= _monthLength[LEAPYEAR(tempYear)][tempMonth]) {
dayNum -= _monthLength[LEAPYEAR(tempYear)][tempMonth];
tempMonth++;
}
tempMonth++;
dayNum++; // correct for zero-base
if (tempMonth>3 && tempMonth<11) {
result = true;
} else if (tempMonth == 3) {
if ((dayNum == _usDSTStart[tempYear-SPARKTIMEBASEYEAR] && tempHour >=2) ||
(dayNum > _usDSTStart[tempYear-SPARKTIMEBASEYEAR])) {
result = true;
}
} else if (tempMonth == 11) {
if (!((dayNum == _usDSTEnd[tempYear-SPARKTIMEBASEYEAR] && tempHour >=2) ||
(dayNum > _usDSTEnd[tempYear-SPARKTIMEBASEYEAR]))) {
result = true;
}
}
return result;
}
bool SparkTime::isEuroDST(uint32_t tnow) {
// 1am last Sunday in March to 1am on last Sunday October
// can't use offset here
bool result = false;
uint32_t dayNum = (tnow+_timezone*3600UL-SPARKTIMEBASESTART)/SPARKTIMESECPERDAY;
uint32_t tempYear = SPARKTIMEBASEYEAR;
uint8_t tempMonth = 0;
uint8_t tempHour = ((tnow+_timezone*3600UL) % 86400UL)/3600UL;
while(dayNum >= YEARSIZE(tempYear)) {
dayNum -= YEARSIZE(tempYear);
tempYear++;
}
while(dayNum >= _monthLength[LEAPYEAR(tempYear)][tempMonth]) {
dayNum -= _monthLength[LEAPYEAR(tempYear)][tempMonth];
tempMonth++;
}
tempMonth++;
dayNum++; // correct for zero-base
if (tempMonth>3 && tempMonth<10) {
result = true;
} else if (tempMonth == 3) {
if ((dayNum == _EuDSTStart[tempYear-SPARKTIMEBASEYEAR] && tempHour >=1) ||
(dayNum > _EuDSTStart[tempYear-SPARKTIMEBASEYEAR])) {
result = true;
}
} else if (tempMonth == 10) {
if (!((dayNum == _usDSTEnd[tempYear-SPARKTIMEBASEYEAR] && tempHour >=1) ||
(dayNum > _usDSTEnd[tempYear-SPARKTIMEBASEYEAR]))) {
result = true;
}
}
return result;
}
void SparkTime::updateNTPTime() {
//digitalWrite(D7,HIGH);
_isSyncing = true;
_UDPClient->begin(_localPort);
memset(_packetBuffer, 0, SPARKTIMENTPSIZE);
_packetBuffer[0] = 0b11100011; // LI, Version, Mode
_packetBuffer[1] = 0; // Stratum, or type of clock
_packetBuffer[2] = 6; // Polling Interval
_packetBuffer[3] = 0xEC; // Peer Clock Precision
// 4-11 are zero
_packetBuffer[12] = 49;
_packetBuffer[13] = 0x4E;
_packetBuffer[14] = 49;
_packetBuffer[15] = 52;
_UDPClient->beginPacket(_serverName, 123); //NTP requests are to port 123
_UDPClient->write(_packetBuffer,SPARKTIMENTPSIZE);
_UDPClient->endPacket();
//gather the local offset close to the send time
uint32_t localmsec = millis();
int32_t retries = 0;
int32_t bytesrecv = _UDPClient->parsePacket();
while(bytesrecv == 0 && retries < 1000) {
bytesrecv = _UDPClient->parsePacket();
retries++;
}
if (bytesrecv>0) {
_UDPClient->read(_packetBuffer,SPARKTIMENTPSIZE);
// Handle Kiss-of-death
if (_packetBuffer[1]==0) {
_interval = max(_interval * 2, 24UL*60UL*60UL);
}
_lastSyncNTPTime = _packetBuffer[40] << 24 | _packetBuffer[41] << 16 | _packetBuffer[42] << 8 | _packetBuffer[43];
_lastSyncNTPFrac = _packetBuffer[44] << 24 | _packetBuffer[45] << 16 | _packetBuffer[46] << 8 | _packetBuffer[47];
_lastSyncMillisTime = localmsec;
_syncedOnce = true;
}
//digitalWrite(D7,LOW);
_UDPClient->stop();
_isSyncing = false;
}
int32_t SparkTime::timeZoneDSTOffset(uint32_t tnow) {
int32_t result = _timezone*3600UL;
if ((_useDST && (!_useEuroDSTRule && isUSDST(tnow))) ||
(_useDST && (_useEuroDSTRule && isEuroDST(tnow)))) {
result += 3600UL;
}
return result;
}
#ifndef _SPARKTIME
#define _SPARKTIME
#include "application.h"
#define SPARKTIMENTPSIZE 48
#define SPARKTIMEHOSTNAMESIZE 64
#define SPARKTIMEYEARZERO 1900
#define SPARKTIMEEPOCHSTART 2208988800UL
#define SPARKTIMESECPERDAY 86400UL
#define SPARKTIMEBASESTART 3597523200UL
#define SPARKTIMEBASEYEAR 2014UL
#define LEAPYEAR(year) (!((year) % 4) && (((year) % 100) || !((year) % 400)))
#define YEARSIZE(year) (LEAPYEAR(year) ? 366 : 365)
#define SPARKTIMEWRAPSECS 4294967UL
class SparkTime {
public:
SparkTime();
void begin(UDP * UPDClient);
void begin(UDP * UPDClient, const char * NTPServer);
bool hasSynced();
void setNTPInvterval(uint32_t intervalMinutes);
void setTimeZone(int32_t hoursOffset);
void setUseDST(bool value);
void setUseEuroDSTRule(bool value);
uint32_t now();
uint32_t nowNoUpdate();
uint32_t nowEpoch();
uint32_t lastNTPTime();
uint8_t hour(uint32_t tnow);
uint8_t minute(uint32_t tnow);
uint8_t second(uint32_t tnow);
uint8_t dayOfWeek(uint32_t tnow);
uint8_t day(uint32_t tnow);
uint8_t month(uint32_t tnow);
uint32_t year(uint32_t tnow);
bool isUSDST(uint32_t tnow); //2nd Sun in Mar to 1st Sun in Nov
bool isEuroDST(uint32_t tnow); //Last Sun in Mar to last Sun in Oct
String hourString(uint32_t tnow);
String hour12String(uint32_t tnow);
String minuteString(uint32_t tnow);
String secondString(uint32_t tnow);
String AMPMString(uint32_t tnow);
String dayOfWeekShortString(uint32_t tnow);
String dayOfWeekString(uint32_t tnow);
String dayString(uint32_t tnow);
String monthString(uint32_t tnow);
String monthNameShortString(uint32_t tnow);
String monthNameString(uint32_t tnow);
String yearShortString(uint32_t tnow);
String yearString(uint32_t tnow);
String ISODateString(uint32_t tnow);
String ISODateUTCString(uint32_t tnow);
private:
const char _digits[60][3] = {"00","01","02","03","04","05","06","07","08","09",
"10","11","12","13","14","15","16","17","18","19",
"20","21","22","23","24","25","26","27","28","29",
"30","31","32","33","34","35","36","37","38","39",
"40","41","42","43","44","45","46","47","48","49",
"50","51","52","53","54","55","56","57","58","59"}; /* for speed! */
const char _days[7][10] = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
const char _days_short[7][4] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
const char _months[12][10] = {"January","February","March","April","May","June","July","August","September","October","November","December"};
const char _months_short[12][4] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
const uint8_t _monthLength[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}}; // Leap year
// for years 2014-2036 Day in March or November
// 2014 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
const uint8_t _usDSTStart[23] = { 9, 8,13,12,11,10, 8,14,13,12,10, 9, 8,14,12,11,10, 9,14,13,12,11, 9};
const uint8_t _usDSTEnd[23] = { 2, 1, 6, 5, 4, 3, 1, 7, 6, 5, 3, 2, 1, 7, 5, 4, 3, 2, 7, 6, 5, 4, 2};
const uint8_t _EuDSTStart[23] = {30,29,27,26,25,31,29,28,27,26,31,30,29,28,26,25,31,30,28,27,26,25,30};
const uint8_t _EuDSTEnd[23] = {26,25,30,29,28,27,25,31,30,29,27,26,25,31,29,28,27,26,31,30,29,28,26};
UDP * _UDPClient;
char _serverName[SPARKTIMEHOSTNAMESIZE];
bool _syncedOnce = false;
bool _isSyncing = false;
uint32_t _lastSyncMillisTime;
uint32_t _lastSyncNTPTime;
uint32_t _lastSyncNTPFrac;
uint32_t _interval;
int32_t _timezone;
bool _useDST;
bool _useEuroDSTRule;
uint32_t _localPort;
uint8_t _packetBuffer[SPARKTIMENTPSIZE];
void updateNTPTime();
int32_t timeZoneDSTOffset(uint32_t tnow);
};
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment