Skip to main content
Version: 1.0.12

LXMASTER C++ API Reference

The entire library is consumed through a single umbrella header and the flat lxmaster:: namespace alias:

#include <lxmaster/lxmaster.hpp>

No other headers are needed for application code. Link against lxmaster::lxmaster from CMake:

find_package(lxmaster CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE lxmaster::lxmaster)

NetworkConfig

lxmaster::NetworkConfig holds every knob for a network session. Most programs start from NetworkConfig::defaults(), which reads the per-host environment written by lxmaster host setup, and only override the ENI path:

lxmaster::NetworkConfig cfg = lxmaster::NetworkConfig::defaults();
cfg.eni.eni_path = "network.eni.xml"; // required

defaults() sources the following from the host config (/etc/profile.d/lxmaster-config.sh):

Env varNetworkConfig fieldEffect
LXMASTER_RT_IFACEbus.ifnameEtherCAT NIC name
LXMASTER_RT_CPUrt.cpu_affinityRT thread CPU index
LXMASTER_RT_PRIORITYrt.rt_prioritySCHED_FIFO priority (default 49)
LXMASTER_EC_SYNC_TO_DCdc.ec_sync_to_dcDC alignment gate
LXMASTER_DC_SYNC_KP_DIVdc.dc_sync_kp_divDC-sync PI proportional divisor
LXMASTER_DC_SYNC_KI_DIVdc.dc_sync_ki_divDC-sync PI integral divisor

Sub-config structs

EniConfig — ENI (EtherCAT Network Information) file:

cfg.eni.eni_path = "/path/to/network.eni.xml"; // required; generate with lxmaster eni gen

The cyclic period (bus.cycle_ns) and sync mode (DC/SM-event) are read from the ENI — not from code. There is no code override.

BusConfig — raw network parameters (usually left at defaults):

cfg.bus.ifname = "eth0"; // EtherCAT NIC; normally from LXMASTER_RT_IFACE

RtConfig — real-time scheduling:

cfg.rt.enable_rt_scheduling = true; // set false for dev/testing without root
cfg.rt.rt_priority = 49; // SCHED_FIFO priority
cfg.rt.cpu_affinity = 3; // pin the cyclic thread to CPU 3 (-1 = no pin)

DcConfig — distributed-clock tuning (rarely changed in application code):

cfg.dc.dc_sync_kp_div = 3; // PI proportional divisor (larger = less aggressive)
cfg.dc.dc_sync_ki_div = 20; // PI integral divisor
cfg.dc.dc_lock_warmup_cycles = 200; // cycles before allowing OP entry

DebugConfig — logging:

cfg.debug.enabled = true; // enable logging (off by default)
cfg.debug.log_file = "/tmp/lxm.log"; // empty = stderr/stdout

on_bus_fault — fault notification callback:

cfg.on_bus_fault = [](const lxmaster::BusFault& fault) {
std::cerr << "Bus fault: " << fault.description << "\n";
// Safe to call EcNetwork::stop() from here (runs on a detached helper thread).
};

extra_profile_factories — register a custom device class without the static LXMASTER_REGISTER_DEVICE macro (see Custom device profiles):

cfg.extra_profile_factories.push_back(
ecdev::makeIdentityProfileFactory(
ecdev::DeviceIdentityMatch{kVendorId, kProductCode},
MyProfile::make,
"vendor:my-device"));

EcNetwork

lxmaster::EcNetwork owns the EtherCAT master, the cyclic RT thread, and every device handle. It is non-copyable.

Lifecycle

lxmaster::EcNetwork net(cfg);

One-step bring-up (most programs):

if (!net.start()) {
std::cerr << net.lastError() << "\n";
return 1;
}
// ... run ...
net.stop();

Two-step bring-up — use this when you need to inspect discovered devices and configure them (e.g. choose operating mode) before committing:

// Step 1: load ENI, open NIC, reach PRE_OP, bind devices.
if (!net.prepare()) {
std::cerr << net.lastError() << "\n";
return 1;
}

