Mappings

Mappings connect hardware signals to Task member variables. In Adapnex, the directional operators >> and << establish continuous links between hardware drivers and application tasks. When you connect a driver channel during the setup() phase, the runtime automatically synchronizes the bound variables on every execution cycle.

Quick Start

Connecting inputs and outputs follows the physical direction of signal flow. As a best practice, hardware drivers are instantiated before application control tasks:

#include "adapnex.h"

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

    void Update() override {
        // Run conveyor when part is present
        motor_output = sensor_input;
    }
};

void setup() {
    const auto control_group = Application::CreateCyclicTaskGroup({.period = 10ms});

    // 1. Instantiate hardware drivers first
    const auto io = control_group->CreateTask<CC100IODriver>();

    // 2. Instantiate application control tasks after drivers
    const auto task = control_group->CreateTask<ConveyorTask>();

    // 3. Connect inputs: signal flows from hardware into task variable
    io->DI1 >> task->sensor_input;

    // 4. Connect outputs: signal flows from task variable into hardware
    io->DO1 << task->motor_output;
}

On every cycle, DI1 is sampled from hardware and copied into task→sensor_input. After Update() finishes, task→motor_output is read and written to DO1.

Input Mappings

Hardware drivers expose input channels (such as digital inputs, analog voltage inputs, and temperature sensors) as public InputValueMapping members.

Binding Inputs

Use operator>> to connect a driver input to a Task member variable:

// Digital input binding
io->DI1 >> task->part_detected;

// Analog input binding (e.g. 0 to 10V sensor)
io->AI1 >> task->tank_level;

During each cycle, the driver updates task→part_detected and task→tank_level before Update() is called.

Reading Values Directly

To inspect an input value directly without creating a continuous variable binding, use the value() accessor:

const float current_voltage = io->AI1.value();
Direct access via value() is primarily useful during initialization or diagnostics. For cyclic control logic, continuous bindings with operator>> are recommended.

Output Mappings

Hardware drivers expose controllable channels (such as digital outputs and analog setpoints) as public OutputValueMapping members.

Binding Outputs

Use operator<< to bind a Task member variable to a hardware output channel:

// Digital output binding
io->DO1 << task->run_motor;

// Analog output binding (e.g. speed command)
io->AO1 << task->speed_setpoint;

At the end of each cycle, the driver reads the values of task→run_motor and task→speed_setpoint and transmits them to the physical hardware.

Writing Values Directly

Outputs can also be assigned directly through value():

io->DO1.value() = true;

This is useful in test fixtures, simulation setups, or manual commissioning routines.

Continuous Binding

Unlike standard C++ stream operators that perform an immediate one-time transfer, Adapnex mappings establish permanent connections.

The runtime handles synchronization in two steps during each cycle:

  1. Before Update: The driver reads physical hardware registers and writes the values to any Task variables bound with >>.

  2. After Update: The driver reads the latest values from Task variables bound with << and writes them to the physical hardware.

Because the synchronization happens automatically around Update(), your task logic deals only with standard member variables and remains decoupled from hardware driver details.

Synchronization Across Task Groups

The mapping engine handles both intra-task-group and cross-task-group connections safely:

Within the Same Task Group

When a driver and a Task belong to the same task group, they execute sequentially on a single thread. Variables bound with >> or << use plain C++ types (bool, float, int) without mutexes or atomic overhead.

Across Task Groups

When a driver in a real-time cyclic task group is mapped to a Task in a background freewheeling task group, binding a plain variable could lead to a data race.

To cross thread boundaries safely, Adapnex supports binding directly to std::atomic<T> variables:

class MonitoringTask final : public Task {
public:
    std::atomic<bool> alarm_state = false;

    void Update() override {
        if (alarm_state.load(std::memory_order_relaxed)) {
            // Process alarm in background task group
        }
    }
};

void setup() {
    const auto rt_group = Application::CreateCyclicTaskGroup({.period = 1ms});
    const auto io = rt_group->CreateTask<CC100IODriver>();

    const auto bg_group = Application::CreateFreewheelingTaskGroup(0);
    const auto monitor = bg_group->CreateTask<MonitoringTask>();

    // Thread-safe cross-task-group mapping to atomic variable
    io->DI1 >> monitor->alarm_state;
}

The driver updates the atomic variable using relaxed memory order, ensuring low-latency execution without locking on the cyclic thread.

Variable Lifetime

Mappings store references to the variables they bind. The bound variable should outlive the driver mapping to prevent accessing invalid memory.

Do not bind local variables declared on the stack of setup():

void setup() {
    const auto group = Application::CreateCyclicTaskGroup({.period = 10ms});
    const auto io = group->CreateTask<CC100IODriver>();

    bool local_button = false; // Destroyed when setup() exits
    io->DI1 >> local_button;   // Dangling reference
}

Always bind member variables of Task classes created via TaskGroup::CreateTask<T>(). Because task instances are owned by their task group through std::shared_ptr, their member variables remain valid for the entire duration of the application.