loading Timezone objects from flash [solved]

I used Jack Christensen’s Timezone library to place multiple timezones in EEPROM. It worked but I want to include a bigger database of zones than will fit there and leave room for other configuration variables. So I want to put a table of time zones in flash. In a sense, I think the problem boils down to how to store objects in flash, but I’m not having any luck. I have looked at the avr-libc docs and can’t figure out how the functions there can be used for this. The Arduino docs for PROGMEM don’t go beyond string storage. I started putting together a sketch to fetch objects from a table, but that was a failure so I stripped it down to the problem of getting just one time zone object from flash. Still I am having no luck. Here is the test sketch:

#include <Timezone.h>    // https://github.com/JChristensen/Timezone

//Japan Standard Time Zone (Tokyo)
// defined in RAM
TimeChangeRule jCST = {"JST", Second, Sun, Mar, 2, 540};  //Japan Standard Time = UTC + 9 hours
Timezone jST(jCST, jCST);

//British Time Zone (London)
// defined in flash
const TimeChangeRule ukDT PROGMEM = {"BDT", Last, dowSunday, Mar, 2, 60};
const TimeChangeRule ukST PROGMEM = {"GMT", Last, dowSunday, Oct, 2, 0};
const Timezone PROGMEM ukT(ukDT, ukST);

TimeChangeRule *tcr;        //time change rule pointer, use to get abbreviations

//active zone in RAM
TimeChangeRule DT;
TimeChangeRule ST;
Timezone localzone(DT, ST);

void setup(void)
{
  Serial.begin(9600);

  // this part prints "JST" as it should
  jST.toLocal(now(), &tcr);
  Serial.print(F("fetched Japan time zone from RAM: "));
  Serial.println(tcr->abbrev);

  // this part prints junk characters:
  // move one zone object from flash to RAM
  void *table = pgm_read_ptr(&ukT);
  memcpy_P(&localzone, table, sizeof(Timezone));

  localzone.toLocal(now(), &tcr);
  Serial.print(F("fetched active zone from flash memory: "));
  Serial.println(tcr->abbrev);
}

void loop(void) {}

The output:

fetched Japan time zone from RAM: JST
fetched active zone from flash memory: ⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮

This is really just step one, as I need to create a table that looks like this:

const Timezone* const zoneTable[] PROGMEM = {&jST, &ukT, &ceT};

that I can index.

Actually, the final goal is to create a struct that includes a text field to describe each zone. But right now, I can’t even get off the ground.

Here’s the class definition, for good measure:

// structure to describe rules for when daylight/summer time begins,
// or when standard time begins.
struct TimeChangeRule
{
    char abbrev[6];    // five chars max
    uint8_t week;      // First, Second, Third, Fourth, or Last week of the month
    uint8_t dow;       // day of week, 1=Sun, 2=Mon, ... 7=Sat
    uint8_t month;     // 1=Jan, 2=Feb, ... 12=Dec
    uint8_t hour;      // 0-23
    int offset;        // offset from UTC in minutes
};
        
class Timezone
{
    public:
        Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart);
        Timezone(int address);
        time_t toLocal(time_t utc);
        time_t toLocal(time_t utc, TimeChangeRule **tcr);
        time_t toUTC(time_t local);
        bool utcIsDST(time_t utc);
        bool locIsDST(time_t local);
        void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart);
        void readRules(int address);
        void writeRules(int address);

    private:
        void calcTimeChanges(int yr);
        time_t toTime_t(TimeChangeRule r, int yr);
        TimeChangeRule m_dst;   // rule for start of dst or summer time for any year
        TimeChangeRule m_std;   // rule for start of standard time for any year
        time_t m_dstUTC;        // dst start for given/current year, given in UTC
        time_t m_stdUTC;        // std time start for given/current year, given in UTC
        time_t m_dstLoc;        // dst start for given/current year, given in local time
        time_t m_stdLoc;        // std time start for given/current year, given in local time
};