// axes() / ioModules() / encoders() / devices() are valid here.
for (lxmaster::Axis* ax : net.axes()) {
ax->setDriveMode(lxmaster::DriveOpMode::Csp);
ax->configure(); // raise this axis to OPERATIONAL
}

// Step 2: commit modes, reach OPERATIONAL, spawn the cyclic thread.
if (!net.start()) {
std::cerr << net.lastError() << "\n";
return 1;
}
note

start() calls prepare() automatically when it has not run, so the single-step form is safe. Calling prepare() after start() is an error.

Method reference

MethodDescription
bool prepare()Phase 1: load ENI, open NIC, reach PRE_OP, bind devices. Returns false on failure.
bool start()Phase 2 (calls prepare() if needed): configure DC, reach OP, spawn cyclic thread.
void stop()Stop the cyclic thread and close the master. Safe to call multiple times.
bool isRunning()true while the cyclic thread is live.
std::uint32_t cycleTimeNs()Cyclic period in ns from the ENI. Valid after start().
std::string lastError()Last error string (thread-safe).
std::uint64_t cycleCount()Total RT cycles since start().
void reportDeviceStatus(std::ostream&)Print per-device exit status. Call after stop().

Device accessors

Call these after prepare() or start():

std::vector<lxmaster::Axis*> axes = net.axes();
std::vector<lxmaster::IoModule*> ioModules = net.ioModules();
std::vector<lxmaster::Encoder*> encoders = net.encoders();
std::vector<lxmaster::GenericDevice*> devices = net.devices();

devices() is the complete set of profile-carrying slaves. A device also appears in its typed list (axes(), etc.) when it implements that capability. Use devices() for device classes that fit none of the typed contracts (e.g. an IMU).

Diagnostics

EcNetwork::JitterStats js = net.jitterStats(); // cyclic timing
EcNetwork::DcSyncStats dc = net.dcSyncStats(); // DC alignment

Both are updated throughout the run and finalized at stop().


Axis

lxmaster::Axis is the motion handle — analogous to a TwinCAT NC axis. It wraps a CiA402 profile (or any custom profile implementing IMotionProfile). Use it from the application thread; all methods are RT-safe (lock-free backing).

Operating modes

Set the operating mode between prepare() and start() (or live, if the ENI maps object 0x6060 into the RxPDO):

ax->setDriveMode(lxmaster::DriveOpMode::Csp); // Cyclic Synchronous Position
ax->setDriveMode(lxmaster::DriveOpMode::Csv); // Cyclic Synchronous Velocity
ax->setDriveMode(lxmaster::DriveOpMode::Cst); // Cyclic Synchronous Torque
Enum valueModeCommand method
DriveOpMode::CspCyclic Synchronous PositionmoveTo(counts)
DriveOpMode::CsvCyclic Synchronous VelocitymoveAtVelocity(counts_per_sec)
DriveOpMode::CstCyclic Synchronous TorqueapplyTorque(per_mille_rated)

Commands

ax->enable(); // power up, hold position
ax->disable(); // power down gracefully
ax->resetFault(); // clear a latched fault (one-shot)

ax->moveTo(pos); // CSP: absolute target in encoder counts
ax->moveAtVelocity(vel); // CSV: velocity in counts/s
ax->applyTorque(tm); // CST: torque in per-mille of rated

Status

std::int32_t pos = ax->actualPosition(); // encoder counts
std::int32_t cmd = ax->commandedPosition();
std::int32_t vel = ax->actualVelocity();
std::int32_t torque = ax->actualTorque(); // per-mille; 0 if unmapped
std::int32_t modeDisp = ax->modeDisplay(); // 0x6061 (8=CSP,9=CSV,10=CST)
bool enabled = ax->isEnabled();
bool faulted = ax->isFaulted();
std::uint16_t sw = ax->statusword(); // raw CiA402 statusword
std::string nm = ax->name(); // ENI slave name

Typical CSP pattern

// After net.start():
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::int32_t hold = ax->actualPosition();

while (net.isRunning()) {
ax->moveTo(computeTarget());
std::this_thread::sleep_for(std::chrono::nanoseconds(net.cycleTimeNs()));
}

IoModule

