Best Way To Fire Events from Static Time Schedule (8 different times / day)?

I am building a shift bell controller for our facility that rings a bell for 3 seconds at eight different times throughout the day. This schedule is fixed (don't need ability to modify or edit) and runs the same each day of the week, so all I need to do is identify the hour and minute to energize a relay for the bell.

This code uses NTP time synchronization and automatically calculates local time and accounts for DST (based on some NTP examples I found). The one-second loop and time schedule logic is what I've come up with so far. It will become apparent that C++ is not my preferred coding language lol (but I'm working on that).

The two primary elements in question here are is the shift_times string array and the use of std::find to compare the current time (formatted into a working string) against the shift_times string array - if the current time matches one of the defined times, the relay is energized and I set a flag (Relay) to monitor that. Because each trigger only needs to ring the bell for 3 seconds, I don't check the hour and only see if the seconds > 2 if the Relay flag is set to de-energize the relay output.

This is the closest thing I could come up with in C++ to match the logic flow in my head - I'm sure there are better ways to accomplish this task. Also, I understand that strings can be quirky in C so they need to be handled carefully - I'm not sure if I am doing that or creating a memory issue down the road lol.

PS - I am using an M5Stack DinMeter board... Just disregard all the board specific stuff for display etc.

EDIT - I am using the one second looping interval so that the scheduled events fire as close to exact time as possible (employees like having the shop schedule match their phones lol). I also have an lcd display with M/D/Y + H:M:S

#include <WiFi.h>
#include <time.h>
#include <M5DinMeter.h>

const int switchPin = 2;  // Relay GPIO pin

const char* ssid = "wifi_ssid";
const char* password = "wifi_pw";

const char* NTP_SERVER = "us.pool.ntp.org";
const char* TZ_INFO    = "EST5EDT";  // enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php)

tm timeinfo;
time_t now;
long unsigned lastNTPtime;
unsigned long lastEntryTime;

// Shift Bell Time Triggers Schedule
String shift_times[8] = {"10:54", "10:55", "10:56", "10:57", "12:00", "12:40", "14:30", "15:30"};

void setup() {
  DinMeter.begin();
  DinMeter.Display.setRotation(1);
  DinMeter.Display.setTextColor(GREEN, BLACK);
  DinMeter.Display.setTextDatum(MC_DATUM);
  DinMeter.Display.setTextFont(&fonts::FreeMono24pt7b);
  DinMeter.Display.setTextSize(1);
  DinMeter.Display.setBrightness(32);
  DinMeter.Display.clear();

  M5.Display.drawString("WiFi...",M5.Display.width() / 2, M5.Display.height() /2);

  pinMode(switchPin, OUTPUT);  // Set pin to output mode for Relay control

	Serial.begin(115200);
  
  Serial.println("\n\nNTP Time Test\n");
  WiFi.begin(ssid, password);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    if (++counter > 100) ESP.restart();
    Serial.print ( "." );
  }
  Serial.println("\n\nWiFi connected\n\n");

  configTime(0, 0, NTP_SERVER);
  // See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region
  setenv("TZ", TZ_INFO, 1);

  if (getNTPtime(10)) {  // wait up to 10sec to sync
  } else {
    Serial.println("Time not set");
    ESP.restart();
  }
  
  DinMeter.Display.clear();
  
  lastNTPtime = time(&now);
  lastEntryTime = millis();
}


void loop() {
  static int LastSecond;
  static tm LastTime;
  static bool Relay;       // Relay Status
  char TimeTxt[6];

  while (LastTime.tm_sec == timeinfo.tm_sec) {
   // loop until next second
    time(&now);
    localtime_r(&now, &timeinfo);
  }

  if (LastTime.tm_min != timeinfo.tm_min) {
    strftime (TimeTxt, 6, "%H:%M", &timeinfo);
    if(std::find(std::begin(shift_times), std::end(shift_times), TimeTxt) != std::end(shift_times)){
        // Relay ON
        digitalWrite(switchPin, HIGH); 
        Relay = true;
        Serial.println("BELL ON");
      }
  }

  if ((timeinfo.tm_sec > 2) && (Relay)) {
    // Relay OFF after 3 seconds
    digitalWrite(switchPin, LOW);
    Relay = false;
    Serial.println("BELL OFF");
  }

  LastTime = timeinfo;

  showTime(LastTime); // FOO update display with localtime(&now)

}

