Timezone_Generic Library to convert UTC to local time

Working libraries, libraries being ported and related hardware
Post Reply
khoih-prog
Posts: 102
Joined: Thu Feb 27, 2020 7:54 am
Location: Toronto

Timezone_Generic Library to convert UTC to local time

Post by khoih-prog »

Timezone_Generic Library

How To Install Using Arduino Library Manager


Why do we need this Timezone_Generic Library

The Timezone_Generic Library is designed to work in conjunction with the Arduino Time library, which must also be installed on your system. This documentation assumes some familiarity with the Time library.

The primary goal of the Timezone_Generic Library is to convert Universal Coordinated Time (UTC) to the correct local time, whether it is Daylight Saving Time (a.k.a. summer time, DST) or standard time. The time source could be a GPS receiver, an NTP server, or a Real-Time Clock (RTC) set to UTC. But whether a hardware RTC or other time source is even present is immaterial, since the Time library can function as a software RTC without additional hardware (although its accuracy is dependent on the accuracy of the microcontroller's system clock.)

The Timezone_Generic Library implements two objects to facilitate time zone conversions:

- A TimeChangeRule object describes when local time changes to daylight (summer) time, or to standard time, for a particular locale.

- A Timezone object uses TimeChangeRules to perform conversions and related functions. It can also write its TimeChangeRules to or read them from EEPROM/DueFlashStorage/FlashStorage/LittleFS/SPIFFS. Multiple time zones can be represented by defining multiple Timezone objects.

The examples will demonstrate how to get the UTC time from NTP server, then update the DS3231 RTC to make sure the time is perfectly correct.
You can also modify the examples to read the NTP and update RTC once per every pre-determined period to ensure the RTC accuracy.

This Timezone_Generic Library is based on and modified from Jack Christensen's Timezone Library to add functions and support to many boards and shields.


Releases v1.2.5

1. Add examples to use STM32 Built-In RTC.

Releases v1.2.4

1. Initial porting to many Generic boards using WiFi/Ethernet modules/shields.
2. Add support to SAMD21/SAMD51, nRF52, STM32F/L/H/G/WB/MP1 with WiFiNINA, ESP8266-AT, W5x00, ENC28J60, LAN8742A Ethernet modules/shields.
3. Add support to SAM DUE DueFlashStorage, SAMD FlashStorage, nRF52 LittleFS, STM32 and AVR EEPROM
4. Add functions.
5. Completely new examples using NTP time to update DS3231 RTC.


Currently Supported Boards

 - ESP8266. To be done soon.
 - ESP32. To be done soon.
 - AdaFruit Feather nRF52832, nRF52840 Express, BlueFruit Sense, Itsy-Bitsy nRF52840 Express, Metro nRF52840 Express, NINA_B302_ublox, NINA_B112_ublox etc..
 - Arduino SAMD21 (ZERO, MKR, NANO_33_IOT, etc.).
 - Adafruit SAMD21 (Itsy-Bitsy M0, Metro M0, Feather M0, Gemma M0, etc.).
 - Adafruit SAMD51 (Itsy-Bitsy M4, Metro M4, Grand Central M4, Feather M4 Express, etc.).
 - Seeeduino SAMD21/SAMD51 boards (SEEED_WIO_TERMINAL, SEEED_FEMTO_M0, SEEED_XIAO_M0, Wio_Lite_MG126, WIO_GPS_BOARD, SEEEDUINO_ZERO, SEEEDUINO_LORAWAN, SEEED_GROVE_UI_WIRELESS, etc.)
 - STM32 (Nucleo-144, Nucleo-64, Nucleo-32, Discovery, STM32F1, STM32F3, STM32F4, STM32H7, STM32L0, etc.).
 - STM32F/L/H/G/WB/MP1 (Nucleo-64 L053R8,Nucleo-144, Nucleo-64, Nucleo-32, Discovery, STM32Fx, STM32H7, STM32Lx, STM32Gx, STM32WB, STM32MP1, etc.) having 64K+ Flash program memory.
 
Currently Supported WiFi Modules/Shields

 - ESP8266 built-in WiFi. To be done soon.
 - ESP32 built-in WiFi. To be done soon.
 - WiFiNINA using WiFiNINA or WiFiNINA_Generic library.
 - ESP8266-AT, ESP32-AT WiFi shields using WiFiEspAT or ESP8266_AT_WebServer library.
 
Currently Supported Ethernet Modules/Shields

 - W5x00's using Ethernet, EthernetLarge, Ethernet2 or Ethernet3 Library.
 - ENC28J60 using EthernetENC or UIPEthernet library.
 - LAN8742A using STM32Ethernet / STM32 LwIP libraries.
 
Currently Supported storage

- ESP8266 EEPROM, LittleFS, SPIFFS. To be done soon.
- ESP32 EEPROM, SPIFFS. To be done soon.
- SAM DUE DueFlashStorage.
- SAMD FlashStorage.
- nRF52 LittleFS.
- STM32 and AVR EEPROM.

Sample Code

This is the BI_RTC_Alarm_STM32_Ethernet example

Code: Select all

/****************************************************************************************************************************
  STM32 has five clock sources: HSI, HSE, LSI, LSE, PLL.
  
  (1) HSI is a high-speed internal clock, RC oscillator, with a frequency of 8MHz and low accuracy.
  (2) HSE is a high-speed external clock, which can be connected with quartz/ceramic resonator or external clock source. 
      Its frequency range is from 4MHz to 16MHz.
  (3) LSI is a low-speed internal clock, RC oscillator, with a frequency of 40 kHz, providing a low-power clock.  
  (4) LSE is a low-speed external clock connected to 32.768 kHz quartz crystal.
  (5) PLL is the frequency doubling output of PLL, and its clock input source can be HSI/2, HSE or HSE/2. 
      Frequency doubling can be chosen as 2 to 16 times, but the maximum output frequency should not exceed 72MHz.
      
  The system clock SYSCLK can be derived from three clock sources:
  (1) HSI oscillator clock
  (2) HSE oscillator clock
  (3) PLL Clock
  STM32 can choose a clock signal to output to MCO foot (PA8), and can choose 2-frequency, HSI, HSE, or system clock for PLL output.
  Before any peripheral can be used, its corresponding clock must be enabled first.
 *****************************************************************************************************************************/
 
#include "defines.h"

#include <Timezone_Generic.h>             // https://github.com/khoih-prog/Timezone_Generic

#include <STM32RTC.h>

/* Get the rtc object */
STM32RTC& rtc = STM32RTC::getInstance();

//////////////////////////////////////////

// US Eastern Time Zone (New York, Detroit)
TimeChangeRule myDST = {"EDT", Second, Sun, Mar, 2, -240};    //Daylight time = UTC - 4 hours
TimeChangeRule mySTD = {"EST", First, Sun, Nov, 2, -300};     //Standard time = UTC - 5 hours
Timezone myTZ(myDST, mySTD);

// If TimeChangeRules are already stored in EEPROM, comment out the three
// lines above and uncomment the line below.
//Timezone myTZ(100);       //assumes rules stored at EEPROM address 100

TimeChangeRule *tcr;        //pointer to the time change rule, use to get TZ abbrev

//////////////////////////////////////////

char timeServer[]         = "time.nist.gov";  // NTP server
unsigned int localPort    = 2390;             // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;       // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT     = 2000;     // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE];   // buffer to hold incoming and outgoing packets

// A UDP instance to let us send and receive packets over UDP
EthernetUDP Udp;

// send an NTP request to the time server at the given address
void sendNTPpacket(char *ntpSrv)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)

  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
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123

  Udp.write(packetBuffer, NTP_PACKET_SIZE);

  Udp.endPacket();
}