I use a lightly hacked version of Jack Christensen’s Timezone library.
I made a slight modification to it so the user can dynamically create his time zone.
In fact I give him a selection of 5 pre defined times zones and one custome time
zone where he can enter the rules in exactly the same format as the library takes.
The dynamic creation should give you an example of how you could represent all
the time zones in a simple tabular text format, which could be easier that the
approach you are taking.

The hack I made to the library was to include an init() method which you could
use to set the timezone dynamically instead of doing in the constructor.

See the custom rule and the init method call:

// global object 
Timezone myTZ;
TimeChangeRule myDST ;
TimeChangeRule mySTD ;



void tzUpdate() {

  //   setup TZ - should be moved to settings.h ??
  // to update timezones:
  //    - create a pair myDST / mySTD below
  //    - ensure name eg "CET/CEST" is unique (beware of special characters)
  //    - 5 char may for sub name.
  //    - update select statement "tzNameSelect" on mainPage.h
  //    - if necessary, update configHtml.h

  String tzName = String( config.persistent.tzName );
  tzName.trim() ;
  Serial.print( F("tzName: " )) ;
  Serial.println ( tzName ) ;

  // central Europe
  if ( tzName == "CET/CEST" ) {
    myDST = (TimeChangeRule) {
      "CEST", Last, Sun, Mar, 2, 120
    };
    mySTD = (TimeChangeRule) {
      "CET", Last, Sun, Oct, 3, 60
    };
  }
  // UK
  else if ( tzName == "GMT/BST" ) {
    myDST = (TimeChangeRule) {
      "BST", Last, Sun, Mar, 1, 60
    };
    mySTD = (TimeChangeRule) {
      "GMT", Last, Sun, Oct, 2, 0
    };
  }
  //US Eastern Time Zone (New York, Detroit)
  else if ( tzName == "usEST/EDT" ) {
    myDST = (TimeChangeRule) {
      "EDT", Second, Sun, Mar, 2, -240
    };
    mySTD = (TimeChangeRule) {
      "EST", First, Sun, Nov, 2, -300
    };
  }
  //US Central Time Zone (Chicago, Houston)
  else if ( tzName == "usCST/CDT" ) {
    myDST = (TimeChangeRule) {
      "CDT", Second, dowSunday, Mar, 2, -300      // dowSunday ?
    };
    mySTD = (TimeChangeRule) {
      "CST", First, dowSunday, Nov, 2, -360
    };
  }
  else if ( tzName == "Custom" ) {
    // values obtained from user config /eeprom
    myDST = (TimeChangeRule) {
      "cuDST", config.persistent.custTzDstWeek, config.persistent.custTzDstDow,
      config.persistent.custTzDstMonth, config.persistent.custTzDstHour, config.persistent.custTzDstOffset
    };
    mySTD = (TimeChangeRule) {
      "cuSTD", config.persistent.custTzStdWeek, config.persistent.custTzStdDow,
      config.persistent.custTzStdMonth, config.persistent.custTzStdHour, config.persistent.custTzStdOffset
    };
  }
  // force to UTC
  else {
    // there could be a better way
    myDST = (TimeChangeRule) {
      "UTCa", Last, Sun, Mar, 3, 0
    };
    mySTD = (TimeChangeRule) {
      "UTCb", Last, Sun, Oct, 3, 0
    };
  }
  myTZ.init(myDST, mySTD) ;   // fdm custom method init()
}

I’ve attached the modified library. It is sometime ago that I made these changes but if you are interested in following this method, I could study the matter a bit more and maybe give a better explanation.

Timezone.h (2.76 KB)

Timezone.ino (9.08 KB)

You're calling the Timezone constructor implicitly when you declare ukT, but populating flash is a compile time operation. Consequently, I can't figure out any way to put an object into flash. Struct is fine as your example illustrates and you could declare an array of them holding your TimezoneRules to allow you to build appropriate single Timezone objects at runtime.

