@@ -59,6 +59,7 @@ Scheduler::Scheduler() :
5959 m_loadSchedule(true ),
6060 m_lastLoadDate(0 ),
6161 m_lastProcTime(0 ),
62+ m_lastCommandCheckTime(0 ),
6263 m_timeDelta(0 ),
6364 m_timeDeltaThreshold(0 ),
6465 m_forcedNextPlaylist(SCHEDULE_INDEX_INVALID ),
@@ -260,66 +261,14 @@ void Scheduler::AddScheduledItems(ScheduleEntry* entry, int index) {
260261 }
261262 }
262263
263- // Convert everything to a day mask to simplify code below
264- switch (dayIndex) {
265- case INX_SUN :
266- dayIndex = INX_DAY_MASK_SUNDAY ;
267- break ;
268- case INX_MON :
269- dayIndex = INX_DAY_MASK_MONDAY ;
270- break ;
271- case INX_TUE :
272- dayIndex = INX_DAY_MASK_TUESDAY ;
273- break ;
274- case INX_WED :
275- dayIndex = INX_DAY_MASK_WEDNESDAY ;
276- break ;
277- case INX_THU :
278- dayIndex = INX_DAY_MASK_THURSDAY ;
279- break ;
280- case INX_FRI :
281- dayIndex = INX_DAY_MASK_FRIDAY ;
282- break ;
283- case INX_SAT :
284- dayIndex = INX_DAY_MASK_SATURDAY ;
285- break ;
286- case INX_EVERYDAY :
287- dayIndex = INX_DAY_MASK_EVERYDAY ;
288- break ;
289- case INX_WKDAYS :
290- dayIndex = INX_DAY_MASK_WEEKDAYS ;
291- break ;
292- case INX_WKEND :
293- dayIndex = INX_DAY_MASK_WEEKEND ;
294- break ;
295- case INX_M_W_F :
296- dayIndex = INX_DAY_MASK_M_W_F ;
297- break ;
298- case INX_T_TH :
299- dayIndex = INX_DAY_MASK_T_TH ;
300- break ;
301- case INX_SUN_TO_THURS :
302- dayIndex = INX_DAY_MASK_SUN_TO_THURS ;
303- break ;
304- case INX_FRI_SAT :
305- dayIndex = INX_DAY_MASK_FRI_SAT ;
306- break ;
307- case INX_ODD_DAY :
308- case INX_EVEN_DAY :
309- // Odd/Even is based on the FPP 'epoch', the date of the first
310- // commit to the FPP repository on github, July 15, 2013
311- struct std ::tm FPPEpoch = { 0 , 0 , 0 , 15 , 6 , 113 };
312- std::time_t FPPEpochTimeT = std::mktime (&FPPEpoch);
313- int daysSince = (int )std::difftime (currTime, FPPEpochTimeT) / (60 * 60 * 24 );
314-
315- int dayOffset = 0 ;
316- if (daysSince % 2 ) { // Today is an odd day
317- if (entry->dayIndex == INX_EVEN_DAY )
318- dayOffset = 1 ;
319- } else { // Today is an even day
320- if (entry->dayIndex == INX_ODD_DAY )
321- dayOffset = 1 ;
322- }
264+ // Convert everything to a day mask to simplify code below. Odd/even is
265+ // not a fixed set of weekdays, so it schedules its own occurrences here
266+ // rather than through the mask; dayIndex is left as INX_ODD_DAY/EVEN_DAY.
267+ if ((dayIndex == INX_ODD_DAY ) || (dayIndex == INX_EVEN_DAY )) {
268+ // dayOffset is 0 when today already matches this entry's parity, 1
269+ // when it does not (so the next - and previous - matching day is one
270+ // day away, since odd/even parity alternates daily).
271+ int dayOffset = entry->IsOddEvenMatch (currTime) ? 0 : 1 ;
323272
324273 // Schedule yesterday if needed to handle midnight crossovers
325274 if (dayOffset)
@@ -328,12 +277,20 @@ void Scheduler::AddScheduledItems(ScheduleEntry* entry, int index) {
328277 for (int i = now.tm_wday + dayOffset; i <= scheduleDistance; i += 2 ) {
329278 entry->pushStartEndTimes (i, m_timeDelta, m_timeDeltaThreshold);
330279 }
331-
332- break ;
280+ } else {
281+ dayIndex = ScheduleEntry::DayIndexToMask (dayIndex) ;
333282 }
334283
335- // Special case if today is Sunday, handle any Saturday night crossovers
336- if (now.tm_wday == 0 )
284+ // Special case: if today is Sunday, a Saturday-night start that crosses
285+ // midnight (e.g. 22:00->02:00) bleeds into Sunday morning. Push it only
286+ // for entries that actually run on Saturday - hence the Saturday-mask
287+ // test - so a Monday-only crossover entry no longer plays early Sunday.
288+ // Odd/even entries are excluded because the block above already pushes
289+ // yesterday (Saturday, when today is Sunday) whenever today's parity does
290+ // not match; firing here too would double-push or push the wrong parity.
291+ if ((now.tm_wday == 0 ) &&
292+ (entry->dayIndex != INX_ODD_DAY ) && (entry->dayIndex != INX_EVEN_DAY ) &&
293+ (dayIndex & INX_DAY_MASK_SATURDAY ))
337294 entry->pushStartEndTimes (-1 , m_timeDelta, m_timeDeltaThreshold);
338295
339296 if ((entry->dayIndex != INX_ODD_DAY ) && (entry->dayIndex != INX_EVEN_DAY )) {
@@ -701,6 +658,12 @@ void Scheduler::CheckScheduledItems(bool restarted) {
701658 std::vector<PlayerAction> actions;
702659 std::vector<CountdownPreset> presets;
703660
661+ // Longest a scheduled FPP command may fire after its second passed. Bounds
662+ // the (m_lastCommandCheckTime, now] catch-up window so a forward clock step
663+ // does not replay a batch of stale commands; anything older is logged and
664+ // marked ran instead of run.
665+ constexpr std::time_t maxCommandCatchup = 60 ;
666+
704667 // Each pass decides what to do while holding m_scheduleLock and runs it
705668 // after releasing the lock, since starting or stopping a playlist takes
706669 // m_playlistMutex in the opposite order (see Scheduler.h). A pass stops
@@ -730,9 +693,27 @@ void Scheduler::CheckScheduledItems(bool restarted) {
730693 continue ;
731694 }
732695 } else {
733- if (itemTime.first < now) {
696+ // An FPP Command has no duration to catch up to, so a
697+ // missed second would silently drop it. Fire any
698+ // command whose second falls in the (lastCheck, now]
699+ // window this run has not examined yet, but never one
700+ // more than maxCommandCatchup seconds late so a large
701+ // clock step cannot replay a batch of stale commands.
702+ if (itemTime.first <= m_lastCommandCheckTime) {
703+ // A prior run already had its chance at this second.
704+ // A reload can reset item.ran (FPP-command ran-state
705+ // is not restored from m_ranItems), so gate on the
706+ // check window, not on item.ran, to avoid re-firing.
707+ SetItemRan (item, true );
708+ continue ;
709+ }
710+ if ((now - itemTime.first ) > maxCommandCatchup) {
711+ LogWarn (VB_SCHEDULE ,
712+ " Scheduled command '%s' skipped, %ld seconds late (catch-up cap is %ld s)\n " ,
713+ item.command .c_str (), (long )(now - itemTime.first ),
714+ (long )maxCommandCatchup);
734715 SetItemRan (item, true );
735- continue ; // skip any FPP Commands that are in the past
716+ continue ;
736717 }
737718 doScheduledCommand (itemTime.first , item);
738719 }
@@ -751,6 +732,14 @@ void Scheduler::CheckScheduledItems(bool restarted) {
751732 RunPlayerActions (actions);
752733 RunCountdownPresets (presets);
753734 } while (!actions.empty ());
735+
736+ // Advance the command catch-up window past every second this call examined.
737+ // Done once, after all rescans, so each rescan judges commands against the
738+ // same window; within a call the item.ran flag prevents a second firing.
739+ // Left unchanged if the clock ran backwards so the window never moves ahead
740+ // of the wall clock.
741+ if (now > m_lastCommandCheckTime)
742+ m_lastCommandCheckTime = now;
754743}
755744
756745void Scheduler::ClearScheduledItems () {
0 commit comments