Problem with using Time.day() to trigger something once a day

Theres a really similar post that says the solution to by problem accept for it does not seem to be working for me

void updateTime()
{
        if (newSec)
        {
            newSec = false;
        }
        else if (Time.second() != secondTracker && Particle.syncTimeDone())
        {
            secondTracker = Time.second();
            newSec = true;
        }

        if (newMin)
        {
            newMin = false;
        }
        else if (Time.minute() != minuteTracker && Particle.syncTimeDone())
        {
            minuteTracker = Time.minute();
            newMin = true;
        }

        if (newHour)
        {
            newHour = false;
        }
        else if (Time.hour() != hourTracker && Particle.syncTimeDone())
        {
            hourTracker = Time.hour();
            newHour = true;
        }

        if (newDay)
        {
            newDay = false;
        }
        else if (Time.day() != dayTracker && Particle.syncTimeDone())
        {
            dayTracker = Time.day();
            newDay = true;
        }

        if (newMonth)
        {
            newMonth = false;
        }
        else if (Time.month() != monthTracker && Particle.syncTimeDone())
        {
            monthTracker = Time.month();
            newMonth = true;
        }
}

This is called once per loop it sets flags so that every function called after it will have the flag be true then it will be turned off again the next loop

Im calling this every loop but it should only do something at the top of the day 00:00, or whenever the Time.day() actually changes

void runDaily()
{
    if (isNewDay())
    {
        Particle.syncTime(); // Sync time
    }
} 

I also have this function that runs hourly, but maybe do different logic depending on if the loop is also a new day

void runHourly()
{
    if (isNewHour()) // Check if it's time to run the hourly report
    {
        if(isNewDay()){
            publish(true,"New Day","%d",dayTracker);
        }
        Log.info("Hourly report: %d:00", Time.hour());

        on_The_Hour(isNewDay()); // forces unthrottled publish if new day

        powerReport();
    }
}

This screenshot is from this morning of 7/25/24

I am not sure why but its not only running my new hour check twice but also it is new day for both of those loops as well (It also jumped to 25 then back to 24?), im not sure if this has to do with me running Particle.syncTime() at the top of the day, so I tried to account for it with the Particle.syncTimeDone() which didnt seem to fix my issue.

I can't tell exactly what's wrong from briefly looking at the code, but there are several things that need to be taken into account:

  • Checking for equality of seconds is dangerous because if the loop is blocked or the time jumps because of synchronization, you're likely to miss the exact second.
  • Timezones can be tricky.
  • If you are in a location with daylight saving, that's even worse.

It may be overkill for your use case, but the LocalTimeRK library is designed to make scheduling things based on clock time easy.

2 Likes

Thank you Ill look into that!

@rickkas7 so i looked into your library you recommended, and im not sure if what i did was the wrong way to implement it but i seem to still be getting a similar issue



image
currently isNew____ is just returning the bool that is set ex: newMonth
but i still seem to be getting double publishes? but now its not just at midnight

it also happened at 1 pm

This is for one device^^

However on another it triggered at 3 and 7 twice

Im not sure if that has anything to do with how im implementing those if statements to check for the scheduled time or not, also since the midnight double publish is consistent could that be related to this?
image

Hi, I succeeded in the past using the DailyTimerSpark library:

I'm unsure how it compares to LocalTimeRK though.

2 Likes

I also cannot tell what is wrong with your code, but I think the order of the "if" statements feels strange to me. An "if-else" tree like this forms a priority encoder and things at the top of the tree happen first, while things below are skipped. The priority order of those statements matters.

There are a few concepts you need to keep in mind here:

  1. You probably do not want to compare to the exact second because sometimes things in the user thread are delayed. You want to check if the current time is "greater than" the alarm time.
  2. You should plan that your code can be called many times with the hh:mm:ss equal to the alarm time. You need a flag that tells you "this alarm has yet to go off" in one state, and "this alarm has already been handled and no action is required" in the other. Resetting this flag should be done only after you are sure you will not re-trigger the alarm.
  3. There are complicated factors as others have said above, like changing time zones, daylight savings time (Summer time), etc. These complexities mostly affect when you reset the flag in point 2 above to look for the next alarm and prevent re-triggering, but they can effect the first triggering as well, particularly when the change moves the time forward. That's why point 1 above of checking for "greater than" the alarm time can help.
1 Like

I will be heading out from work soon today but i may try a latch of some sort like this tomorrow

if(scheduledTime && timesinceLastHit >= someMillis){
timeSinceLastHit = timeNow
//etc etc
}

just to ensure it can only trigger once every so often to block multiple back to back runs being the "scheduled time"

Thanks for all of the help from everyone

1 Like

Could you copy and paste more of the code? It's not possible to tell if there's a logic error in the screenshot of the code above, and presumably there is one somewhere because it failed both ways.

2 Likes

I updated LocalTimeRK to version 1.1.2 to add some utility functions, unit tests, and an example of how to do this. It looks like this:

#include "LocalTimeRK.h"

SerialLogHandler logHandler;
SYSTEM_THREAD(ENABLED);

const unsigned long checkPeriodMs = 1000;
unsigned long checkLast = 0;

time_t nextMinutely = 0;
time_t nextHourly = 0;
time_t nextDaily = 0;

void setup() {
    // Set timezone to the Eastern United States
    LocalTime::instance().withConfig(LocalTimePosixTimezone("EST5EDT,M3.2.0/2:00:00,M11.1.0/2:00:00"));

}

void loop() {
    static unsigned long lastLog = 0;
    if (millis() - checkLast > checkPeriodMs && Time.isValid()) {
        checkLast = millis();

        time_t now = Time.now();

        if (!nextMinutely || nextMinutely <= now) {
            LocalTimeConvert conv;
            conv.withCurrentTime().convert();
            String currentTime = conv.timeStr().c_str();
            
            conv.nextMinute();
            nextMinutely = conv.time;
            Log.info("minutely current=%s next=%s", currentTime.c_str(), conv.timeStr().c_str());
        }
        if (!nextHourly || nextHourly <= now) {
            LocalTimeConvert conv;
            conv.withCurrentTime().convert();
            String currentTime = conv.timeStr().c_str();

            conv.nextHour();
            nextHourly = conv.time;
            Log.info("hourly current=%s next=%s", currentTime.c_str(), conv.timeStr().c_str());
        }
        if (!nextDaily || nextDaily <= now) {
            LocalTimeConvert conv;
            conv.withCurrentTime().convert();
            String currentTime = conv.timeStr().c_str();
            
            conv.nextDayMidnight();
            nextDaily = conv.time;

            Log.info("daily current=%s next=%s", currentTime.c_str(), conv.timeStr().c_str());
        }
    }
}

  • This code does catch-up, which is to say if you run it the first time between intervals it will run the code in the off-interval. To only run on actual intervals, only do your activity when the variable such as nextDaily is non-zero.
  • The daily check is for 00:00:00 not 23:59:59 because you should never check for an exact second, because that second could be missed.

If you need to know the date of the actual end of the time period (yesterday), use something like this:

conv.withTime(nextDaily - 1).convert();
Log.info("for day ending: %s", conv.timeStr().c_str());

The logs look like this. Time are in the local time zone with daylight saving, but calculations are done at UTC.

0000489083 [app] INFO: minutely current=Thu Aug  1 05:58:00 2024 next=Thu Aug  1 05:59:00 2024
0000549143 [app] INFO: minutely current=Thu Aug  1 05:59:00 2024 next=Thu Aug  1 06:00:00 2024
0000609203 [app] INFO: minutely current=Thu Aug  1 06:00:00 2024 next=Thu Aug  1 06:01:00 2024
0000609228 [app] INFO: hourly current=Thu Aug  1 06:00:00 2024 next=Thu Aug  1 07:00:00 2024
3 Likes

Here is the main loop that determines when these things are called, and below is pretty much the whole file for timing/scheduling

void setUpSchedules()
{
    LocalTime::instance().withConfig(LocalTimePosixTimezone("EST5EDT,M3.2.0/2:00:00,M11.1.0/2:00:00"));
    secondTracker = millis();
    everyMinute.withMinuteOfHour(1);
    everyHour.withMinuteOfHour(60);
    everyDay.withHourOfDay(24);
    everyMonth.withDayOfMonth(-1, LocalTimeHMS("23:59:59")); // last day of the month at 11:59
}

