Skip to content

TimerService in Pico-Framework

TimerService is a high-level scheduling component designed for embedded applications. It enables your application to schedule future actions using simple time-based declarations, ranging from fixed intervals to day-of-week schedules and start/stop durations.

This guide walks through the design, usage, and best practices for using TimerService.


What TimerService Provides

Unlike raw FreeRTOS timers, TimerService integrates with the EventManager to deliver high-level Event objects instead of raw callbacks. This enables non-blocking, event-driven scheduling using familiar application semantics.

Key features:

  • One-shot timers (scheduleAt)
  • Repeating timers (scheduleEvery)
  • Day-of-week + time-of-day schedules (scheduleDailyAt)
  • Paired duration-based events (scheduleDuration)
  • Job ID tracking and cancellation
  • Thread-safe on multicore systems
  • Support for both SystemNotification and UserNotification
  • Future: persistent job reloading and missed event recovery

Design Philosophy

TimerService embodies the event-first architecture of Pico-Framework. Rather than relying on raw callbacks or polling loops, it publishes well-typed Event objects to the EventManager, enabling clear separation of logic, easy observability, and full testability.


How It Works

TimerService wraps FreeRTOS timers internally and manages an active map of jobs using std::map<std::string, TimerHandle_t>. Each job has a string ID and is automatically cleaned up when completed or canceled.

Events are posted via EventManager, and can use either system or user notifications:

Event::user(UserNotification::SomethingHappened);
Event::system(SystemNotification::NetworkReady);

All timers are protected by a FreeRTOS mutex for thread and multicore safety.


One-Shot Events with scheduleAt()

Schedule a one-time event at a specific time_t timestamp:

time_t when = PicoTime::now() + 10; // 10 seconds from now
Event e = Event::user(UserNotification::StartPump);
TimerService::instance().scheduleAt(when, e);

Optionally, provide a job ID to allow cancellation:

TimerService::instance().scheduleAt(when, e, "pump_start_10s");

Repeating Events with scheduleEvery()

Schedule a repeating timer every N milliseconds:

TimerService::instance().scheduleEvery(1000, Event::user(UserNotification::Heartbeat));

Or with a named job ID:

TimerService::instance().scheduleEvery(30000, Event::user(UserNotification::CheckStatus), "check_status");

These timers persist until explicitly canceled.


Daily Scheduling with scheduleDailyAt()

Run a job at a specific time on selected days:

TimeOfDay tod = TimeOfDay::fromString("06:30:00");
DaysOfWeek days = Days::Mon | Days::Wed | Days::Fri;

TimerService::instance().scheduleDailyAt(tod, days, Event::user(UserNotification::Irrigate));

Internally, this uses the TimerServiceTOD subclass to compute the next match and re-schedule daily. Each run requeues itself for the next valid day/time.

Note: Daily jobs are not persistent across restarts and are not rehydrated. This will be addressed in a future version.


Duration-Based Start/Stop Events

You can pair a start event with a delayed stop event:

TimeOfDay start = TimeOfDay::fromString("07:00:00");
uint32_t duration = 30 * 60 * 1000; // 30 minutes

TimerService::instance().scheduleDuration(start, Days::All,
    Event::user(UserNotification::ZoneOn),
    Event::user(UserNotification::ZoneOff),
    "morning_watering");

The stop event is automatically scheduled after the configured delay. You only need to track the base job ID.


Canceling a Job

To cancel any job:

bool removed = TimerService::instance().cancel("check_status");

This stops and deletes the FreeRTOS timer and removes the job from internal tracking.


Thread Safety

All access to internal state is protected by a StaticSemaphore_t-based mutex, ensuring:

  • Safe scheduling and cancellation from any task
  • Multicore compatibility
  • No race conditions on job map

Summary

Use TimerService for:

Schedule Type Method
One-shot at time scheduleAt()
Fixed interval scheduleEvery()
Time of day scheduleDailyAt()
Start + stop scheduleDuration()

All jobs are cancelable by ID, and all events are published to EventManager.


Example: Irrigation Scheduling

void MyApp::scheduleIrrigation() {
    TimeOfDay start = TimeOfDay::fromString("06:00");
    TimerService::instance().scheduleDuration(start, Days::Mon | Days::Wed,
        Event::user(UserNotification::Zone1Start),
        Event::user(UserNotification::Zone1Stop),
        "zone1_mw_6am");
}

This schedules a start event at 6:00 AM on Monday and Wednesday, and automatically schedules a stop after the configured duration.