bool getNTPtime(int sec) {

  {
    uint32_t start = millis();
    do {
      time(&now);
      localtime_r(&now, &timeinfo);
      Serial.print(".");
      delay(10);
    } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
    if (timeinfo.tm_year <= (2016 - 1900)) return false;  // the NTP call was not successful
  }
  return true;
}


 // Update Display
  void showTime(tm localTime) {
    char time_output[9];
    char date_output[9];
    strftime(time_output, 9, "%H:%M:%S", localtime(&now));
    strftime(date_output, 9, "%m/%d/%y", localtime(&now));

    M5.Display.drawString(date_output,M5.Display.width() / 2,33);
    M5.Display.drawString(time_output,M5.Display.width() / 2,102);

  }

  



There is a TimeAlarms library, which I think goes with the Time library that you are already using. It should do everything you need.

Are you referring to this library? I won't discount that option, but it's not related to the standard time.h that all the ESP32 NTP examples seem to be based on.

I did play around with a similar library named ezTime before I learned that NTP was already included in time.h, so I went back to basics to start from the ground up as generically as possible.

EDIT - I will say, TimeAlarms seems very close to what I am looking for. As a C++ noob, I am trying to balance "not reinventing the wheel" vs creating my own code to learn and improve. Thanks for the tip!

I’d convert/parse the alarm times into a nice “seconds since midnight” number in setup and then, once per second check them against the seconds.

Funny you should mention that - after writing this post and thinking about options I also thought that a numeric comparison would be preferable to the complicated string comparison.

Definite improvement there - thanks!

1 Like

Since this while loop monopolizes the processor and prevents any other activity, you could switch your logic around use an if() to do things only on a change in seconds. That would free up the processor and allow you to do several things at the same time and disentangle the relay turning off function from the rest of the code—you could turn on a buzzer for 0.5 sec or turn on a grow light for 8 hours.

I actually thought of that but was concerned that I might miss the trigger if I wasn't exactly on the second, which is why my time comparison is based on the change of minutes. I agree that your suggestion is preferable - I'll have to look into time math to come up with something like:

 if (timeinfo.tm_sec == LastTime.tm_sec + 1){
    // Do all my time comparisons and other stuff?
  }

OR introduce a new variable (or rework how the current time variable plays)