bool isNewMonth()
{
    return newMonth;
}

bool isNewDay()
{
    return newDay;
}

bool isNewHour()
{
    return newHour;
}

bool isNewMinute()
{
    return newMin;
}

bool isNewSecond()
{
    return newSec;
}

void updateTime()
{

    if (newSec)
    {
        newSec = false;
    }
    else if (millis() - secondTracker >= 1000)
    {
        secondTracker = millis();
        newSec = true;
    }

    if (newMin)
    {
        newMin = false;
    }
    else if (everyMinute.isScheduledTime())
    {
        newMin = true;
    }

    if (newHour)
    {
        newHour = false;
    }
    else if (everyHour.isScheduledTime())
    {
        newHour = true;
    }

    if (newDay)
    {
        newDay = false;
    }
    else if (everyDay.isScheduledTime())
    {
        newDay = true;
    }

    if (newMonth)
    {
        newMonth = false;
    }
    else if (everyMonth.isScheduledTime())
    {
        newMonth = true;
    }
}

void runMonthly()
{
    if (isNewMonth())
    {
        Particle.publish("Monthly Restart", Time.timeStr());
        System.reset();
    }
}

void runDaily()
{
    if (isNewDay())
    {
        Particle.syncTime(); // Sync time
    }
}

void runHourly()
{
    if (isNewHour()) // Check if it's time to run the hourly report
    {
        if (isNewDay())
        {
            publish(true, "New Day", "%d", Time.day());
        }
        Log.info("Hourly report: %d:00", Time.hour());

        publishErrorCount(true); // do first since everything after is part of new hour

        publishAndResetUptime();

        Particle.publishVitals();

        on_The_Hour(isNewDay()); // forces unthrottled publish if new day

        reportGrossCalculationFromLastHour(true); // true makes unthrottled

        powerReport();
    }
}

void runEveryMinute()
{
    if (isNewMinute())
    {
        Log.info("Sys free mem: %lu bytes", System.freeMemory());
        EEPROM_Helper("print");
    }
}

void runEverySecond()
{
    if (isNewSecond())
    {
        publishNext();
    }
}

@rickkas7 is there a reason you only check every second, rather than just every loop through. also i beleive your solution probably still is affected by the same problem I was having before, when I did particle sync time for some reason the date from Time would jump forward or back a day, since i was using the greater than like you are here

if (!nextMinutely || nextMinutely <= now) {

it would trigger it twice in a row, once when it was actually the new day and once when the timing was being synced (I think). I see that youre using the Time.valid in the check before testing times, however i tried to implment that and it didnt seem to make a difference to what happened

You can check every loop, but since the real time clock is second-based, the only thing it would do is lower the latency slightly.

There is always the possibility of events occurring twice when the time synchronizes. This is probably only a real concern for minutely. The solution is to remember the time_t value for the last minutely check and if it occurs again, skip it. There are weird edge cases that it still misses, but it fixes the most common one easily.

1 Like

How does this example account for minutes / hour / etc swapping back to 0
Like if it goes 58 -> 59 that looks like it works fine but then would 59 -> 0 immediately trigger after a second since 0<= 59

I see now it comparing time not the direct minute sorry for the confusion

On second thought, the easiest solution is to do sync time when it's unlikely to cause harm, such as at 03:30:30. Use a separate sync time variable and conv.nextDay(LocalTimeHMS("03:30:30"))

1 Like
    if (!nextMonth || nextMonth <= now)
    {
        LocalTimeConvert conv;
        conv.withCurrentTime().convert();
        conv.nextDayOfNextMonth(-1,LocalTimeHMS("23:59:59"));
        nextMonth = conv.time;
        newMonth = true;
    }

Would this be a correct implementation of how do this sort of timing for the last day of the month

I would use nextDayOfNextMonth(1). This will get you midnight on the first of the next month, but you can then subtract to get the correct month you are generating the data for.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.