Skip to main content
Version: 1.0.15

LXMASTER API Guide

caution

This page is a narrative guide and may lag behind the latest release. For the authoritative and up-to-date interface, refer to the 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);