Extension Lesson: Timeliness and Deadline-Driven Components
This extension lesson builds on the completed LED Blinker tutorial. You will refactor the Led component from an active component to a queued component driven synchronously by a rate group, introducing the concept of timeliness — the ability to detect when work exceeds its allotted time.
Prerequisites
You must have completed the LED Blinker Tutorial through at least Step 7: Full System Integration.
Why Timeliness Matters
In the original tutorial, the Led component is an active component. Active components own a thread and process work from an internal queue whenever that thread is scheduled by the OS. This approach works well for event-driven tasks (like responding to commands) and for non-deadline timing signals where approximate periodicity is sufficient. However, since there is jitter in the dispatch of the message and there is no way to detect slips, this approach is insufficient for deadline-driven work.
Consider what happens when the rate group invokes the Led component’s run port:
- The rate group puts a message on
Led’s queue via theasyncport - The rate group immediately moves on to its next member — it does not wait for
Ledto finish Led’s thread eventually picks up the message and processes it
If Led’s work takes longer than one rate group cycle, the rate group has no way of knowing. Messages simply pile up in the queue. In flight software, this is dangerous — a control loop that silently falls behind its deadline can lead to mission-critical failures.
The Solution: Queued Components with Synchronous Rate Group Dispatch
A queued component solves this by using a synchronous (sync) port for the rate group invocation. This means:
- The rate group calls
Led’srunhandler directly — it blocks until the handler returns - If the handler takes too long, the rate group detects a slip and emits a
WARNING_HIevent - Commands and other event-driven work are still accepted via
asyncports, queued up, and dispatched inside the synchronous handler
This pattern gives you the best of both worlds: hard deadline enforcement from the rate group, and asynchronous command handling without thread contention.
Rate Group (sync call)
|
v
+-----------+-----------+
| Led (queued) |
| |
| run_handler (sync): |
cmd ---->| 1. dispatchAvail..() |----> gpioSet
(async) | 2. blink logic |
+-----------------------+
[!TIP] For a deeper discussion of component and port kind selection, see the F Prime documentation on Selecting Component, Port, and Command Kinds.
Step 1: Update the FPP Model
Open LedBlinker/Components/Led/Led.fpp. We need to make two changes:
1a. Change the Component Kind from active to queued
- active component Led {
+ queued component Led {
1b. Change the run Port from async to sync
The run port must be synchronous so that it executes in the rate group’s thread context. This is what enables deadline detection.
@ Port receiving calls from the rate group
- async input port run: Svc.Sched
+ sync input port run: Svc.Sched
[!IMPORTANT] The
BLINKING_ON_OFFcommand remainsasync. This is intentional — commands are event-driven work and should be queued for processing during the next rate group cycle rather than executing immediately on the command dispatcher’s thread. This prevents thread contention and keeps all component state modifications on a single execution context.
Step 2: Update the Component Implementation
2a. Add Queue Dispatch to the run Handler
Since the BLINKING_ON_OFF command is async, it will be placed in the component’s queue when received. However, a queued component has no thread of its own to process the queue — we must explicitly dispatch queued messages during the synchronous run handler. This has the advantage that the time to dispatch the queue is also summed into the rate group, so all work done for the component is accounted for in deadline detection.
Open LedBlinker/Components/Led/Led.cpp and add the queue dispatch at the beginning of the run_handler:
void Led ::run_handler(FwIndexType portNum, U32 context) {
// Dispatch all queued messages (e.g. commands) that arrived since the last cycle
this->dispatchAvailableMessages();
// Read back the parameter value
Fw::ParamValid isValid = Fw::ParamValid::INVALID;
// ... rest of existing implementation unchanged ...
dispatchAvailableMessages() is a helper provided by QueuedComponentBase that dispatches all currently queued messages in a single call. It iterates over each message in the queue, calling doDispatch() for each one, and stops when the queue is empty or an error occurs.
2b. Understanding the Dispatch Pattern
In the active component version, the component’s internal thread continuously waited for messages and dispatched them one at a time. Now that we have removed the thread, nothing will process the queue unless we do it explicitly.
By placing dispatchAvailableMessages() at the beginning of run_handler, we ensure that:
- Commands received between cycles are processed at a well-defined point
- All work (command handling and blink logic) runs in the rate group’s thread context
- If the total work (dispatch + blink logic) exceeds the cycle time, the rate group detects the slip
Step 3: Update the Topology Instance
Since the component is no longer active, it does not need its own thread. However, it still needs a queue to buffer the async commands.
Open LedBlinker/LedBlinkerDeployment/Top/instances.fpp and find the led instance definition.
instance led: LedBlinker.Led base id 0x10005000 \
- queue size Default.QUEUE_SIZE \
- stack size Default.STACK_SIZE \
- priority 95
+ queue size Default.QUEUE_SIZE
Move the led instance from the “Active component instances” section to the “Queued component instances” section:
# ----------------------------------------------------------------------
# Queued component instances
# ----------------------------------------------------------------------
instance led: LedBlinker.Led base id 0x10005000 \
queue size Default.QUEUE_SIZE
[!NOTE] We keep
queue sizebecause the component still has an async command port that requires a queue. We removestack sizeandprioritybecause those configure a thread, which a queued component does not have.
Build the deployment to verify the changes:
# In LedBlinker/LedBlinkerDeployment
fprime-util build
Step 4: Update Unit Tests
Since the run port is now sync instead of async, invoking it in unit tests will execute the handler immediately — there is no queue message to dispatch. We need to remove the doDispatch() calls that follow invoke_to_run().
Open LedBlinker/Components/Led/test/ut/LedTester.cpp and update the test cases.
this->invoke_to_run(0, 0); // invoke the 'run' port to simulate running one cycle
- this->component.doDispatch(); // Trigger execution of async port
Remove all this->component.doDispatch() lines that follow invoke_to_run calls. The run port is now sync, so calling it executes the handler immediately — there is no queued message to dispatch.
[!IMPORTANT] Commands are still
async, sothis->component.doDispatch()is still required afterthis->sendCmd_BLINKING_ON_OFF(...). Only the run port dispatch changes.
Verify the tests pass:
# In LedBlinker/Components/Led
fprime-util check
Step 5: Observe Deadline Detection
Now that the Led component runs synchronously in the rate group, the rate group will detect if the component’s work takes longer than one cycle.
To test this, you could temporarily add a simulated delay to the run_handler:
// TEMPORARY: Simulate slow work to demonstrate rate group slip detection
Os::Task::delay(Fw::TimeInterval(2, 0)); // Sleep 2 seconds in a 1Hz rate group
Run the deployment with the GDS:
# In LedBlinker/LedBlinkerDeployment
fprime-util build
fprime-gds --ip-client
You should see a WARNING_HI event from the rate group indicating a cycle slip — the rate group detected that Led did not complete within its allotted time.
[!WARNING] Remove the simulated delay after testing! This is for demonstration purposes only.
What Changed: A Summary
| Aspect | Before (Active) | After (Queued) |
|---|---|---|
| Component kind | active |
queued |
run port kind |
async |
sync |
BLINKING_ON_OFF command |
async |
async (unchanged) |
| Thread | Owns a dedicated thread | No thread — runs on rate group’s thread |
| Queue | Dispatched by internal thread | Dispatched explicitly in run_handler |
| Deadline detection | None — rate group cannot detect slips | Rate group emits WARNING_HI on cycle slip |
| Instance definition | queue size, stack size, priority |
queue size only |
Key Takeaways
-
Active components hide deadline violations. Because the rate group sends an async message and moves on, it cannot detect if the component falls behind.
-
Queued components with sync rate group ports enforce deadlines. The rate group blocks on the component’s work and detects slips when the cycle time is exceeded.
-
Async commands should be dispatched inside the sync handler. This keeps all state modifications on a single execution context (the rate group’s thread) and avoids concurrency issues.
-
This is how flight software typically works. Components performing cyclic work are driven by rate groups and run synchronously. The
Svc::Healthcomponent in F Prime itself uses this exact pattern.
[!TIP] For more details on rate groups and timeliness, see: