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
SystemNotificationandUserNotification - 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.