Working with Function Blocks

The Adapnex SDK adopts the Function Block pattern from the IEC 61131-3 industrial standard. Unlike standard C++ functions which are stateless, Function Blocks maintain internal memory (state) across execution cycles.

To implement this pattern in C++, we utilize Functors (Function Objects). These are C++ classes that overload the function call operator operator(), allowing instances to be invoked as if they were functions.

The Cyclic Update Pattern

In industrial control, logic is executed cyclically. Your application’s Update() loop runs repeatedly (e.g., every 10ms). Function blocks like Timers and Triggers must be called every cycle to update their internal clocks and state machines.

Comparison: IEC 61131-3 vs. C++

The Adapnex C++ syntax is designed to be similar to Structured Text (ST) syntax. Below is a comparison showing how a simple "Motor Delay" program is structured in both languages.

IEC 61131-3 (Structured Text) Adapnex C++
PROGRAM Main
    VAR
        MyTimer : TON;
        xStart : BOOL := FALSE;
        xMotor : BOOL := FALSE;
    END_VAR

    // Cyclic Code
    MyTimer(IN := xStart, PT := T#2s);
    xMotor := MyTimer.Q;

END_PROGRAM
class MainTask : public Task {
public:
    TON my_timer;
    bool start = false;
    bool motor = false;

    // Cyclic Code
    void Update() override {
        my_timer(start, 2s);
        motor = my_timer.Q;
    }
};

Passing Parameters

Function Blocks in Adapnex are flexible. You can interact with them using concise function arguments or by explicitly setting member variables.

Style 1: The Functor Argument (Compact)

Pass inputs and outputs directly into the function call. This is the most common pattern for logic where values change frequently.

  • Inputs are passed by value.

  • Outputs are passed by reference.

// Update logic AND read the result into 'motor_on' in one line
timer(start_signal, 5s, motor_on);

// Update logic AND read result + elapsed time
timer(start_signal, 5s, motor_on, elapsed_time);

Style 2: Member Access (Explicit)

Alternatively, you can set input members manually before calling the block, and read output members afterwards. This is useful for fixed configuration parameters (like Hysteresis thresholds) that do not change every cycle, keeping your Update() calls cleaner.

class TempControl : public Task {
    Hysteresis<float> temp_switch;

public:
    TempControl() {
        // 1. Set static configuration in Constructor
        temp_switch.SET = 30.0f;
        temp_switch.RESET = 25.0f;
    }

    void Update() override {
        // 2. Set dynamic inputs
        temp_switch.IN = current_temp;

        // 3. Update internal state (using the pre-set members)
        temp_switch();

        // 4. Read Output
        if (temp_switch.OUT) {
            enable_fan();
        }
    }
};

Critical: Scope & Lifetime

A Function Block must remember its past. Therefore, you cannot declare a Function Block as a local variable inside your Update() loop.

If you declare a block locally, it is created and destroyed within a single execution cycle. Its internal state is lost, and the timer will never increment beyond 0.

Incorrect (Local Scope)
void Update() {
    // WRONG! A new timer is created every cycle.
    // It starts at 0ms, counts a tiny amount, and is destroyed.
    TON timer;
    timer(start, 5s, out);
}
Correct (Class Member Scope)
class MyTask : public Task {
    // CORRECT. The object persists for the lifetime of the task.
    TON timer;

    void Update() {
        // The timer remembers the state from the previous call.
        timer(start, 5s, out);
    }
};