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.
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:
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
|
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.