Application Lifecycle
Every Adapnex application follows a structured lifecycle designed for predictable real-time execution, safe hardware
initialization, and orderly shutdown. Rather than running procedural code in a traditional main() function, an
Adapnex application configures its components during an initial setup phase and hands execution over to the runtime
engine.
Setup Function
The global setup() function is the application entry point. It runs once when the process starts.
During setup(), you define the architecture of your application by creating task groups, instantiating
hardware drivers and tasks, and wiring input and output mappings:
#include "adapnex.h"
void setup() {
// 1. Create a cyclic task group running at a 10ms interval
const auto control_group = Application::CreateCyclicTaskGroup({.period = 10ms});
// 2. Instantiate hardware drivers first
const auto io_driver = control_group->CreateTask<CC100IODriver>();
// 3. Instantiate application control tasks after drivers
const auto main_task = control_group->CreateTask<MainTask>();
// 4. Connect hardware channels to task variables
io_driver->DI1 >> main_task->sensor_input;
io_driver->DO1 << main_task->actuator_output;
}
Once setup() completes, the Adapnex runtime initializes all registered task groups and starts their
execution threads.
Defining Custom Tasks
The Task class is the foundational building block for control algorithms and hardware drivers in Adapnex.
A task bundles inputs, outputs, internal state, and lifecycle hooks into an object-oriented C++ class.
To create your own automation logic, derive a class from Task and override its lifecycle methods:
The runtime manages task instances through task groups. Rather than creating standalone instances directly, you register
tasks with a task group factory method during the setup() phase.
Cyclic Execution with Update
The Update() method is the primary execution function for every Task. It is the
only pure virtual method on Task and contains the cyclic logic of your derived class.
In a cyclic task group, Update() is called once per cycle period (for example, every 10ms). In a
freewheeling task group, it runs repeatedly in a continuous loop.
Recommendations for Cyclic Execution
Code executed inside Update() on a real-time cyclic thread should remain predictable and low latency:
-
Pre-allocate memory during initialization to avoid dynamic allocations during cycles.
-
Avoid blocking system calls, synchronous disk operations, or blocking network reads on the cyclic path.
-
Invoke standard function blocks such as timers and triggers on every cycle (for example inside
Update()or helper methods called by it) so their internal state updates consistently.
Advanced Lifecycle Hooks
For typical control applications, implementing Update() is sufficient. For hardware drivers or
specialized tasks that require dedicated initialization and cleanup phases, Task provides four
additional virtual hooks:
| Hook | Method | Primary Purpose |
|---|---|---|
Initialization |
One-time initialization and buffer pre-allocation after |
|
Input Acquisition |
Cyclic input sampling. Drivers read hardware registers and update input mappings. |
|
Output Actuation |
Cyclic output dispatch. Drivers flush output mappings to physical devices. |
|
Cleanup |
Orderly shutdown. Tasks command safe states and drivers release hardware handles. |
These four methods have empty default implementations, so you only override the ones your Task needs.
Setup Hook
-
Execution Order: Setup follows a recursive registration order. The application iterates through each task group in the order it was created, and each task group invokes
Setup()on all of its registered tasks in the order they were added. -
Recommended Use: Pre-allocating fixed-size data structures, opening local configuration files, and initializing hardware interfaces.
-
Driver Role: Hardware drivers use this phase to establish communication over fieldbuses or backplane buses (such as CAN, Modbus, or modular slice backplanes).
PreUpdate Hook
The PreUpdate() method runs at the start of every execution cycle, before any
Update() method in that task group is called.
-
Primary Use: Primarily utilized by input drivers. Drivers read raw peripheral registers, sample ADC values, or process incoming bus frames, and update their input mappings.
-
Application Tasks: Standard control tasks rarely need to override
PreUpdate()because mapped inputs are already fresh whenUpdate()runs.
PostUpdate Hook
The PostUpdate() method runs at the end of each cycle, after all tasks in the task group have
completed their Update() calls.
-
Primary Use: Primarily utilized by output drivers. Drivers read mapped output variables and transmit updated commands to physical actuators, digital outputs, or fieldbus devices.
-
Application Tasks: Tasks producing values write to their member variables during
Update(). They do not need to overridePostUpdate().
TearDown Hook
When an application receives a stop request, the active cycle runs to completion, including
PostUpdate(). Then, TearDown() is called once for each
Task before the task group thread exits.
-
Reverse Execution Order & Registration:
TearDown()executes tasks in reverse registration order. Because tasks are torn down in the opposite order of their creation, registering hardware drivers before application tasks ensures that control tasks executeTearDown()first. This gives tasks the opportunity to command safe actuator values or de-energize outputs while drivers are still active. When registered first, drivers executeTearDown()after application tasks, allowing them to transmit those final safe values to physical hardware before shutting down and releasing device handles. -
Thread Affinity:
TearDown()executes on the same thread asUpdate().
Application Shutdown
Applications can be stopped programmatically or by receiving operating system signals such as SIGINT and SIGTERM.
Requesting Shutdown
To stop an application programmatically, call Application::Stop():
if (batch_finished) {
Application::Stop();
}
Application::Stop() is thread-safe and can be called from any Task or
background thread. Repeated calls have no additional effect. When requested:
-
The cycle currently executing finishes normally, including
PostUpdate(). -
No further update cycles are started.
-
TearDown()runs for all tasks in reverse registration order. -
The application exits once all task groups have finished.
Polling Shutdown Status
Tasks that contain their own internal loops or long-running computations should periodically check
Application::IsStopping():
void Update() override {
while (has_work() && !Application::IsStopping()) {
process_item();
}
}
Returning promptly from Update() when IsStopping() is true
allows the application shutdown sequence to proceed without delay.
Unblocking Tasks
If a background task is waiting inside a synchronous call, TearDown() cannot execute until that
call unblocks. You can register a stop handler using Application::AddStopHandler():
void Setup() override {
Application::AddStopHandler([this] {
socket->Close();
});
}
Stop handlers run immediately when shutdown begins, before TearDown(), executing in reverse
order of registration.
Complete Example
The following example demonstrates a task implementing the lifecycle hooks:
#include "adapnex.h"
#include <iostream>
class LifecycleDemoTask final : public Task {
public:
bool emergency_stop = false;
protected:
void Setup() override {
std::cout << "[Setup] Preparing task resources..." << std::endl;
}
void PreUpdate() override {
// Typically implemented by input drivers
}
void Update() override {
if (emergency_stop) {
std::cout << "[Update] Stop requested. Halting application." << std::endl;
Application::Stop();
return;
}
}
void PostUpdate() override {
// Typically implemented by output drivers
}
void TearDown() override {
std::cout << "[TearDown] Driving actuators to safe state..." << std::endl;
}
};
void setup() {
const auto group = Application::CreateCyclicTaskGroup({.period = 100ms});
group->CreateTask<LifecycleDemoTask>();
}