lxmaster::IoModule wraps a digital/analog I/O device (e.g. the LXDIO33-16). All channel indices are zero-based.

Introspection

std::size_t di = mod->digitalInputCount();
std::size_t dout = mod->digitalOutputCount();
std::size_t ai = mod->analogInputCount();
std::size_t aout = mod->analogOutputCount();
std::string nm = mod->name();

Digital I/O

bool state = mod->readDigital(channel); // read a digital input
bool out = mod->digitalOutputState(channel); // read back a digital output
mod->writeDigital(channel, true); // write a digital output

Analog I/O

std::int32_t val = mod->readAnalog(channel); // read an analog input
std::int32_t out = mod->analogOutputState(channel);
mod->writeAnalog(channel, value);

Typical pattern

// After net.prepare(), opt the module into OPERATIONAL:
mod->configure();

// After net.start(), clear all outputs then walk through channels:
for (std::size_t ch = 0; ch < mod->digitalOutputCount(); ++ch)
mod->writeDigital(ch, false);

mod->writeDigital(3, true); // channel 3 high

Encoder

lxmaster::Encoder wraps a CiA 406 encoder device.

std::int32_t pos = enc->position(); // encoder counts
std::int32_t vel = enc->velocity();
std::uint16_t status = enc->status();
std::string nm = enc->name();

GenericDevice

Use lxmaster::GenericDevice for any device that is neither an axis, an I/O module, nor an encoder — for example a custom IMU, gateway, or any vendor device with a bespoke profile.

// After net.prepare():
for (lxmaster::GenericDevice* d : net.devices()) {
ecdev::IDeviceProfile* p = d->deviceProfile();
if (p && std::strcmp(p->profileName(), "vendor:imu") == 0) {
d->configure(); // raise to OPERATIONAL
imu = static_cast<ImuProfile*>(p);
}
}
No RTTI

The cyclic runtime is compiled without RTTI. Never use dynamic_cast on the device path. Guard on profileName() and static_cast exactly once at setup; cache the typed pointer.


Custom device profiles

LXMASTER allows you to bind a custom profile to any slave identified by vendor ID + product code.

Option A — extend a built-in profile

Subclass the built-in CiA402 drive profile to inherit the full motion contract, then add vendor-specific PDO variables:

#include "devices/cia402_drive_profile.hpp"
#include "devices/identity_profile.hpp"
#include "devices/profile_registry.hpp"

class MyDriveProfile : public ecdev::CiA402DriveProfile {
public:
using CiA402DriveProfile::CiA402DriveProfile;
const char* profileName() const override { return "vendor:my-drive"; }

// Override readInputs to snapshot extra vendor PDOs.
void readInputs(const ecdev::ProcessImage& img, bool wkc, bool op) override {
CiA402DriveProfile::readInputs(img, wkc, op); // chain to base first
std::int32_t v = 0;
if (wkc && img.readI32(extra_ref_, &v))
extra_.store(v, std::memory_order_release);
}

std::int32_t extraValue() const noexcept {
return extra_.load(std::memory_order_acquire);
}

static std::unique_ptr<ecdev::IDeviceProfile> make(
const ecdev::ProfileSelectionInput& in) {
ecdev::CiA402DriveProfile::Config cfg;
cfg.op_mode = in.op_mode;
return std::make_unique<MyDriveProfile>(cfg);
}

private:
ecdev::PdoEntryRef extra_ref_{};
std::atomic<std::int32_t> extra_{0};
};

Register with the network config:

cfg.extra_profile_factories.push_back(
ecdev::makeIdentityProfileFactory(
ecdev::DeviceIdentityMatch{kVendorId, kProductCode},
MyDriveProfile::make,
"cia402:my-drive"));

Because MyDriveProfile inherits asMotion(), the Axis facade keeps working. Access the vendor extension through axis->deviceProfile():

ecdev::IDeviceProfile* p = drive->deviceProfile();
if (p && std::strcmp(p->profileName(), "vendor:my-drive") == 0)
auto* mine = static_cast<MyDriveProfile*>(p);

Option B — brand-new device class

For a device that fits none of the typed contracts (e.g. an IMU), implement ecdev::IDeviceProfile directly:

#include "devices/device_profile.hpp"
#include "devices/process_image.hpp"
#include "devices/slave_services.hpp"

class ImuProfile : public ecdev::IDeviceProfile {
public:
const char* profileName() const override { return "vendor:imu"; }

// Called once at PRE_OP: resolve PDO object references.
std::string configurePreOp(ecdev::ISlaveServices& svc,
ecdev::ProcessImage& img) override {
(void)svc;
accel_ref_[0] = img.resolve(0x6000, 0x01);
accel_ref_[1] = img.resolve(0x6000, 0x02);
accel_ref_[2] = img.resolve(0x6000, 0x03);
// Return non-empty string to abort bring-up with that reason.
return {};
}

// Called every RT cycle: copy PDO words into lock-free atomics.
void readInputs(const ecdev::ProcessImage& img, bool wkc, bool) override {
if (!wkc) return;
for (int i = 0; i < 3; ++i) {
std::int16_t v = 0;
if (img.readI16(accel_ref_[i], &v))
accel_[i].store(v, std::memory_order_release);
}
}

std::int16_t accel(int axis) const noexcept {
return accel_[axis].load(std::memory_order_acquire);
}

static std::unique_ptr<ecdev::IDeviceProfile> make(
const ecdev::ProfileSelectionInput&) {
return std::make_unique<ImuProfile>();
}

private:
ecdev::PdoEntryRef accel_ref_[3]{};
std::atomic<std::int16_t> accel_[3]{};
};

Register and use via GenericDevice:

cfg.extra_profile_factories.push_back(
ecdev::makeIdentityProfileFactory(
ecdev::DeviceIdentityMatch{kImuVendorId, kImuProductCode},
ImuProfile::make, "vendor:imu"));

lxmaster::EcNetwork net(cfg);
net.prepare();

ImuProfile* imu = nullptr;
lxmaster::GenericDevice* imu_dev = nullptr;
for (lxmaster::GenericDevice* d : net.devices()) {
auto* p = d->deviceProfile();
if (p && std::strcmp(p->profileName(), "vendor:imu") == 0) {
imu_dev = d;
imu = static_cast<ImuProfile*>(p);
}
}
imu_dev->configure(); // raise to OPERATIONAL
net.start();

// Application thread:
std::cout << imu->accel(0) << "\n";

IDeviceProfile method reference

MethodThreadDescription
profileName()anyUnique stable identifier string.
resolveTopology(image)bindResolve PDO topology from ENI geometry (before live bus). Default: no-op.
configurePreOp(svc, img)bring-upSDO setup + PDO reference resolution. Non-empty return aborts.
prepareSafeOp(svc, img)bring-upAdditional SAFE_OP preparation.
writeOutputs(img, cycle)RTWrite output PDO values each cycle.
readInputs(img, wkc, op)RTSnapshot input PDO values each cycle.
primeOutputs(img)bring-upPrime outputs before first SAFE_OP exchange.
captureExitDiagnostics(svc)shutdownSDO diagnostics after RT thread joins.

DeviceFacade base (shared by all handles)

All device handles (Axis, IoModule, Encoder, GenericDevice) share:

const std::string& name() // ENI slave name
void configure() // raise to OPERATIONAL (call between prepare/start)
void configure(ecdev::BringupState) // raise to a specific ESM state
ecdev::IDeviceProfile* deviceProfile() // underlying profile; nullptr for passive slaves

Not calling configure() leaves a device at PRE_OP (passive; not cyclic).


Error handling pattern

Every start() and prepare() call follows the same pattern:

if (!net.start()) {
std::cerr << "Failed: " << net.lastError() << "\n";
return 1;
}

lastError() is thread-safe and is also written when the cyclic watchdog trips during a run. Check net.isRunning() in the control loop; if it returns false mid-run, read net.lastError() to find out why, then call net.stop().

while (net.isRunning() && !interrupted) {
// ... command axes ...
}

if (!net.isRunning() && !interrupted) {
std::cerr << "Network stopped: " << net.lastError() << "\n";
}
net.stop();
net.reportDeviceStatus(std::cerr);