ESP32 Time library, formatting date and writing it to char array

Hello,

one thing I am working on right now as part of my still ongoing Arduino trip computer project is converting a given Unix timestamp on the ESP32 into a specific date and time.

I would like to translate a Unix timestamp into a char array like so, for example:

Apr 09 - 21:22

I've read up on how to retrieve day, hour and minutes from a timestamp using day(), month(), hour() and minute(), but I'm not quite sure yet what's the best way to turn a numeric month into a three-letter month. Does the time library have any functions for that?

Here's my code so far, at the moment the time is printed out via serial, when I'm done with it, as I said, I would like to have it all in a char array because I will need that char array to print actual text on my TFT display using my bespoke font libraries.

#include <Time.h>
#include <TimeLib.h>

unsigned long lastTime = millis();
unsigned long timePassed;
byte i = 0;

//Creating union
union unixTime {

  long unixTimeValue;
  byte unixByteNr[4];
} unixBytes;


void setup() {

  Serial.begin(115200);
  Serial.println("Ready. Requesting UNIX timestamp.");
}

void loop() {


  //Reading UNIX time input from serial
  while (Serial.available()) {

    //Populating array with received serial bytes
    unixBytes.unixByteNr[i] = Serial.read();
    i++;
  

  if (i == 3) {

    unsigned long unixTimestamp = unixBytes.unixTimeValue;

    //Synchronizing system time with the received timestamp
    setSyncProvider(unixTimestamp);

    i = 0;
  }
}

if ((timePassed = millis() - lastTime) >= 1000) {

  Serial.print("Time: ");
  Serial.print(year());
  Serial.print("-");
  Serial.print(month());
  Serial.print("-");
  Serial.print(day());
  Serial.print(" -- ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.println(second());

  lastTime = millis();
}
}

Right now, the code doesn't fully compile, I'm not quite sure at first glance what I have done to it that it doesn't, because it was working fine before I made the last changes and then unsuccessfully tried to revert to a previous stable version.

So what is the best way to go from months expressed in numbers from 1 to 12 to the months as three letter words?

So what is the best way to go from months expressed in numbers from 1 to 12 to the months as three letter words?

Subtract one from the month number and use a string array to look-up the month name.

Here is how I would do it:

char buf[25]; // but you probably really only need 15

uint8_t mo =  4;  // month (1 to 12)
uint8_t dd = 14;  // day of month
uint8_t hh = 17;  // hour
uint8_t mi = 45;  // minute

// This next statement does the work:
sprintf(buf, "%c%c%c %02d - %02d:%02d",
 ("BJFMAMJJASOND"[(mo<=12)?mo:0]),
 ("aaeapauuuecoe"[(mo<=12)?mo:0]),
 ("dnbrrynlgptvc"[(mo<=12)?mo:0]),
 dd, hh, mi);
// Actually, I'm not sure how to "correctly"
// split one statement among multiple lines.
// I hope I did that right.
// Even if I didn't, the compiler won't care.
 
Serial.println(buf); // to see what you made

... and now you see how my sense of humor manifests itself in code.

@odometer:

Somehow, my ESP32 doesn't like your code.

Here's how my code looks now:

#include <Time.h>
#include <TimeLib.h>

unsigned long lastTime = millis();
unsigned long timePassed;
byte i = 0;

//Creating union
union unixTime {

  long unixTimeValue;
  byte unixByteNr[4];
} unixBytes;


void setup() {

  Serial.begin(115200);
  Serial.println("Ready. Requesting UNIX timestamp.");
}

void loop() {


  //Reading UNIX time input from serial
  while (Serial.available()) {

    //Populating array with received serial bytes
    unixBytes.unixByteNr[i] = Serial.read();
    i++;


    if (i == 3) {

      long unixTimestamp = unixBytes.unixTimeValue;

      //Synchronizing system time with the received timestamp
      setTime(unixTimestamp);

      i = 0;
    }
  }

  if ((timePassed = millis() - lastTime) >= 1000) {

    char buf[12];

    uint8_t mo = month();  // month (1 to 12)
    uint8_t dd = day();  // day of month
    uint8_t hh = hour();  // hour
    uint8_t mi = minute();  // minute

    sprintf(buf, "%c%c%c %02d - %02d:%02d",
            ("BJFMAMJJASOND"[(mo <= 12) ? mo : 0]),
            ("aaeapauuuecoe"[(mo <= 12) ? mo : 0]),
            ("dnbrrynlgptvc"[(mo <= 12) ? mo : 0]),
            dd, hh, mi);


    Serial.println(buf);

    lastTime = millis();
  }
 
}

It compiles, but then when I go to the cereal monitor, this is what I get:

Ready. Requesting UNIX timestamp.
Jan 01 - 00:00

Stack smashing protect failure!

abort() was called at PC 0x400d3c7c on core 1

Backtrace: 0x400874f8:0x3ffca430 0x400875f7:0x3ffca450 0x400d3c7c:0x3ffca470 0x400d0a35:0x3ffca490 0x400e3849:0x3ffca4e0

Rebooting...

I've tried commenting out the section that you wrote and it then runs without a problem, so there must be something about the sprintf function that it doesn't like.

Also, if I enter a near enough time stamp for today, e.g. 1523794541 , it comes back one time before rebooting with the date Feb 09 - 14:24. So there's something off there as well... :frowning:

How many characters in your date string?
How many characters in the buffer you try to put it into?

Not sure if your Time.h is the same as the ESP core time.h but if it is then the below excerpt works for me.

#include "time.h"

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 3600;

/*
  %a Abbreviated weekday name 
  %A Full weekday name 
  %b Abbreviated month name 
  %B Full month name 
  %c Date and time representation for your locale 
  %d Day of month as a decimal number (01-31) 
  %H Hour in 24-hour format (00-23) 
  %I Hour in 12-hour format (01-12) 
  %j Day of year as decimal number (001-366) 
  %m Month as decimal number (01-12) 
  %M Minute as decimal number (00-59) 
  %p Current locale’s A.M./P.M. indicator for 12-hour clock 
  %S Second as decimal number (00-59) 
  %U Week of year as decimal number,  Sunday as first day of week (00-51) 
  %w Weekday as decimal number (0-6; Sunday is 0) 
  %W Week of year as decimal number, Monday as first day of week (00-51) 
  %x Date representation for current locale 
  %X Time representation for current locale 
  %y Year without century, as decimal number (00-99) 
  %Y Year with century, as decimal number 
  %z %Z Time-zone name or abbreviation, (no characters if time zone is unknown) 
  %% Percent sign 
  You can include text literals (such as spaces and colons) to make a neater display or for padding between adjoining columns. 
  You can suppress the display of leading zeroes  by using the "#" character  (%#d, %#H, %#I, %#j, %#m, %#M, %#S, %#U, %#w, %#W, %#y, %#Y) 
*/

void printLocalTime()
{
  struct tm timeinfo;
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

  if(!getLocalTime(&timeinfo))
  {
    Serial.println(F(" Failed to obtain time"));
    return;
  }
  Serial.println(&timeinfo, " %d %B %Y %H:%M:%S ");
}

carguy:
@odometer:

Somehow, my ESP32 doesn't like your code.

Here's how my code looks now:

    char buf[12];

NO!
The time string will not fit in twelve bytes!

Like I said:

odometer:

char buf[25]; // but you probably really only need 15

Characters: Apr 15 - 11:57* (the * is where the null terminator should go)

   Indices: 012345678901234

So, you need 15 bytes: 14 for the string proper, and 1 for its null terminator.
I decided to use 25 bytes, just to be safe. But go ahead and use 15 and it should still work.

What is a null terminator?

It's something you need to allow room for.

If you don't allow room for all the characters in your string plus the null terminator at the end, then Bad Things Happen.

ok it's working now. A buffer size of 15 produces correct results.

One minor problem I still have is that I can't enter a Unix timestamp correctly via Serial. When I do, I get back erratic dates that can be anything from Feb 11 to Jan 25.

Here's my code again:

#include <Time.h>
#include <TimeLib.h>

unsigned long lastTime = millis();
unsigned long timePassed;
byte i = 0;

//Creating union
union unixTime {

  unsigned long unixTimeValue;
  byte unixByteNr[4];
} unixBytes;

void setup() {

  Serial.begin(115200);
  Serial.println("Ready. Requesting UNIX timestamp.");
}

void loop() {

  //Reading UNIX time input from serial
  while (Serial.available()) {

    //Filling array with received serial bytes
    unixBytes.unixByteNr[i] = Serial.read();
    i++;

    if (i == 3) {

      unsigned long unixTimestamp = unixBytes.unixTimeValue;

      //Synchronizing system time with the received timestamp
      setTime(unixTimestamp);
      i = 0;
    }
  }
  if ((timePassed = millis() - lastTime) >= 1000) {

    char buf[15];
 
        uint8_t mo = month();  // month (1 to 12)
        uint8_t dd = day();  // day of month
        uint8_t hh = hour();  // hour
        uint8_t mi = minute();  // minute

    sprintf(buf, "%c%c%c %02d - %02d:%02d",
            ("BJFMAMJJASOND"[(mo <= 12) ? mo : 0]),
            ("aaeapauuuecoe"[(mo <= 12) ? mo : 0]),
            ("dnbrrynlgptvc"[(mo <= 12) ? mo : 0]),
            dd, hh, mi);

    Serial.println(buf);
    lastTime = millis();
  }
}

I guess I must have gotten something wrong with the way the received bytes are put together to form a timestamp, but I'm not sure where the problem is.

This is just a test sketch; on the finished trip computer, date and time will be set using a settings menu and buttons, not the Serial monitor. But it'd be nice to be able to manipulate the time and date via Serial while the trip computer is still in development.

EDIT:

I've decided to use a lookup table after all, because the trip computer will come with language settings and it's just more convenient that way.

Right now, my code doesn't compile yet.

Here's my char array for the months in different languages:

const char* months[][12] PROGMEM = {

  // English
  {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},

  // German
  {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"},

  // Spanish
  {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"},

  //  French
  {"jan", "fév", "mar", "avr", "mai", "jui", "jul", "aou", "sep", "oct", "nov", "déc"},

};

And this is how I am trying to generate a date and time at the moment:

char composeDateChar(){

char dateBuf[15];

byte dateDay = day();
byte dateMonth = month();
byte dateHour = hour();
byte dateMinute = minute();

// Let's stick with English for now
char 3cMonth = pgm_read_byte_near(months[0][&dateMonth-1]);
sprintf(dateBuf, "%d %s %d:%d", dateDay, 3cMonth, dateHour, dateMinute);

return dateBuf;
  
}

This function does not work; these are the errors I get:

D:\Arduino\HeadUnitESP32\HeadUnitESP32.ino: In function 'char composeDateChar()':

HeadUnitESP32:38: error: expected unqualified-id before numeric constant

 char 3cMonth = pgm_read_byte_near(months[0][&dateMonth-1]);

      ^

HeadUnitESP32:39: error: unable to find numeric literal operator 'operator""cMonth'

 sprintf(dateBuf, "%d %s %d:%d", dateDay, 3cMonth, dateHour, dateMinute);

                                          ^

HeadUnitESP32:41: error: invalid conversion from 'char*' to 'char' [-fpermissive]

 return dateBuf;

        ^

How do I fix this?

sprintf(dateBuf, "%d %s %d:%d", dateDay, 3cMonth, dateHour, dateMinute);

3cMonth is not a legal variable name.

PaulS:
3cMonth is not a legal variable name.

... because a variable name cannot begin with a digit.

carguy:
ok it's working now. A buffer size of 15 produces correct results.

Good.
I'm just wondering: why did you try 12 ? Did you assume that the proper buffer size was somehow related to the number of months in a year? (It isn't.)

One minor problem I still have is that I can't enter a Unix timestamp correctly via Serial. When I do, I get back erratic dates that can be anything from Feb 11 to Jan 25.

Are you typing a 10-digit number directly into the Serial monitor and expecting it to work that way? Because the way you have it, that can't possibly work.

By the way, do your dates have years associated with them? What about leap years?

odometer:
I'm just wondering: why did you try 12 ? Did you assume that the proper buffer size was somehow related to the number of months in a year? (It isn't.)

No, I kind of miscounted in my head the number of characters that I would need to display the date and time.

odometer:
By the way, do your dates have years associated with them? What about leap years?

In normal operation mode, the trip computer is supposed to display the date as "Apr 17 13:43". But there will also be an "off screen" that will be active when the ignition is off or when the user chooses to toggle it to that screen.

Like so... off screen on the left, normal operation screen on the right:

To save car battery capacity, the trip computer will then probably go into deep sleep after five or ten minutes, meaning the screen will be turned off and the ESP32 will only keep counting up UNIX time internally.

I've changed things around a bit in my code, but it still won't compile...

const char* months[][12] PROGMEM = {
 
    // English
    {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},

    // German
    {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"},

    // Spanish
    {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"},

    //  French
    {"jan", "fév", "mar", "avr", "mai", "jui", "jul", "aou", "sep", "oct", "nov", "déc"},
};

char* composeDateChar() {

  char dateBuf[15];
  char month3c;

  byte dateDay = day();
  byte dateMonth = month();
  byte dateHour = hour();
  byte dateMinute = minute();

  // Let's stick with English for now
  month3c = pgm_read_byte_near(months[0][&dateMonth - 1]);
  sprintf(dateBuf, "%d %s %d:%d", dateDay, month3c, dateHour, dateMinute);

  return dateBuf;

}

These are the errors I get:

In file included from C:\Users\Me\Documents\Arduino\hardware\espressif\esp32\cores\esp32/WString.h:29:0,

                 from C:\Users\Me\Documents\Arduino\hardware\espressif\esp32\cores\esp32/Arduino.h:150,

                 from sketch\HeadUnitESP32.ino.cpp:1:

D:\Arduino\HeadUnitESP32\HeadUnitESP32.ino: In function 'char* composeDateChar()':

HeadUnitESP32:40: error: invalid types 'const char* [12][byte* {aka unsigned char*}]' for array subscript

   month3c = pgm_read_byte_near(months[0][&dateMonth - 1]);

                                                        ^

C:\Users\Me\Documents\Arduino\hardware\espressif\esp32\cores\esp32/pgmspace.h:39:57: note: in definition of macro 'pgm_read_byte'

 #define pgm_read_byte(addr)   (*(const unsigned char *)(addr))

                                                         ^

D:\Arduino\HeadUnitESP32.ino:40:13: note: in expansion of macro 'pgm_read_byte_near'

   month3c = pgm_read_byte_near(months[0][&dateMonth - 1]);

             ^

exit status 1
invalid types 'const char* [12][byte* {aka unsigned char*}]' for array subscript

EDIT:

I've changed it yet again. It compiles now, but the ESP32 crashes and reboots upon execution.

Here's the latest version of my composeDateChar function:

const char* months[][12] PROGMEM = {
 
    // English
    {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},

    // German
    {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"},

    // Spanish
    {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"},

    //  French
    {"jan", "fév", "mar", "avr", "mai", "jui", "jul", "aou", "sep", "oct", "nov", "déc"},
};

char* composeDateChar() {

  char dateBuf[15];
  char month3c[3];

  byte dateDay = day();
  byte dateMonth = month();
  byte dateHour = hour();
  byte dateMinute = minute();

  // Let's stick with English for now
  sprintf(month3c, "%3c", pgm_read_word(&months[0][dateMonth - 1]));
  sprintf(dateBuf, "%2d %3c %2d:%2d", dateDay, month3c, dateHour, dateMinute);

  return dateBuf;

}

When I run this code as part of my sketch, the ESP32 crashes and reboots:

Guru Meditation Error: Core  1 panic'ed (LoadProhibited)
. Exception was unhandled.
Register dump:
PC      : 0x400014fd  PS      : 0x00060530  A0      : 0x800d09f2  A1      : 0x3ffca5e0  
A2      : 0x00000000  A3      : 0xfffffffc  A4      : 0x000000ff  A5      : 0x0000ff00  
A6      : 0x00ff0000  A7      : 0xff000000  A8      : 0x00000000  A9      : 0x3ffca230  
A10     : 0x0000000c  A11     : 0xffffffff  A12     : 0x00000000  A13     : 0x3f400f06  
A14     : 0x00000002  A15     : 0x00000004  SAR     : 0x00000004  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000000  LBEG    : 0x400014fd  LEND    : 0x4000150d  LCOUNT  : 0xffffffff  

Backtrace: 0x400014fd:0x3ffca5e0 0x400d09ef:0x3ffca5f0 0x400d0cd0:0x3ffca640 0x400e71e5:0x3ffca670
sprintf(month3c, "%3c", pgm_read_word(&months[0][dateMonth - 1]));

There's your problem.

'c' format means "char", so how can you have a three character wide character?
A word would also imply just two chars.

So then how do I read a three-character element from my const char* months[][12] array from PROGMEM?

You can forget the PROGMEM gymnastics, it is not necessary on ESP32.

  return dateBuf;

What is the pointer that the function returns going to point to when datebuf goes out of scope?

ok so I've tried putting the char array in SRAM, here's my code:

const char*  lang_months[][12] = {

  // English
  {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},

  // German
  {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"},

  // Spanish
  {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"},

  //  French
  {"jan", "fév", "mar", "avr", "mai", "jui", "jul", "aou", "sep", "oct", "nov", "déc"},
};


char* composeDateChar() {

  char dateBuf[30];

  byte dateDay = day();
  byte dateMonth = month();
  byte dateHour = hour();
  byte dateMinute = minute();

  // Let's stick with English for now
  sprintf(dateBuf, "%2d %3c %2d:%2d", dateDay, lang_months[0][dateMonth - 1], dateHour, dateMinute);

  return dateBuf;
}

I see the improvement over the way it would have to be done in PROGMEM (I've read up on that in the mean time), but when I run this code within my Arduino sketch, the ESP32 still crashes and reboots over and over the same way it did before.