6v6gt:
I use a lightly hacked version of Jack Christensen's Timezone library.
I made a slight modification to it so the user can dynamically create his time zone.
In fact I give him a selection of 5 pre defined times zones and one custome time
zone where he can enter the rules in exactly the same format as the library takes.
The dynamic creation should give you an example of how you could represent all
the time zones in a simple tabular text format, which could be easier that the
approach you are taking.

Thanks for the detail. Well, that does lead me towards some kind of dynamic creation, because I realized that it would be a good fallback to allow the user to define their own zone(s) in addition to the flash database. The current definitions are likely to change at least in some regions, before too long.

wildbill:
You're calling the Timezone constructor implicitly when you declare ukT, but populating flash is a compile time operation. Consequently, I can't figure out any way to put an object into flash. Struct is fine as your example illustrates and you could declare an array of them holding your TimezoneRules to allow you to build appropriate single Timezone objects at runtime.

Rats, I see what you mean. However, I think that means I could create an array of structs containing the two TimezoneRules and my text description, and put that in flash. Then create the object from it. I assume that is the direction you're suggesting.

Strange, now it finds and prints the description string “British” but still fails to create the object properly:

fetched a time zone from RAM: JST
fetched "Britain" zone from flash memory:
#include <Timezone.h>    // https://github.com/JChristensen/Timezone

struct TimezoneFlashRecord
{
  char description[32];
  TimeChangeRule DT;
  TimeChangeRule ST;
};

//Japan Standard Time Zone (Tokyo)
// defined in RAM
TimeChangeRule jCST = {"JST", Second, Sun, Mar, 2, 540};  //Japan Standard Time = UTC + 9 hours
Timezone jST(jCST, jCST);

//British Time Zone (London)
// defined in flash
const TimeChangeRule ukDT = {"BDT", Last, dowSunday, Mar, 2, 60};
const TimeChangeRule ukST = {"GMT", Last, dowSunday, Oct, 2, 0};
const TimezoneFlashRecord PROGMEM british = {"Britain", ukDT, ukST};

TimeChangeRule *tcr;        //time change rule pointer, use to get abbreviations


void setup(void)
{
  Serial.begin(9600);

  // this part prints "JST" as it should
  jST.toLocal(now(), &tcr);
  Serial.print(F("fetched a time zone from RAM: "));
  Serial.println(tcr->abbrev);

  // this part prints junk characters:
  // move one zone object from flash to RAM
  // active zone in RAM

  // fetch from flash to buffer
  TimezoneFlashRecord buf;
  memcpy_P(&buf, &british, sizeof(TimezoneFlashRecord));

  // create object
  Timezone localzone(buf.DT, buf.ST);

  localzone.toLocal(now(), &tcr);
  Serial.print(F("fetched \""));
  Serial.print(buf.description);
  Serial.print(F("\" zone from flash memory: "));
  Serial.println(tcr->abbrev);
}

void loop(void) {}

Okay, so what fixed it was changing

const TimeChangeRule ukDT = {"BDT", Last, dowSunday, Mar, 2, 60};
const TimeChangeRule ukST = {"GMT", Last, dowSunday, Oct, 2, 0};
const TimezoneFlashRecord PROGMEM british = {"Britain", ukDT, ukST};

to

const TimezoneFlashRecord PROGMEM british = {"Britain", {"BDT", Last, dowSunday, Mar, 2, 60}, {"GMT", Last, dowSunday, Oct, 2, 0}};

I think you can also get it to work with taking the address of the TimeChangeRules, but then you’ve got additional memcpy_P calls to get at them.

const TimeChangeRule ukDT = {"BDT", Last, dowSunday, Mar, 2, 60};
const TimeChangeRule ukST = {"GMT", Last, dowSunday, Oct, 2, 0};
const TimezoneFlashRecord PROGMEM british = {"Britain", &ukDT, &ukST};