Task Runner
The Infuse-IoT Task Runner is a library designed for high-level task scheduling based on the current application state.
The task runner consists of a core scheduling loop, which determines when tasks should be started or terminated, and individual tasks, which perform some action when scheduled.
In a typical Infuse-IoT application, the Task Runner is the driver of the majority of the application behaviour. Combined with builtin task implementations for the most common application actions, the Task Runner allows the basis of new applications to be created in extremely small amounts of code.
Task Scheduling
Tasks are scheduled based on the evaluation of individual task_schedule’s,
which are evaluated once per second. In order for a task to be started, all of the start
conditions must be met, while only a single termination condition must be met to trigger
the task termination.
The current set of potential scheduling conditions are:
Battery charge percentage
Application runtime
Run on N second multiples (
TASK_PERIODICITY_FIXED)Run at most every N seconds (
TASK_PERIODICITY_LOCKOUT)Run N seconds after another schedule finishes (
TASK_PERIODICITY_AFTER)Task runtime timeout
Application states
Combining these basic options together allows the construction of complex scheduling conditions in a compact form, for example:
Run this task once a minute while moving, as long as the battery is over 20% charged and the current global time is known. If the battery drops below 15%, or the task has been running for over 15 seconds, terminate it.
struct schedules schedule_list[] = {
{
.task_id = SOME_TASK_ID,
.validity = TASK_VALID_ALWAYS,
.periodicity_type = TASK_PERIODICITY_FIXED,
.timeout_s = 15,
.battery_start.lower = 20,
.battery_terminate.lower = 15,
.periodicity.fixed.period_s = 60,
.states_start = TASK_STATES_DEFINE(TR_NOT | INFUSE_STATE_DEVICE_STATIONARY, INFUSE_STATE_TIME_KNOWN),
},
};
Common Scheduling Fields
Each task_schedule contains the following common fields, independent
of the task-specific arguments described in the next section:
task_idIdentifies the task implementation that this schedule starts. Multiple schedules can reference the same task ID, although only one schedule for a task implementation can run at a time.
validityControls when the schedule itself is valid.
TASK_VALID_ALWAYSis always eligible,TASK_VALID_ACTIVEis eligible only whileINFUSE_STATE_APPLICATION_ACTIVEis set, andTASK_VALID_INACTIVEis eligible only while it is clear.TASK_VALID_PERMANENTLY_RUNSbypasses normal entry and exit checks and restarts the task if it terminates. TheTASK_LOCKEDflag can be ORed into this field to prevent KV store updates from replacing the schedule.periodicity_typeSelects which member of the
periodicityunion is used for start timing. A zero value means there is no periodicity condition, so start timing is controlled only by the other start conditions.boot_lockout_minutesPrevents the task from starting until the application has been running for this many minutes. A value of
0disables the boot lockout.timeout_sRequests task termination once the current run has lasted this many seconds. A value of
0disables timeout-based termination.battery_startOptional battery charge thresholds for starting the task.
lowerrequires the battery percentage to be greater than or equal to the configured value, whileupperrequires it to be less than or equal to the configured value. A threshold value of0disables that side of the range.Example:
.battery_start.lower = 30, .battery_start.upper = 80,
This schedule can only start when the battery charge is between 30% and 80%, inclusive. If only
lowerwas set, the task could start at 30% or above; if onlyupperwas set, it could start at 80% or below.battery_terminateOptional battery charge thresholds for terminating the task.
lowerrequests termination when the battery percentage is less than or equal to the configured value, whileupperrequests termination when it is greater than or equal to the configured value. A threshold value of0disables that threshold.Example:
.battery_terminate.lower = 20,
Once the task is running, this requests termination if the battery charge falls to 20% or below.
periodicity.fixed.period_sUsed with
TASK_PERIODICITY_FIXED. The task can start only when the current global time is on anNsecond boundary.periodicity.lockout.lockout_sUsed with
TASK_PERIODICITY_LOCKOUT. The task can start only after this many seconds have elapsed since the schedule last started. OR inTASK_RUNNER_LOCKOUT_IGNORE_FIRSTto allow the first run to start without waiting for the initial lockout period.Example:
.periodicity_type = TASK_PERIODICITY_LOCKOUT, .periodicity.lockout.lockout_s = TASK_RUNNER_LOCKOUT_IGNORE_FIRST | (30 * SEC_PER_MIN),
The first run may start as soon as the other start conditions pass. After that, each run is separated from the previous start time by at least 30 minutes.
periodicity.after.schedule_idxandperiodicity.after.duration_sUsed with
TASK_PERIODICITY_AFTER. The task can startduration_sseconds after the schedule atschedule_idxterminates.Example:
.periodicity_type = TASK_PERIODICITY_AFTER, .periodicity.after.schedule_idx = 0, .periodicity.after.duration_s = 10,
This schedule can start 10 seconds after schedule index 0 terminates, assuming the other start conditions are also satisfied.
periodicity.lockout_dynamic_batteryUsed with
TASK_PERIODICITY_LOCKOUT_DYNAMIC_BATTERY. The lockout behaves likeTASK_PERIODICITY_LOCKOUT, but the interval is derived from the current battery percentage. The lockout islockout_minat or belowbattery_min,lockout_maxat or abovebattery_max, and linearly interpolated between those points.Example:
.periodicity_type = TASK_PERIODICITY_LOCKOUT_DYNAMIC_BATTERY, .periodicity.lockout_dynamic_battery = { .battery_min = 20, .battery_max = 80, .lockout_min = 60 * SEC_PER_MIN, .lockout_max = 10 * SEC_PER_MIN, },
At 20% battery or below, runs are separated by 60 minutes. At 80% battery or above, runs are separated by 10 minutes. Between those thresholds, the lockout is linearly interpolated, so a mid-range battery gives a mid-range lockout.
states_start_timeout_2x_sOptional fallback for the start state conditions. When non-zero,
states_startis treated as satisfied once twice this value in seconds has elapsed since the schedule last started. UseTASK_STATES_START_TIMEOUTwhen initialising this field.Example:
.states_start_timeout_2x_s = TASK_STATES_START_TIMEOUT(20 * SEC_PER_MIN), .states_start = TASK_STATES_DEFINE(INFUSE_STATE_TIME_KNOWN),
The task can start when time is known. If that state is not set, the state condition is still treated as satisfied once 20 minutes have elapsed since the schedule last started.
states_startApplication state conditions that must evaluate true before the task can start. Construct this field with
TASK_STATES_DEFINE; conditions are ANDed by default, can be inverted withTR_NOT, and can be ORed withTR_OR.Example:
.states_start = TASK_STATES_DEFINE( TR_NOT | INFUSE_STATE_DEVICE_STATIONARY, INFUSE_STATE_TIME_KNOWN),
The task can start only when the device is not stationary and the global time is known.
.states_start = TASK_STATES_DEFINE( INFUSE_STATE_DEVICE_STARTED_MOVING, TR_OR | INFUSE_STATE_HIGH_PRIORITY_UPLINK),
The task can start when either the device has started moving or a high priority uplink is requested.
states_terminateApplication state conditions that request task termination when they evaluate true. This field uses the same
TASK_STATES_DEFINE,TR_NOT, andTR_ORhelpers asstates_start.Example:
.states_terminate = TASK_STATES_DEFINE(INFUSE_STATE_DEVICE_STATIONARY),
Once the task is running, this requests termination when the device becomes stationary.
task_loggingCommon logging configuration for task output. Each entry selects a set of TDF loggers and a task-defined TDF mask. The task implementation decides which masks are meaningful.
Example:
.task_logging[0].loggers = TDF_DATA_LOGGER_SERIAL, .task_logging[0].tdf_mask = TASK_GNSS_LOG_LLHA | TASK_GNSS_LOG_FIX_INFO,
The task may emit the LLHA and fix information TDFs to the serial logger. The
tdf_maskbits are task-specific, so the available values depend on the selectedtask_id.
Task Arguments
Each task schedule can also be assigned arguments related to the task itself. This allows the behaviour of the task to be customised as the application desires, without needing to modify the tasks source code. These arguments can also be updated without needing to perform a full firmware update, in case parameters need to be tweaked after deployment.
struct schedules schedule_list[] = {
{
.task_id = TASK_ID_IMU,
.validity = TASK_VALID_ALWAYS,
.task_args.imu =
{
.accelerometer =
{
.range_g = 4,
.rate_hz = 50,
},
.gyroscope =
{
.range_dps = 500,
.rate_hz = 50,
},
.fifo_sample_buffer = 100,
},
},
};
Task argument structures, task IDs, and task logging masks are generated from
scripts/west_commands/cloud_definitions/tasks.json by west cloudgen.
Generated headers are written under
generated/include/infuse/task_runner/tasks. The task-specific argument
headers define the struct task_<task>_args types and constants such as
TASK_<TASK>_LOG_*. The common
generated/include/infuse/task_runner/tasks/infuse_task_args.h header
combines them into task_arguments, which is embedded directly
as task_schedule.task_args.
Downstream applications can add task definitions by providing an extension
tasks.json to west cloudgen:
west cloudgen -d path/to/extensions -o path/to/application
Extension task definitions are merged with the built-in definitions.
When a downstream task should be included by the generated
infuse_tasks.h aggregate header, the application should provide a matching
task API header such as include/infuse/task_runner/tasks/<task>.h. That
header typically declares the task implementation entry points or helper macros.
Updating Task Schedules
Task schedules can be updated at runtime without a full firmware update through the
usage of the Key-Value Store. When CONFIG_KV_STORE_KEY_TASK_SCHEDULES
is enabled, the schedules provided to task_runner_init() are treated as the
default schedules distributed with the application.
Any writes to the underlying task schedule KV slots will replace the default
schedule until a new set of default schedules are distributed. A new set of defaults
are signified by incrementing the CONFIG_TASK_RUNNER_DEFAULT_SCHEDULES_ID
option. This must be used if the default schedules are changing in a way that
could be incompatible with previous definitions. One example of this is if a new
schedule is inserted in the middle of the default schedule list.
When a new schedule is written to KV_KEY_TASK_SCHEDULES or a default schedule
reset is triggered by a write to KV_KEY_TASK_SCHEDULES_DEFAULT_ID, all currently
running tasks are terminated and all schedules are reloaded and revalidated.
Disabling schedule updates
If there is a particular task schedule that must never be updated for correct
operation of a device, that can be controlled by adding the TASK_LOCKED
flag to the task_schedule.validity field of the schedule like below:
struct schedules schedule_list[] = {
{
.task_id = TASK_ID_IMU,
.validity = TASK_LOCKED | TASK_VALID_ALWAYS,
},
};
This flag will prevent task_runner_schedules_load() from modifying the
provided schedule, regardless of the value saved in the KV store.
Inspecting Encoded Schedules
Encoded task schedules can be inspected with infuse schedule decode. Pass
the schedule payload as hex or base64 to print a readable description:
infuse schedule decode <schedule>
Use --python to emit Python assignment lines instead. This is useful when
turning an existing encoded schedule into a starting point for small edits:
infuse schedule decode --python <schedule>
The python-tools/scripts/encode_task_schedule_example.py script shows the
opposite flow: build an infuse_iot.task_runner.schedule.TaskSchedule in
Python, set common fields, task logging, and task-specific arguments, then print
the encoded bytes as hex or base64. Copy the decoded assignments into a similar
script, adjust the fields of interest, and re-encode the schedule for a KV
update or other deployment path.
Task Schedule vs Task Implementation
A task schedule is a description of when a task implementation should be run.
A task schedule is linked to the implementation through the task_schedule.task_id
field. A single application can have multiple schedules referring to the same
task implementation, although only a single schedule per task implementation can
be running at a given time.
Schedule Evaluation
All schedules in an application are evaluated at the same time by the
task_runner_iterate() function, which is required to be run once
a second. This task can be offloaded from the application by calling
task_runner_start_auto_iterate(), which will automatically call
the former function from the Infuse Workqueue context.
The application is able to receive notifications of when a schedule is started,
requested to terminate, or stopped, by assigning a task_schedule_event_cb_t
to the appropriate task_schedule_state.event_cb field AFTER the
task runner is initialised with task_runner_init().
Schedule Event notifications
If required, applications can register to be notified of scheduling events for
a given schedule. The available events are defined in task_schedule_event.
To register for callbacks on these events, populate task_schedule_state.event_cb
on the same index as the schedule of interest, after the call to task_runner_init().
For example to subscribe to scheduling callbacks for the battery task:
struct schedules schedules[] = {
{
.task_id = TASK_ID_IMU,
.validity = TASK_VALID_ALWAYS,
},
{
.task_id = TASK_ID_BATTERY,
.validity = TASK_VALID_ALWAYS,
},
};
TASK_SCHEDULE_STATES_DEFINE(states, schedules);
void my_callback(const struct task_schedule *schedule, enum task_schedule_event event)
{
...
}
int main(void)
{
task_runner_schedules_load(0, schedules, ARRAY_SIZE(schedules));
task_runner_init(schedules, states, ARRAY_SIZE(schedules), ...);
states[1].event_cb = my_callback;
}
Task Implementations
Tasks can be implemented as running as either a dedicated thread or as a delayable workqueue item running on the Infuse Workqueue. The former allows for more flexibility in terms of blocking operations, while the latter is more lightweight in terms of RAM resources since there is no need for a dedicated thread stack per task.
Built-in Tasks
Infuse-IoT comes with a selection of builtin task implementations for a range of common application tasks. Each task uses the standard Zephyr or Infuse-IoT API, allowing each task to be re-used across any hardware driver that implements the API.
Battery state sampling
Environmental sensor sampling
GNSS location retrieval
IMU controller (3 or 6 axis)
Wi-Fi Access Point & LTE Cell scanning
Nearby Bluetooth device scanner
Tagged Data Format (TDF) logger