void update_RTC(unsigned long epoch)
{
  // Update RTC
  Serial.println("\nUpdating Time for STM32 RTC");

  // STM32 RTC clock starts from 01/01/2000 if not set. No battery-backed RTC.
  // STM32 RTC specific code

  // Can use either one of these functions
  
  // 1. The best way to set from epoch
  rtc.setEpoch(epoch);
  
  // 2. you can also use the harder way
  // Get the time_t from epoch
  //time_t epoch_t = epoch;
  //rtc.setTime(hour(epoch_t), minute(epoch_t), second(epoch_t));
  //rtc.setDate(weekday(epoch_t), day(epoch_t), month(epoch_t), year(epoch_t));
}

void alarmMatch(void *data)
{
  UNUSED(data);
  Serial.println("*****RTC ALARM ACTIVATED*****");
  Serial.println("*****RTC ALARM ACTIVATED*****");
}

void setRTC_Alarm(unsigned long epochAlarm)
{
  rtc.attachInterrupt(alarmMatch);

  // Alarm at epochAlarm
  rtc.setAlarmEpoch(epochAlarm, rtc.MATCH_DHHMMSS);

  // Display Alarm time from RTC
  Serial.println("=======RTC ALARM SET========");

  // STM32 RTC specific code
  time_t utcAlarm   = epochAlarm;
  time_t localAlarm = myTZ.toLocal(utcAlarm, &tcr);
  //////

  printDateTime(utcAlarm, "UTC");
  printDateTime(localAlarm, tcr -> abbrev);
  Serial.println("============================");
}

void getNTPTime(void)
{
  static bool gotCurrentTime = false;

  // Just get the correct ime once
  if (!gotCurrentTime)
  {
    sendNTPpacket(timeServer); // send an NTP packet to a time server
    // wait to see if a reply is available
    delay(1000);

    if (Udp.parsePacket())
    {
      Serial.println(F("Packet received"));
      // We've received a packet, read the data from it
      Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

      //the timestamp starts at byte 40 of the received packet and is four bytes,
      // or two words, long. First, esxtract the two words:

      unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
      unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
      // combine the four bytes (two words) into a long integer
      // this is NTP time (seconds since Jan 1 1900):
      unsigned long secsSince1900 = highWord << 16 | lowWord;
      Serial.print(F("Seconds since Jan 1 1900 = "));
      Serial.println(secsSince1900);

      // now convert NTP time into everyday time:
      Serial.print(F("Unix time = "));
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;
      // subtract seventy years:
      unsigned long epoch = secsSince1900 - seventyYears;

      // print Unix time:
      Serial.println(epoch);
      
      // Get the time_t from epoch
      time_t epoch_t = epoch;

      // set the system time to UTC
      // warning: assumes that compileTime() returns US EDT
      // adjust the following line accordingly if you're in another time zone
      setTime(epoch_t);

      update_RTC(epoch);

      // Set test alarm after 30s
      setRTC_Alarm(epoch + 30L);
       
      // print the hour, minute and second:
      Serial.print(F("The UTC time is "));       // UTC is the time at Greenwich Meridian (GMT)
      Serial.print((epoch  % 86400L) / 3600); // print the hour (86400 equals secs per day)
      Serial.print(':');

      if (((epoch % 3600) / 60) < 10)
      {
        // In the first 10 minutes of each hour, we'll want a leading '0'
        Serial.print('0');
      }
      Serial.print((epoch  % 3600) / 60); // print the minute (3600 equals secs per minute)
      Serial.print(':');

      if ((epoch % 60) < 10)
      {
        // In the first 10 seconds of each minute, we'll want a leading '0'
        Serial.print('0');
      }
      Serial.println(epoch % 60); // print the second

      gotCurrentTime = true;
    }
    else
    {
      // wait ten seconds before asking for the time again
      delay(10000);
    }
  }
}

//////////////////////////////////////////

// format and print a time_t value, with a time zone appended.
void printDateTime(time_t t, const char *tz)
{
  char buf[32];
  char m[4];    // temporary storage for month string (DateStrings.cpp uses shared buffer)
  strcpy(m, monthShortStr(month(t)));
  sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
          hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tz);
  Serial.println(buf);
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);

  Serial.println("\nStart BI_RTC_Alarm_STM32_Ethernet on " + String(BOARD_NAME) + ", using " + String(SHIELD_TYPE));

  ET_LOGWARN3(F("Board :"), BOARD_NAME, F(", setCsPin:"), USE_THIS_SS_PIN);

  ET_LOGWARN(F("Default SPI pinout:"));
  ET_LOGWARN1(F("MOSI:"), MOSI);
  ET_LOGWARN1(F("MISO:"), MISO);
  ET_LOGWARN1(F("SCK:"),  SCK);
  ET_LOGWARN1(F("SS:"),   SS);
  ET_LOGWARN(F("========================="));

