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.

For most applications, development centers on two core elements: the global setup() function and the Update() method of each Task. Additional lifecycle hooks are available for hardware drivers and advanced initialization.

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:

class ConveyorTask final : public Task {
public:
    // Inputs and outputs mapped to physical hardware or other tasks
    bool sensor_input = false;
    bool motor_output = false;

    // Cyclic control logic
    void Update() override {
        motor_output = sensor_input;
    }
};

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.

class ConveyorTask final : public Task {
public:
    bool sensor_input = false;
    bool motor_output = false;

    void Update() override {
        // Cyclic logic runs on every cycle of the task group
        if (sensor_input) {
            motor_output = true;
        } else {
            motor_output = false;
        }
    }
};

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

Setup()

One-time initialization and buffer pre-allocation after setup() returns.

Input Acquisition

PreUpdate()

Cyclic input sampling. Drivers read hardware registers and update input mappings.

Output Actuation

PostUpdate()

Cyclic output dispatch. Drivers flush output mappings to physical devices.

Cleanup

TearDown()

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

The Setup() method is called once per task after the global setup() function has finished.

  • 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 when Update() 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 override PostUpdate().

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 execute TearDown() first. This gives tasks the opportunity to command safe actuator values or de-energize outputs while drivers are still active. When registered first, drivers execute TearDown() 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 as Update().

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:

  1. The cycle currently executing finishes normally, including PostUpdate().

  2. No further update cycles are started.

  3. TearDown() runs for all tasks in reverse registration order.

  4. 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>();
}