NextTime = LastTime.tm_sec +1;
 if (timeinfo.tm_sec == NextTime.tm_sec {
    // Do all my time comparisons and other stuff?
  }

When it hits 59 the logic doesn't work anymore. I think.

Earlier I posted a thing that just watches for any change in the reported seconds. I don't know if it uses the same library, but you should see the essence of the method:

void loop() {

// anything right here executes freely many times per second


// but

  t = rtc.getTime();

// freshly on a new second?
  static int lastSecond = -1;
  if (t.second == lastSecond) return;  // not yet!
  lastSecond = t.second;
  
// stuff below here executes exactly once per second

}

+1 for numbers not strings or Strings. Use unsigned long variables and confirm your calculations for second since midnight.

You could might use minutes since midnight. Then a bit of logic to launch three seconds of bell ringing but once per matching time.

Just use the same method to check for the minutes to transition, and only at a transition do the bell ringing. Which with all the processor available could just as well be done timed by delay(), offending some but who cares?

a7

Thanks for the ideas - I'll play around with them and see what happens.

Plus learn something new in the process - like RETURN (I suspected there was some kind of loop escape in C :wink:

Thanks for the ideas - I learn best by studying how others have solved an issue. There are so many ways to accomplish the same task in code, it's always interesting to me to see how others approach it. The balance of speed, code size, efficiency and readability are always being juggled. I always walk away a little smarter :wink:

@tomkatt20

I had something like this that could match your needs

  • runs on ESP32
  • maintains the time thanks to NTP
  • uses tone() to tickle a pin when one alarm time is reached

Alarm schedules are declared in a fixed way in an array (kept as minutes since midnight - could move to seconds, this would require using uint32_t instead of uint16_t and adding seconds into the macro)

may be it can help

/*
  # ============================================
  # code is placed under the MIT license
  #  Copyright (c) 2022 J-M-L
  #  For the Arduino Forum : https://forum.arduino.cc/u/j-m-l
  #
  #  Permission is hereby granted, free of charge, to any person obtaining a copy
  #  of this software and associated documentation files (the "Software"), to deal
  #  in the Software without restriction, including without limitation the rights
  #  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  #  copies of the Software, and to permit persons to whom the Software is
  #  furnished to do so, subject to the following conditions:
  #
  #  The above copyright notice and this permission notice shall be included in
  #  all copies or substantial portions of the Software.
  #
  #  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  #  IMPLIED, INCLUDING BUT NOT

  LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  #  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  #  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  #  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  #  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  #  THE SOFTWARE.
  #  ===============================================
*/

#include <WiFi.h>
#include <esp_sntp.h> // https://github.com/espressif/esp-idf/blob/v5.2/components/lwip/include/apps/esp_sntp.h (doc https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/system_time.html)

const int bellRelayPin = 2;  // Relay GPIO pin

#define convertToAlarm(hour,minute) ((hour)*60u+(minute))
const uint16_t alarms[] = {
  convertToAlarm(10, 0),
  convertToAlarm(12, 0),
  convertToAlarm(14, 50),
};
const byte numAlarms = sizeof alarms / sizeof * alarms;
byte nextAlarmIndex = 0;

const char* ssid = "xxxt";
const char* wifiPassword = "xxxx";
const char* ntpServer = "fr.pool.ntp.org"; // French one, use "pool.ntp.org" to be generic

bool ntpSyncCompleted = false;
bool wifiConnected = false;
time_t lastNtpSync;

void ntpSyncCallback(struct timeval *tv) {
  Serial.println("NTP synchronized");
  lastNtpSync = time(NULL);
  Serial.print("NTP sync completed on ");
  struct tm *pTime = localtime(&lastNtpSync);
  Serial.println(pTime, "%d/%m/%Y %H:%M:%S"); // https://github.com/espressif/arduino-esp32/blob/ccacb7e3d1dd0e58b309c83e5ebc2302ce97c7b2/cores/esp32/Print.h#L98
  ntpSyncCompleted = true;
}

void WiFiStationConnection(WiFiEvent_t event, WiFiEventInfo_t info) {
  Serial.println("...Network connection established");
}

void WiFiStationGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
  Serial.print("Obtained an IP address = ");
  Serial.println(WiFi.localIP());
  wifiConnected = true;
}

void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
  wifiConnected = false;
  Serial.print("Disconnected from network. (reason: ");
  Serial.println(info.wifi_sta_disconnected.reason);
  Serial.println(")\nAttempting to reconnect.");
  WiFi.begin(ssid, wifiPassword);
}

// Function that displays the current time every second (can force display by passing true)
//
bool checkTime(bool forceDisplay = false) {
  static unsigned long lastDisplay = -2000;
  if (ntpSyncCompleted) {
    time_t timestamp = time( NULL );
    struct tm *pTime = localtime(&timestamp );

    if (forceDisplay || (millis() - lastDisplay >= 1000)) {
      Serial.println(pTime, "%d/%m/%Y %H:%M:%S"); // https://github.com/espressif/arduino-esp32/blob/ccacb7e3d1dd0e58b309c83e5ebc2302ce97c7b2/cores/esp32/Print.h#L98
      lastDisplay = millis();
    }

    uint16_t now = convertToAlarm(pTime->tm_hour, pTime->tm_min);
    if (now >= alarms[nextAlarmIndex]) {
      nextAlarmIndex = (nextAlarmIndex + 1) % numAlarms;
      Serial.println("***** Bell ringing *****");
      tone(bellRelayPin, 500, 3000); // buzz for 3 seconds
    }
  }
  return ntpSyncCompleted;
}

void setup() {
  pinMode(bellRelayPin, OUTPUT);
  Serial.begin(115200);
  Serial.println("NTP based Bell Management");

  Serial.println("Connecting to WiFi network");
  // Callbacks to check if the network is operational
  WiFi.onEvent(WiFiStationConnection, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
  WiFi.onEvent(WiFiStationGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
  WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);

  // Configure the time retrieval from NTP, setting the timezone to France
  // and define winter / summer time
  // Last Sunday of March at 2:00 AM: GMT+2
  // Last Sunday of October at 3:00 AM: GMT+1
  configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer);

  // Callback to be notified when time sync with the NTP server is successful
  sntp_set_time_sync_notification_cb(ntpSyncCallback);

  WiFi.begin(ssid, wifiPassword);
}

void loop() {
  checkTime();

  // If your code requires WiFi, you can check
  if (wifiConnected) {
    // •••
  }

  // Code that does not require WiFi goes here
}
1 Like

One change-detection trick is to update the last observation inside the conditional. Then you get one trigger per change. Here's an untested snippet:

void loop(){
  time(&now);
  localtime_r(&now, &timeinfo);
  if (timeinfo.tm_sec != LastTime.tm_sec){
     // Do all my time comparisons and other stuff?
    LastTime = timeinfo;
  }
}

or slightly tested:

time_t now, lastTime;

void loop() {
  time(&now);
  if(now != lastTime){
    Serial.println(now);
    lastTime = now;
  }
}
1 Like

Where you have this originally

String shift_times[8] = {"10:54", "10:55", "10:56", "10:57", "12:00", "12:40", "14:30", "15:30"};

we've variously led you to using numbers. A nice trick for minutes since midnight is to have you time array as integers:

int shift_times[8] = {730, 945, 1115, 1200, 1415, 1800, 1947, 2130};

Then when you get the time from the RTC, calculate time since midnight as 100 * hours + minutes.

Comparisons for equality or greater than or less than are unaffected by using numbers that look like times.

You can always let the compiler count things, viz:

const byte nShiftTimes = sizeof shift_times / sizeof *shift_times;

Use nShiftTimes instead of 8 eveywhere else.

a7

Probably. Stoffregen was the RTC hero at that time. I didn't need it in the end. I just use change of day to start a new file at midnight.

here's that uses a local time structure holding hour and minutes that can be synchronized using any methed (e.g. NTP) but uses millis() to update a clock tic once every minute

the alarm is checked every clock tic

needs code to update the time (as often as desired)

a clock tic in the posted code is every 20 msec for testing. Should be 60,000, every minute


// -----------------------------------------------------------------------------
struct HrMin {
    unsigned hrs;
    unsigned mins;
};

HrMin time;

// -------------------------------------
void
timeSet (
    unsigned hrs,
    unsigned mins )
{
    time.hrs  = hrs;
    time.mins = mins;
}

// -------------------------------------
unsigned long MsecPeriod =  20;
unsigned long msec0;

bool
timerTic ()
{
    unsigned long  msec = millis ();
    if (msec - msec0 >= MsecPeriod)  {
        msec0 += MsecPeriod;

        if (60 <= ++time.mins)  {
            time.mins = 0;
            if (24 <= ++time.hrs)
                time.hrs = 1;
        }

        return true;
    }

    return false;
}

// -----------------------------------------------------------------------------
HrMin alarms [] = {
    { 10, 54 }, { 10, 55 }, { 10, 56 }, { 10, 57 },
    { 12,  0 }, { 12, 40 }, { 14, 30 }, { 15, 30 },
};
int Nalarm = sizeof(alarms)/sizeof(HrMin);

// -------------------------------------
bool
alarmChk ()
{
    for (int n = 0; n < Nalarm; n++)  {
        if (alarms [n].hrs == time.hrs && alarms [n].mins == time.mins)
            return true;
    }
    return false;
}

// -----------------------------------------------------------------------------
char s [90];

void loop ()
{
    if (timerTic ())  {
        sprintf (s, " %2d:%02d", time.hrs, time.mins);
        Serial.print (s);

        if (alarmChk ())
            Serial.println (" alarm");
        else
            Serial.println ();
    }
}

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

These are great suggestions! I knew the string comparison method wasn't the best choice and some kind of numeric would be better, but I wasn't certain how to approach it. I'm definitely going to play with a few iterations of these.

I also like the idea of outsourcing the time comparison to it's own function, rather than in-line of the main loop... clever.

Yes that’s what I did in the code posted previously

The macro convertToAlarm() does that and I end up with 3 alarms @ 10h00 12h00 and 14h50.

The code I shared also handles WiFi reconnect and awaits for the first NTP sync - so probably some food for thoughts there.

Yes, I skip the macro, 745. That's the trick.

Just don't be tempted to write 0930 for half nine 0730 for half seven.

a7

Using 0930 will at least give you an error, 0730 could silently do something unexpected:

Serial.println(0730);  // prints 472

I have edited my post. Better example!

a7

1 Like