#if !(USE_BUILTIN_ETHERNET || USE_UIP_ETHERNET)
  // For other boards, to change if necessary
  #if ( USE_ETHERNET || USE_ETHERNET_LARGE || USE_ETHERNET2  || USE_ETHERNET_ENC )
    // Must use library patch for Ethernet, Ethernet2, EthernetLarge libraries
    Ethernet.init (USE_THIS_SS_PIN);
  
  #elif USE_ETHERNET3
    // Use  MAX_SOCK_NUM = 4 for 4K, 2 for 8K, 1 for 16K RX/TX buffer
    #ifndef ETHERNET3_MAX_SOCK_NUM
      #define ETHERNET3_MAX_SOCK_NUM      4
    #endif
  
    Ethernet.setCsPin (USE_THIS_SS_PIN);
    Ethernet.init (ETHERNET3_MAX_SOCK_NUM);
  
  #elif USE_CUSTOM_ETHERNET
    // You have to add initialization for your Custom Ethernet here
    // This is just an example to setCSPin to USE_THIS_SS_PIN, and can be not correct and enough
    //Ethernet.init(USE_THIS_SS_PIN);
  
  #endif  //( ( USE_ETHERNET || USE_ETHERNET_LARGE || USE_ETHERNET2  || USE_ETHERNET_ENC )
#endif

  // start the ethernet connection and the server:
  // Use DHCP dynamic IP and random mac
  uint16_t index = millis() % NUMBER_OF_MAC;
  // Use Static IP
  //Ethernet.begin(mac[index], ip);
  Ethernet.begin(mac[index]);

  // you're connected now, so print out the data
  Serial.print(F("You're connected to the network, IP = "));
  Serial.println(Ethernet.localIP());

  Udp.begin(localPort);

  // STM32 RTC specific code
  // Select RTC clock source: LSI_CLOCK, LSE_CLOCK or HSE_CLOCK.
  // By default the LSI is selected as source.
  //rtc.setClockSource(STM32RTC::LSE_CLOCK);
  rtc.begin(); // initialize RTC 24H format
  //////
}

void loop()
{
  // Get time from NTP once, then update RTC
  // You certainly can make NTP check every hour/day to update RTC ti have better accuracy
  getNTPTime();

  // Display time from RTC
  Serial.println("============================");

  // STM32 RTC specific code
  time_t utc = rtc.getEpoch();
  time_t local = myTZ.toLocal(utc, &tcr);
  //////
  
  printDateTime(utc, "UTC");
  printDateTime(local, tcr -> abbrev);
  
  delay(10000);
}

Debug Termimal Output Samples

1. This is terminal debug output when running BI_RTC_STM32_Ethernet example on STM32F7 Nucleo-144 NUCLEO_F767ZI with LAN8742A using STM32Ethernet Library to demonstrate the usage of STM32 built-in RTC

Code: Select all

Start BI_RTC_STM32_Ethernet on NUCLEO_F767ZI, using LAN8742A Ethernet & STM32Ethernet Library
[ETHERNET_WEBSERVER] Board : NUCLEO_F767ZI , setCsPin: 10
[ETHERNET_WEBSERVER] Default SPI pinout:
[ETHERNET_WEBSERVER] MOSI: 11
[ETHERNET_WEBSERVER] MISO: 12
[ETHERNET_WEBSERVER] SCK: 13
[ETHERNET_WEBSERVER] SS: 10
[ETHERNET_WEBSERVER] =========================
You're connected to the network, IP = 192.168.2.97
Packet received
Seconds since Jan 1 1900 = 3812898366
Unix time = 1603909566

Updating Time for STM32 RTC
The UTC time is 18:26:06
============================
18:26:06 Wed 28 Oct 2020 UTC
14:26:06 Wed 28 Oct 2020 EDT
============================
18:26:15 Wed 28 Oct 2020 UTC
14:26:15 Wed 28 Oct 2020 EDT
============================
18:26:24 Wed 28 Oct 2020 UTC
14:26:24 Wed 28 Oct 2020 EDT
============================
18:26:33 Wed 28 Oct 2020 UTC
14:26:33 Wed 28 Oct 2020 EDT
============================
18:26:43 Wed 28 Oct 2020 UTC
14:26:43 Wed 28 Oct 2020 EDT
2. This is terminal debug output when running BI_RTC_Alarm_STM32_Ethernet example on STM32F7 Nucleo-144 NUCLEO_F767ZI with LAN8742A using STM32Ethernet Library to demonstrate the usage of STM32 built-in RTC Alarm function

Code: Select all

Start BI_RTC_Alarm_STM32_Ethernet on NUCLEO_F767ZI, using LAN8742A Ethernet & STM32Ethernet Library
[ETHERNET_WEBSERVER] Board : NUCLEO_F767ZI , setCsPin: 10
[ETHERNET_WEBSERVER] Default SPI pinout:
[ETHERNET_WEBSERVER] MOSI: 11
[ETHERNET_WEBSERVER] MISO: 12
[ETHERNET_WEBSERVER] SCK: 13
[ETHERNET_WEBSERVER] SS: 10
[ETHERNET_WEBSERVER] =========================
You're connected to the network, IP = 192.168.2.96
Packet received
Seconds since Jan 1 1900 = 3812900198
Unix time = 1603911398

Updating Time for STM32 RTC
=======RTC ALARM SET========
18:57:08 Wed 28 Oct 2020 UTC
14:57:08 Wed 28 Oct 2020 EDT
============================
The UTC time is 18:56:38
============================
18:56:38 Wed 28 Oct 2020 UTC
14:56:38 Wed 28 Oct 2020 EDT
============================
18:56:47 Wed 28 Oct 2020 UTC
14:56:47 Wed 28 Oct 2020 EDT
============================
18:56:56 Wed 28 Oct 2020 UTC
14:56:56 Wed 28 Oct 2020 EDT
============================
18:57:05 Wed 28 Oct 2020 UTC
14:57:05 Wed 28 Oct 2020 EDT
*****RTC ALARM ACTIVATED*****
*****RTC ALARM ACTIVATED*****
============================
18:57:15 Wed 28 Oct 2020 UTC
14:57:15 Wed 28 Oct 2020 EDT
zoomx
Posts: 28
Joined: Fri Dec 20, 2019 10:12 am
Location: Near Mt.Etna

Re: Timezone_Generic Library to convert UTC to local time

Post by zoomx »

+1!!!
khoih-prog
Posts: 102
Joined: Thu Feb 27, 2020 7:54 am
Location: Toronto

Re: Timezone_Generic Library to convert UTC to local time

Post by khoih-prog »

Thanks @zoomx

Releases v1.2.6

1. Allow un-initialized TZ then use begin() method to set the actual TZ. Credit of 6v6gt, see Timezone_Generic Library to convert UTC to local time
2. Modify examples to use new un-initialized-TZ feature.

You'll see in example this code snippet

Code: Select all

#define USING_INITIALIZED_TZ      false   //true

#if USING_INITIALIZED_TZ
  // US Eastern Time Zone (New York, Detroit,Toronto)
  TimeChangeRule myDST = {"EDT", Second, Sun, Mar, 2, -240};    // Daylight time = UTC - 4 hours
  TimeChangeRule mySTD = {"EST", First,  Sun, Nov, 2, -300};    // Standard time = UTC - 5 hours
  Timezone myTZ(myDST, mySTD);
#else
  // Allow a "blank" TZ object then use begin() method to set the actual TZ.
  // Feature added by 6v6gt (https://forum.arduino.cc/index.php?topic=711259)
  Timezone myTZ ;
  TimeChangeRule myDST;
  TimeChangeRule mySTD;
#endif

...

void setup()
{
...

#if !(USING_INITIALIZED_TZ)

  // Can read this info from EEPROM, storage, etc
  String tzName = "EDT/EST" ;

  // Time zone rules can be set as below or dynamically built, say through a configuration
  //  interface, or fetched from eeprom, flash etc.

  if ( tzName == "EDT/EST" )
  {
    // America Eastern Time
    myDST = (TimeChangeRule) {"EDT",  Second, Sun, Mar, 2, -240};    // Daylight time = UTC - 4 hours
    mySTD = (TimeChangeRule) {"EST",  First,  Sun, Nov, 2, -300};     // Standard time = UTC - 5 hours
  }
  else if ( tzName == "CET/CEST" ) 
  {
    // central Europe
    myDST = (TimeChangeRule) {"CEST", Last, Sun, Mar, 2, 120};
    mySTD = (TimeChangeRule) {"CET",  Last, Sun, Oct, 3, 60};
  }
  
  else if ( tzName == "GMT/BST" ) 
  {
    // UK
    myDST = (TimeChangeRule) {"BST",  Last, Sun, Mar, 1, 60};
    mySTD = (TimeChangeRule) {"GMT",  Last, Sun, Oct, 2, 0};
  }

  myTZ.init( myDST, mySTD ) ;
  
#endif

  Udp.begin(localPort);
}
Post Reply

Return to “Libraries & Hardware”