APIs, concepts, guides, and more
User Limits

Implement User Limits to configure and trigger actions based on specific input conditions in real-time.

🔹 What are User Limits?

User Limits are a functionality through which users can configure a comparison between inputs, so an output or an interrupt can be triggered when the comparison happens.

A User Limit will evaluate conditions set by the user. These conditions will be processed in the firmware of the RMP. When these conditions are met, the User Limit will generate an interrupt and can also output 32-bit word to any location in the MotionController firmware.

Each User Limit supports up to two input conditions and one output configuration block.

🔹 When to use User Limits?

User Limits are best suited for real-time logic processing that must happen in RMP firmware, not in the user application.

  1. When you want to generate an action based on a digital input state change.
  2. When you want to generate an action based on two different input state changes.
  3. When you want to generate an action based on a specific encoder/axis position.
  4. When you want to trigger a digital output when an Axis’ Actual Position is exceeded.

Where output actions are:

  • Axis ESTOP
  • Axis STOP
  • Axis ABORT
  • Digital Output State Change
  • Proportional Gain Change
  • Monitor Different Variables

🔹 Use cases

High-Speed Sorter Machine

The client has a high-speed conveyor machine that sorts and packages seed packets. They needed to increase the throughput of their machine but were limited by their PLC because the I/O latency was too high. High-speed sorting and packaging machines require high-speed and deterministic IO. Using User Limits, the client was able to set up a comparison of the conveyor position and the location of the packet that is monitored in controller firmware. As soon as the seed packet reached the diverter position, the User Limit comparison became true and fired the “extend” digital output within 1 EtherCAT sample (this system sample = 1 ms) to extend the appropriate actuator and divert the packet into its box.

🔹 RapidCode API Usage

Parameter Definition
int number Enter the user limit number that will be used. Starts from 0 and goes to (UserLimitCountGet – 1.
int conditionNumber Enter 0 or 1 to define the number of input conditions to be processed. Maximum of two input conditions can be combined with AND or OR logic. ConditionNumber would be set to 0 to compare one input bit. ConditionNumber would be set to 1 to compare two input bits.
RSIUserLimitLogic logic Represents logic that user will need to select to select how an input value is compared. See Enumerations below for valid options. Ex: user wants to compare an actual position to see when actual position is greater than 2000 counts. User would have to enter RSIUserLimitLogicGT
ulong address User needs to enter the address of the input. User can call Axis.AddressGet(), MotionController.AddressGet(), or MotionController.NetworkInputAddressGet() to get the address of a particular input.
int mask Decide the bits in an address which need to be used when comparing. Use 0xFFFFFFFF when all bits should be considered. (only applicable when the Condition’s DataType is a 32-bit integer or bitmask)
int limitValue The value to be compared which needs to be set here.
Warning
This method should be called first. UserLimitConditionSet() configures user limits to evaluate an input bit. It sets a condition for when the user limit should trigger.
To track more complex input bit addresses, user can get the address from VM3 or contact us.
Note
A user wants to compare the input condition of only a single axis position. If user intends on tracking two input conditions, call the function twice using 0 for the first condition and 1 for the 2nd condition.
If the user wants the User Limit to trigger when axis-0 position is greater than 2000 counts, then 2000 would be the limitValue.
For a Digital Input Slice I/O, if the 4th bit is changing then the user would simply enter: 0x00010000.

UserLimitConfigSet()

UserLimitConfigSet(int number, RSIUserLimitTriggerType triggerType, RSIAction action, int actionAxis, double duration)
Parameter Definition
int number Enter the user limit number that you are setting up. Starts from 0 and goes to (UserLimitCountGet – 1).
RSIUserLimitTriggerType triggerType Choose the how the condition<em>(s) should be evaluated.
RSIAction action Choose the action you want to cause when the User Limit triggers.
int actionAxis Select the axis that the action (defined above) will occur on.
double duration Enter the time delay (in seconds) before the action is executed after the User Limit has triggered. Resolution of the duration is sample periods (default is 1ms).
Warning
Call this method after UserLimitConditionSet(). This method configures a User Limit and enables it.

UserLimitOutputSet

UserLimitOutputSet(int number, int andMask, int orMask, ulong outputPtr, bool enabled)
Parameter Definition
int number Enter the user limit number that you want to configure to trigger an output. Starts from 0 and goes to (UserLimitCountGet – 1).
int andMask This is a 32-bit AND mask. andMask is always computed before orMask. Bit mask will be AND-ed with the data pointed by the ulong outputPtr.
int orMask This is a 32-bit OR mask. Bit mask will be OR-ed with the result of (andMask & outputPtr).
ulong outputPtr Pointer to any location in the motion controller firmware. This is a host address, like the values returned from AddressGet(...) methods.
bool enabled If TRUE, the output AND-ing and OR-ing will be executed when the User Limit triggers.
Warning
Call this method after UserLimitConfigSet(), but only if you want to configure an output to be triggered. The output will only be triggered when the User Limit is triggered.

🔹 Other User Limit Info

  • It is important to know that our API reserves a user limit per every axis.
  • The RMP controller has up to 2048 User Limit objects.
  • UserLimitCountSet should be done after either setting AxisCountSet and/or starting the network. (Internally, RapidCode Axis objects use UserLimits.) UserLimitCountSet must be called before creating an Axis or MultiAxis object with MotionController::AxisGet() or MotionController::MultiAxisGet().
  • User Limits are “zero” ordinate when accessing them.

🔹 FAQ’s

  1. Are User Limits monitored cyclically like PLC?
    Yes, User Limit objects are running inside the real-time motion firmware and they are cyclic (processed every cycle).

📜 Sample Code

Digital Input with 1 Condition

Trigger: Digital Input Conditions: 1

This sample code is done in AKD Drive with one Actual axis. There are a lots of available/free firmware address. Some are suggested in comment. Available/free firmware address can be found using vm3 as long as there is no label on address, it can be used.

  • C#

    // Constants
    const int INPUT_INDEX = 0; // This is the index of the digital input you will use to trigger the user limit.
    const int OUTPUT_INDEX = 1; // This is the index of the digital output that will go active when the user limit triggers.
    controller.AxisCountSet(1); // The number of user limits must not be greater than the number of axis and must be set before the number of user limits
    controller.UserLimitCountSet(1); // Set the amount of UserLimits that you want to use. (every connected axis will automatically get 1 user limit)
    controller.InterruptEnableSet(true); // Enable User Limit Interrupts. (When a user limit input comparison is met, and interrupt is triggered)
    UInt64 userBufferAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER, 0); // Grabing an memory address to store a simulated IO point.
    //Instead of using physical IO we will use simulated IO using a memory address as simulated IO. See the IO sample apps to see how to use a physical IO instead.
    IOPoint input0 = IOPoint.CreateDigitalInput(controller, userBufferAddress, INPUT_INDEX); // Create a simulated IOpoint using userBuffer memory.
    IOPoint output0 = IOPoint.CreateDigitalOutput(controller, userBufferAddress, OUTPUT_INDEX); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    //-------- PARAMETERS FOR UserLimitConditionSet --------
    int userLimitNumber = 0; // Specify which user limit to use.
    int condition = 0; // Specify which condition to use (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    RSIUserLimitLogic logic = RSIUserLimitLogic.RSIUserLimitLogicEQ; // Logic for input value comparison.
    UInt64 inputAddress = input0.AddressGet(); // Use IOPoint or manually set using controller.NetworkInputAddressGet(INPUT_INDEX); for Beckhoff 10 was the index of 1st input. (To check your IO indexes go to RapidSetup -.
    uint test = (uint)input0.MaskGet();
    uint inputMask = (uint)input0.MaskGet(); // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    uint limtValue = (uint)input0.MaskGet(); // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    // [1] Configure the input's trigger condition.
    controller.UserLimitConditionSet(userLimitNumber,// (User Limit Index) - Specify which user limit to use
    condition, // (Condition Number) - Specify how many inputs you want to compare.
    logic, // (Comparison Logic) - Specify the how the input value(s) will be compared
    inputAddress, // (Input Address) - Specify the address of the input that will be compared.
    inputMask, // (Input Mask) - Specify the bits in an address which need to be used when comparing inputs.
    limtValue); // (Limit Value) - Specify the value to be compared with.
    //-------- PARAMETERS FOR UserLimitConfigSet --------
    RSIUserLimitTriggerType triggerType = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION;
    RSIAction action = RSIAction.RSIActionNONE;
    int axisNumber = 0;
    int duration = 0;
    // [2] Configure and Enable the user limit.
    controller.UserLimitConfigSet(userLimitNumber,// (User Limit Index) - Specify which user limit to use.
    triggerType, // (Trigger Type) - Specify how your condition should be evalutated.
    action, // (User Limit Action) - Specify the action you want to cause on the axis when the user limit triggers.
    axisNumber, // (Current Axis) - Specify the axis that the action (defined above) will occur on.
    duration); // (Output Timer) - Specify the time delay before the action is executed after the User Limit has triggered.
    //-------- PARAMETERS FOR UserLimitOutputSet --------
    uint andMask = (uint)output0.MaskGet(); // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    uint orMask = (uint)output0.MaskGet(); // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    UInt64 outputAddress = output0.AddressGet(); //Alternatively set manually using: controller.NetworkOutputAddressGet(OUTPUT_INDEX);
    bool enableOutput = true;
    // [3] Configure what the output will be. (Call this method after UserLimitConfigSet if you want an output to be triggered) (The output will only be triggered if the input conditions are TRUE.)
    controller.UserLimitOutputSet(userLimitNumber,// (User Limit Index) - Specify which user limit to use.
    andMask, // (Logic AND Mask) - Specify the value that the digital output will be AND-ed with.
    orMask, // (Logic OR Mask) - Specify the value that the digital output will be OR-ed with.
    outputAddress, // (Output Address) - Specify the digital output address.
    enableOutput); // (Enable Output Set) - If TRUE, the output AND-ing and OR-ing will be executed when the User Limit triggers.
    while (controller.InterruptWait(250) != RSIEventType.RSIEventTypeUSER_LIMIT) // Loop will execute every 250 ms
    {
    Assert.That(output0.Get(), Is.False, "We expect the output to NOT be triggered");
    controller.MemorySet(input0.AddressGet(), 1);
    }
    Assert.That(output0.Get(), Is.True, "We expect the output to be triggered");
    controller.UserLimitDisable(userLimitNumber); // Disable User Limit.
    output0.Set(false); // Disable User Limit.

  • C++

    /* CONSTANTS */
    // *NOTICE* The following constants must be configured before attempting to run with hardware.
    // Axis configuration parameters
    const int AXIS_COUNT = 1; // Specify how many axes we will be using in this sample.
    const int AXIS_NUMBER = 0; // Specify which axis/motor we will be controlling.
    const double USER_UNITS = 1048576; // Specify your counts per unit / user units. (the motor used in this example has 1048576 encoder pulses per revolution)
    const int IO_NODE_NUMBER = 0; // which SqNode to use for I/O?
    const int USER_LIMIT = 0; // which user limit to use?
    const int CONDITION = 0; // which condition to use (0 or 1)
    const int INPUT_BIT_NUMBER = 0; // which input bit?
    // To run with hardware, set the USE_HARDWARE flag to true AFTER you have configured the parameters above and taken proper safety precautions.
    USE_HARDWARE = false;
    /* SAMPLE APP BODY */
    // Initialize MotionController class.
    MotionController* controller = MotionController::CreateFromSoftware();
    // Setup the controller for the appropriate hardware configuration.
    if (USE_HARDWARE)
    {
    }
    else
    {
    std::cout << "This sample app cannot be run without hardware." << std::endl;
    return 0;
    }
    // RapidCode interface classes
    IO* io;
    IOPoint* digitalInput;
    try
    {
    // Create a new User Limit
    controller->UserLimitCountSet(1);
    // initialize the I/O class
    io = controller->IOGet(IO_NODE_NUMBER);
    digitalInput = IOPoint::CreateDigitalInput(io, INPUT_BIT_NUMBER);
    auto addr = digitalInput->AddressGet();
    auto mask1 = digitalInput->MaskGet();
    auto mask2 = digitalInput->MaskGet();
    // configure user limit to evaluate input bit
    controller->UserLimitConditionSet(USER_LIMIT,
    CONDITION,
    RSIUserLimitLogic::RSIUserLimitLogicEQ,
    digitalInput->AddressGet(),
    digitalInput->MaskGet(),
    digitalInput->MaskGet());
    // enable the user limit, generate ESTOP_ABORT action when input is turned on
    controller->UserLimitConfigSet(USER_LIMIT, RSIUserLimitTriggerType::RSIUserLimitTriggerTypeSINGLE_CONDITION, RSIAction::RSIActionE_STOP_ABORT, AXIS_NUMBER, 0.0);
    printf("Waiting for the input bit to go high...\n");
    printf("\nPress Any Key To Exit.\n");
    // wait for user limit to trigger
    while (controller->OS->KeyGet((int32_t)RSIWait::RSIWaitPOLL) < 0)
    {
    printf("User Limit state is %d\r", controller->UserLimitStateGet(USER_LIMIT));
    controller->OS->Sleep(1);
    }
    // disable User Limit
    controller->UserLimitDisable(USER_LIMIT);
    }
    catch (RsiError const& rsiError)
    {
    printf("Text: %s\n", rsiError.text);
    return -1;
    }
    controller->Delete(); // Delete the controller as the program exits to ensure memory is deallocated in the correct order.
    return 0;

Digital Input with 2 Conditions

Trigger: Digital Input Conditions: 2

This sample code shows how to configure the RMP controller's User Limits to compare an two different input bits to a specific signal (high signal (1) OR low signal (0)). If the (2 conditions) patterns match, then the specified output bit is activated (turns high).
The INPUTS are specified in two different UserLimitConditionSet()
The OUTPUT is specified in UserLimitOutputSet()
In this example Beckhoff IO Terminals (Model EL1088 for Inputs and Model EL2008 for outputs) were used to control the Digital IO signals. Make sure to check the correct digital IO signal indexes of your system in:
RapidSetup -> Tools -> NetworkIO

  • C#

    // Constants
    const int INPUT_INDEX0 = 0; // This is the index of the digital input you will use to trigger the user limit.
    const int INPUT_INDEX1 = 1; // This is the index of the digital input you will use to trigger the user limit.
    const int OUTPUT_INDEX = 2; // This is the index of the digital output that will go active when the user limit triggers.
    controller.AxisCountSet(1); // The number of user limits must not be greater than the number of axis and must be set before the number of user limits
    controller.UserLimitCountSet(1); // Set the amount of UserLimits that you want to use. (every connected axis will automatically get 1 user limit)
    controller.InterruptEnableSet(true); // Enable User Limit Interrupts. (When a user limit input comparison is met, and interrupt is triggered)
    //-------- PARAMETERS FOR 1st and 2nd UserLimitConditionSet --------
    int userLimitNumber = 0; // Specify which user limit to use.
    RSIUserLimitLogic logic = RSIUserLimitLogic.RSIUserLimitLogicEQ; // Logic for input value comparison.
    UInt64 userBufferAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER, 0); // Grabing an memory address to store a simulated IO point.
    //Instead of using physical IO we will use simulated IO using a memory address as simulated IO. See the IO sample apps to see how to use a physical IO instead.
    IOPoint input0 = IOPoint.CreateDigitalInput(controller, userBufferAddress, INPUT_INDEX0); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    IOPoint input1 = IOPoint.CreateDigitalInput(controller, userBufferAddress, INPUT_INDEX1); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    IOPoint output0 = IOPoint.CreateDigitalOutput(controller, userBufferAddress, OUTPUT_INDEX); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    int condition0 = 0; // Specify which condition to use (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    UInt64 input0Address = input0.AddressGet(); // 10 was the index of my 1st input. (To check your IO indexes go to RapidSetup -.
    uint input0Mask = (uint)input0.MaskGet();
    uint limitValue0 = (uint)input0.MaskGet();
    int condition1 = 1; // Specify which condition to use (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    UInt64 input1Address = input1.AddressGet(); // 11 was the index of my 2nd input. (To check your IO indexes go to RapidSetup -.
    uint input1Mask = (uint)input1.MaskGet();
    uint limitValue1 = (uint)input1.MaskGet();
    // [1] Configure the 1st input's trigger condition. (condition 1)
    controller.UserLimitConditionSet(userLimitNumber, // (User Limit Index) - Specify which user limit to use
    condition0, // (Condition Number) - Specify how many inputs you want to compare.
    logic, // (Comparison Logic) - Specify the how the input value(s) will be compared
    input0Address, // (Input Address) - Specify the address of the input that will be compared.
    input0Mask, // (Input Mask) - Specify the bits in an address which need to be used when comparing inputs.
    limitValue0); // (Limit Value) - Specify the value to be compared with.
    // [2] Configure the 2nd input's trigger condition. (condition 2)
    controller.UserLimitConditionSet(userLimitNumber, // (User Limit Index) - Specify which user limit to use
    condition1, // (Condition Number) - Specify how many inputs you want to compare.
    logic, // (Comparison Logic) - Specify the how the input value(s) will be compared
    input1Address, // (Input Address) - Specify the address of the input that will be compared.
    input1Mask, // (Input Mask) - Specify the bits in an address which need to be used when comparing inputs.
    limitValue1); // (Limit Value) - Specify the value to be compared with.
    //-------- PARAMETERS FOR UserLimitConfigSet --------
    RSIUserLimitTriggerType triggerType = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeCONDITION_AND;
    RSIAction action = RSIAction.RSIActionNONE;
    int axis = 0;
    int duration = 0;
    // [3] Configure and Enable the user limit.
    controller.UserLimitConfigSet(userLimitNumber, // (User Limit Index) - Specify which user limit to use.
    triggerType, // (Trigger Type) - Specify how your condition should be evalutated.
    action, // (User Limit Action) - Specify the action you want to cause on the axis when the user limit triggers.
    axis, // (Current Axis) - Specify the axis that the action (defined above) will occur on.
    duration); // (Output Timer) - Specify the time delay before the action is executed after the User Limit has triggered.
    //-------- PARAMETERS FOR UserLimitOutputSet --------
    uint andMask = (uint)output0.MaskGet();
    uint orMask = (uint)output0.MaskGet();
    UInt64 outputAddress = output0.AddressGet();
    bool enableOutput = true;
    // [4] Configure what the output will be. (Call this method after UserLimitConfigSet if you want an output to be triggered) (The output will only be triggered if the input conditions are TRUE.)
    controller.UserLimitOutputSet(userLimitNumber, // (User Limit Index) - Specify which user limit to use.
    andMask, // (Logic AND Mask) - Specify the value that the digital output will be AND-ed with.
    orMask, // (Logic OR Mask) - Specify the value that the digital output will be OR-ed with.
    outputAddress, // (Output Address) - Specify the digital output address.
    enableOutput); // (Enable Output Set) - If TRUE, the output AND-ing and OR-ing will be executed when the User Limit triggers.
    while (controller.InterruptWait(250) != RSIEventType.RSIEventTypeUSER_LIMIT) // Wait for user limit to trigger. Loop will execute every 250 ms
    {
    Assert.That(output0.Get(), Is.False, "We expect the output to NOT be triggered");
    controller.MemorySet(input0.AddressGet(), 1); // Set bit 0 high.
    Assert.That(output0.Get(), Is.False, "We expect the output to NOT be triggered");
    controller.MemorySet(input0.AddressGet(), 2); // Set bit 1 high.
    Assert.That(output0.Get(), Is.False, "We expect the output to NOT be triggered");
    //---TRIGGER---
    controller.MemorySet(input0.AddressGet(), 3); // Set bits 0 and 1 high.
    }

E-Stop and Store Position

Trigger: Digital Input Conditions: 1 Event: EStop

This sample code shows how to configure a RMP controller's User Limit to compare an input bit to a specific signal (high signal (1) OR low signal (0)). If the (1 condition) pattern matches, then the specified input bit has been activated (turned high) and a User limit Event will trigger. In this example we configure a user limit to trigger when our INPUT turns high(1). Once the INPUT turns high(1) then our user limit will command an E-Stop action on the Axis and store the Axis Command Position. The INPUT is specified in UserLimitConditionSet()
The User Limit configuration is done on UserLimitConfigSet()
The specified address to record on User Limit Event is specified in UserLimitInterruptUserDataAddressSet()
The Data from the speficified addres is retrieved by calling InterruptUserDataGet()
In this example Beckhoff IO Terminals (Model EL1088 for Inputs) were used to control the Digital IO signals. Make sure to check the correct digital IO signal indexes of your system in:
RapidSetup -> Tools -> NetworkIO

  • C#

    // Constants
    const int AXIS_INDEX = 0; // This is the index of the axis you will use to command motion.
    const int INPUT_INDEX = 0;
    const int AXIS_COUNT = 1; // Axes on the network.
    const int USER_LIMIT = 0; // Specify which user limit to use.
    const int CONDITION = 0; // Specify which condition to use. (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    const RSIUserLimitLogic LOGIC = RSIUserLimitLogic.RSIUserLimitLogicEQ; // Logic for input value comparison.
    const int LIMIT_VALUE = 1; // The value to be compared which needs to be set here.
    const RSIUserLimitTriggerType TRIGGER_TYPE = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION; // Choose the how the condition (s) should be evaluated.
    const RSIAction ACTION = RSIAction.RSIActionE_STOP_ABORT; // Choose the action you want to cause when the User Limit triggers.
    const double DURATION = 0.125; // Enter the time delay before the action is executed after the User Limit has triggered.
    const int USER_DATA_INDEX = 0;
    // Some Necessary Pre User Limit Configuration
    controller.AxisCountSet(1);
    controller.UserLimitCountSet(1); // Set the amount of UserLimits that you want to use.
    controller.InterruptEnableSet(true); // Enable User Limit Interrupts. (When a user limit input comparison is met, and interrupt is triggered)
    UInt64 userBufferAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER, 0); // Grabing an memory address to store a simulated IO point.
    //Instead of using physical IO we will use simulated IO using a memory address as simulated IO. See the IO sample apps to see how to use a physical IO instead.
    IOPoint input0 = IOPoint.CreateDigitalInput(controller, userBufferAddress, INPUT_INDEX); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    axis = CreateAndReadyAxis(Constants.AXIS_NUMBER); // Initialize your axis object.
    axis.MoveVelocity(10.0, 20.0); // Command a velocity move (Velocity=1.0, Acceleration=10.0).
    // USER LIMIT CONDITION
    controller.UserLimitConditionSet(USER_LIMIT, CONDITION, LOGIC, input0.AddressGet(), (uint)input0.MaskGet(), LIMIT_VALUE); // Set your User Limit Condition (1st step to setting up your user limit)
    // USER LIMIT CONFIGURATION
    controller.UserLimitConfigSet(USER_LIMIT, TRIGGER_TYPE, ACTION, AXIS_INDEX, DURATION); // Set your User Limit Configuration. (2nd step to setting up your user limit)
    // USER LIMIT USER DATA SET
    controller.UserLimitInterruptUserDataAddressSet(USER_LIMIT, // Specify the user limit you want to add User Data for.
    USER_DATA_INDEX, // Specify what user data index you would like to use. (must be a value from 0 to 4)
    axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_POSITION)); // Specify the address of the data value you want to store in your User Data so that you can retrieve it later after the UserLimit limit triggers.
    // WAIT FOR DIGITAL INPUT TO TRIGGER USER LIMIT EVENT.
    while (controller.InterruptWait(250) != RSIEventType.RSIEventTypeUSER_LIMIT) // Wait until your user limit triggers.
    {
    controller.MemorySet(input0.AddressGet(), 1);
    }
    int triggeredUserLimit = controller.InterruptSourceNumberGet() - AXIS_COUNT; // Check that the correct user limit has triggered. (an extra user limit is allocated for each axis)
    double interruptPosition = controller.InterruptUserDataDoubleGet(USER_DATA_INDEX); // Get the data stored in the interrupt's user data you configured.
    Console.WriteLine("TRIGGERED - User Limit Interrupt Position = " + interruptPosition / axis.UserUnitsGet()); // Get the position of the axis when it user limit event triggered.
    controller.UserLimitDisable(USER_LIMIT);

Change Feed Rate

Trigger: Position Conditions: 1 Event: FeedRate

Configure a UserLimit to change the FeedRate when the axis has reached a specified position.

  • C#

    // Constants
    const int USER_LIMIT = 0; // Specify which user limit to use.
    const int USER_LIMIT_COUNT = 1;
    const int AXIS_COUNT = 1;
    const int CONDITION = 0; // Specify which condition to use. (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    const RSIUserLimitLogic LOGIC = RSIUserLimitLogic.RSIUserLimitLogicGT; // Logic for input value comparison.
    const double POSITION_TRIGGER_VALUE = 5; // The value to be compared which needs to be set here.
    const RSIUserLimitTriggerType TRIGGER_TYPE = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION; // Choose the how the condition (s) should be evaluated.
    const RSIAction ACTION = RSIAction.RSIActionNONE; // Choose the action you want to cause when the User Limit triggers.
    const int DURATION = 0; // Enter the time delay before the action is executed after the User Limit has triggered.
    const double DEFAULT_FEED_RATE = 1.0;
    const double DESIRED_FEED_RATE = 2.0;
    // Other Global Variables
    UInt64 feedRateAddress;
    // Some Necessary Pre User Limit Configuration
    controller.AxisCountSet(AXIS_COUNT); // The number of user limits must not be greater than the number of axis and must be set before the number of user limits
    controller.UserLimitCountSet(USER_LIMIT_COUNT); // Set the amount of UserLimits that you want to use.
    axis = CreateAndReadyAxis(Constants.AXIS_NUMBER); // Initialize your axis object.
    axis.FeedRateSet(DEFAULT_FEED_RATE); // Restore FeedRate to default value.
    // USER LIMIT CONDITION
    // We will use command position because we are working with a phantom axis. Actual position can be used for real axis.
    controller.UserLimitConditionSet(USER_LIMIT, CONDITION, LOGIC, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_POSITION), POSITION_TRIGGER_VALUE); // Set your User Limit Condition (1st step to setting up your user limit)
    // USER LIMIT CONFIGURATION
    controller.UserLimitConfigSet(USER_LIMIT, TRIGGER_TYPE, ACTION, axis.NumberGet(), DURATION); // Set your User Limit Configuration. (2nd step to setting up your user limit)
    // USER LIMIT OUTPUT
    feedRateAddress = axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeTARGET_FEEDRATE);
    controller.UserLimitOutputSet(USER_LIMIT, DESIRED_FEED_RATE, feedRateAddress, true);

Position Trigger

Trigger: Position Conditions: 1 Event: Abort

Configure a User Limit to call abort when a specified position is reached.

  • C#

    // Constants
    const int AXIS_COUNT = 1;
    const int USER_LIMIT_COUNT = 1;
    const int USER_LIMIT_NUMBER = 0;
    const double TRIGGER_POSITION = 0.05; // Specify the position where the user limit will trigger.
    const double MOVE_POSITION = 1.0; // We'll move to this position, which must be past the trigger position.
    const int OUTPUT_INDEX = 1; // This is the index of the digital output that will go active when the user limit triggers.
    const int WAIT_FOR_TRIGGER_MILLISECONDS = 100; // we'll wait for the UserLimit interrupt for this long before giving up.
    // Some Necessary Pre User Limit Configuration
    controller.AxisCountSet(AXIS_COUNT); // The number of user limits must not be greater than the number of axis and must be set before the number of user limits
    controller.UserLimitCountSet(USER_LIMIT_COUNT); // Set the amount of UserLimits that you want to use. (every connected axis will automatically get 1 user limit)
    controller.InterruptEnableSet(true); // Enable User Limit Interrupts. (When a user limit input comparison is met, and interrupt is triggered)
    UInt64 userBufferAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER, 0); // Grabbing a memory address to make a simulated IO point.
    IOPoint output0 = IOPoint.CreateDigitalOutput(controller, userBufferAddress, OUTPUT_INDEX); // Create IOPoint object. Can be used to automatically get the address of a specified node and input index
    output0.Set(false); // be sure we are starting with a value of 0 aka off
    axis = CreateAndReadyAxis(Constants.AXIS_NUMBER); // Initialize your axis object.
    //-------- PARAMETERS FOR UserLimitConditionSet -------- // Specify which user limit to use.
    int condition = 0; // Specify which condition to use (0 or 1) ("0" to compare 1 input || "1" to compare 2 inputs)
    double limitValue = TRIGGER_POSITION; // The limit value will be in counts so we multiply our desired position by USER_UNITS
    controller.UserLimitConditionSet(USER_LIMIT_NUMBER, // (User Limit Index) - Specify which user limit to use
    condition, // (Condition Number) - Specify how many inputs you want to compare.
    RSIUserLimitLogic.RSIUserLimitLogicGT, // (Comparison Logic) - Specify the how the input value(s) will be compared
    axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_POSITION),// (Input Address) - Specify the address of the input that will be compared. // FOR A REAL AXIS USE ACTUAL POSITION
    limitValue); // (Limit Value) - Specify the value to be compared with.
    //-------- PARAMETERS FOR UserLimitConfigSet --------
    RSIUserLimitTriggerType triggerType = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION;
    RSIAction action = RSIAction.RSIActionABORT; // Abort move when user limit triggers.
    int duration = 0;
    // [2] Configure and Enable the user limit.
    controller.UserLimitConfigSet(USER_LIMIT_NUMBER, // (User Limit Index) - Specify which user limit to use.
    triggerType, // (Trigger Type) - Specify how your condition should be evalutated.
    action, // (User Limit Action) - Specify the action you want to cause on the axis when the user limit triggers.
    axis.NumberGet(), // (Current Axis) - Specify the axis that the action (defined above) will occur on.
    duration); // (Output Timer) - Specify the time delay before the action is executed after the User Limit has triggered.
    //-------- PARAMETERS FOR UserLimitOutputSet --------
    uint andMask = (uint)output0.MaskGet(); // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    uint orMask = (uint)output0.MaskGet(); ; // Get the appropriate mask from your IOpoint. OR use 0x00010000 for AKD General IO, 1 for Beckhoff IO Terminals
    UInt64 outputAddress = output0.AddressGet(); //Alternatively set manually using: controller.NetworkOutputAddressGet(OUTPUT_INDEX);
    bool enableOutput = true;
    // [3] Configure what the output will be. (Call this method after UserLimitConfigSet if you want an output to be triggered) (The output will only be triggered if the input conditions are TRUE.)
    controller.UserLimitOutputSet(USER_LIMIT_NUMBER, // (User Limit Index) - Specify which user limit to use.
    andMask, // (Logic AND Mask) - Specify the value that the digital output will be AND-ed with.
    orMask, // (Logic OR Mask) - Specify the value that the digital output will be OR-ed with.
    outputAddress, // (Output Address) - Specify the digital output address.
    enableOutput); // (Enable Output Set) - If TRUE, the output AND-ing and OR-ing will be executed when the User Limit triggers.
    Assert.That(output0.Get(), Is.False, "We expect the output to NOT be triggered");
    // TRIGGER
    axis.MoveSCurve(MOVE_POSITION); // Command simple motion past the trigger position.
    RSIEventType interruptType = controller.InterruptWait(WAIT_FOR_TRIGGER_MILLISECONDS);
    if (interruptType == RSIEventType.RSIEventTypeUSER_LIMIT)
    {
    // note - the object number returned for USER_LIMIT interrupts is raw an unscaled and we must add the controller's AxisCount since internally
    // there is one UserLimit allocated per Axis
    Assert.That(controller.InterruptSourceNumberGet(), Is.EqualTo(USER_LIMIT_NUMBER + AXIS_COUNT), "We got a USER_LIMIT interrupt but it was the wrong one.");
    Assert.That(output0.Get(), Is.True, "We expect the output to be turned on when the user limit triggers.");
    }
    else
    {
    Assert.Fail("We expected a USER_LIMIT interrupt when the trigger position was exceeded but instead got " + interruptType.ToString());
    }
    axis.AmpEnableSet(false); // Disable the motor.
    controller.UserLimitDisable(USER_LIMIT_NUMBER); // Disable User Limit.
    output0.Set(false); // Set output low so program can run again

Set Position Triggered by User Limit

Custom

Configure two UserLimits. The first will trigger on a digital input and copy the Axis Actual Position to the UserBuffer and decelerate to zero velocity with TRIGGERED_MODIFY.
The second UserLimit will trigger after the first UserLimit triggers and the Axis gets to IDLE state and it will directly set the command position of an Axis from the position stored in the UserBuffer.

  • C#

    // Constants
    const int AXIS_COUNT = 1;
    const int USER_LIMIT_FIRST = 0; // Specify which user limit to use.
    const int USER_LIMIT_SECOND = 1;
    const int USER_LIMIT_COUNT = 2;
    const RSIUserLimitLogic LOGIC = RSIUserLimitLogic.RSIUserLimitLogicEQ; // Logic for input value comparison.
    const RSIUserLimitTriggerType TRIGGER_TYPE = RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION; // Choose the how the condition (s) should be evaluated.
    const RSIAction ACTION = RSIAction.RSIActionTRIGGERED_MODIFY; // Choose the action you want to cause when the User Limit triggers.
    const int DURATION = 0; // Enter the time delay before the action is executed after the User Limit has triggered.
    const bool ONE_SHOT = true; // if true, User Limit will only trigger ONCE
    // User Limit Interrupt constants
    const int COMMAND_POSITION_INDEX = 0;
    const int ACTUAL_POSITION_INDEX = 1;
    const int TC_COMMAND_POSITION_INDEX = 2;
    const int TC_ACTUAL_POSITION_INDEX = 3;
    public void UserLimitCommandPositionDirectSet()
    {
    // Some Necessary Pre User Limit Configuration
    controller.AxisCountSet(AXIS_COUNT);
    controller.UserLimitCountSet(USER_LIMIT_COUNT); // Set the amount of UserLimits that you want to use.
    axis = CreateAndReadyAxis(Constants.AXIS_NUMBER); // Initialize your axis object.
    // set the triggered modify values to stop very quickly
    axis.TriggeredModifyDecelerationSet(Constants.DECELERATION);
    axis.TriggeredModifyJerkPercentSet(Constants.JERK_PERCENT);
    // USER LIMIT CONDITION 0 (trigger on digital input)
    controller.UserLimitConditionSet(USER_LIMIT_FIRST, 0, LOGIC, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeDIGITAL_INPUTS), 0x400000, 0x400000); // Set your User Limit Condition (1st step to setting up your user limit)
    // controller.UserLimitOutputSet(USER_LIMIT_FIRST, RSIDataType.RSIDataTypeDOUBLE, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeTC_ACTUAL_POSITION), controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER), true);
    controller.UserLimitConfigSet(USER_LIMIT_FIRST, TRIGGER_TYPE, ACTION, axis.NumberGet(), DURATION, ONE_SHOT); // Set your User Limit Configuration. (2nd step to setting up your user limit)
    // CONDITION 0 (wait for first user limit to trigger)
    controller.UserLimitConditionSet(USER_LIMIT_SECOND, 0, RSIUserLimitLogic.RSIUserLimitLogicEQ, controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSERLIMIT_STATUS, USER_LIMIT_FIRST), 1, 1);
    // CONDITION 1 (AND wait for Axis command velcity = 0.0)
    controller.UserLimitConditionSet(USER_LIMIT_SECOND, 1, RSIUserLimitLogic.RSIUserLimitLogicEQ, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_VELOCITY), 0.0);
    // OUTPUT (copy value from UserBuffer to TC.CommandPosition when trigered)
    //controller.UserLimitOutputSet(USER_LIMIT_SECOND, RSIDataType.RSIDataTypeDOUBLE, controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER), axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeTC_COMMAND_POSITION), true);
    controller.UserLimitConfigSet(USER_LIMIT_SECOND, RSIUserLimitTriggerType.RSIUserLimitTriggerTypeCONDITION_AND, RSIAction.RSIActionNONE, 0, 0, ONE_SHOT);
    // get the Axis moving
    axis.ClearFaults();
    axis.AmpEnableSet(true);
    axis.MoveVelocity(Constants.VELOCITY, Constants.ACCELERATION);
    // configure and enable interrupts
    ConfigureUserLimitInterrupts(USER_LIMIT_FIRST);
    ConfigureUserLimitInterrupts(USER_LIMIT_SECOND);
    controller.InterruptEnableSet(true);
    // wait for (and print) interrupts
    WaitForInterrupts();
    }
    public void ConfigureUserLimitInterrupts(int userLimitIndex)
    {
    controller.UserLimitInterruptUserDataAddressSet(userLimitIndex, COMMAND_POSITION_INDEX, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_POSITION));
    controller.UserLimitInterruptUserDataAddressSet(userLimitIndex, ACTUAL_POSITION_INDEX, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeACTUAL_POSITION));
    }
    public void WaitForInterrupts()
    {
    bool done = false;
    int timeout_millseconds = 10;
    while (!done)
    {
    RSIEventType eventType = controller.InterruptWait(timeout_millseconds);
    Console.WriteLine("IRQ: " + eventType.ToString() + " at sample " + controller.InterruptSampleTimeGet());
    switch (eventType)
    {
    case RSIEventType.RSIEventTypeUSER_LIMIT:
    Console.WriteLine("UserLimit " + controller.InterruptSourceNumberGet());
    Console.WriteLine("CmdPos: " + controller.InterruptUserDataDoubleGet(COMMAND_POSITION_INDEX));
    Console.WriteLine("ActPos: " + controller.InterruptUserDataDoubleGet(ACTUAL_POSITION_INDEX));
    Console.WriteLine("TC.CmdPos: " + controller.InterruptUserDataDoubleGet(TC_COMMAND_POSITION_INDEX));
    Console.WriteLine("TC.ActPos: " + controller.InterruptUserDataDoubleGet(TC_ACTUAL_POSITION_INDEX));
    break;
    case RSIEventType.RSIEventTypeTIMEOUT:
    done = true;
    break;
    default:
    break;
    }
    }
    }

User Limit Triggered by Difference in Position of Two Axes

Trigger: Math Block Process Value Conditions: 1 Event: Abort


This sample code demonstrates how to use a MathBlock to calculate the difference of in position between two axes and trigger a UserLimit when the difference exceeds a certain value.

  • C#

    /* CONSTANTS */
    // *NOTICE* The following constants must be configured before attempting to run with hardware.
    // Controller Object Counts
    const int MATHBLOCK_COUNT = 1; // minimum required MathBlocks for this sample
    const int AXIS_COUNT = 2; // minimum required axes for this sample
    const int USER_LIMIT_COUNT = 1; // minimum required user limits for this sample
    // Axis Configuration
    const int FIRST_AXIS_INDEX = 0; // the first axis index
    const int SECOND_AXIS_INDEX = 1; // the second axis index
    const double USER_UNITS = 1048576; // counts per unit (the user units)
    // MathBlock Configuration
    const int MATHBLOCK_INDEX = 0; // the mathblock index
    // User Limit Configuration
    const int USER_LIMIT_INDEX = 0; // the user limit index
    const double MAX_POSITION_DIFFERENCE = 0.5 * USER_UNITS; // the maximum position difference before the user limit triggers
    const RSIAction USER_LIMIT_ACTION = RSIAction.RSIActionABORT; // the action to take when the user limit is triggered
    const int USER_LIMIT_DURATION = 0; // the time delay before the action is executed after the User Limit has triggered
    // Motion Parameters
    const double RELATIVE_POSITION = 2 * MAX_POSITION_DIFFERENCE; // the relative position to move the axes
    const double VELOCITY = 1; // the velocity to move the axes
    const double ACCELERATION = 10; // the acceleration of the axes movement
    const double DECELERATION = 10; // the deceleration of the axes movement
    const double JERK_PCT = 0; // the jerk percentage of the axes movement
    // To run with hardware, set the USE_HARDWARE flag to true AFTER you have configured the parameters above and taken proper safety precautions.
    USE_HARDWARE = false;
    // Determine which axis address type to use based on the USE_HARDWARE flag
    RSIAxisAddressType INPUT_AXIS_ADDRESS_TYPE = (USE_HARDWARE) ?
    (RSIAxisAddressType.RSIAxisAddressTypeACTUAL_POSITION) : (RSIAxisAddressType.RSIAxisAddressTypeCOMMAND_POSITION);
    /* SAMPLE APP BODY */
    // Initialize MotionController class.
    // NOTICE: Replace "rmpPath" with the path location of the RMP.rta (usually the RapidSetup folder)
    // if project directory is different than rapid setup directory.
    // Use a try/catch/finally to ensure that the controller is deleted when done.
    try
    {
    HelperFunctionsCS.CheckErrors(controller); // [Helper Function] Check that the axis has been initialize correctly.
    // Setup the controller for the appropriate hardware configuration.
    if (USE_HARDWARE)
    {
    }
    else
    {
    HelperFunctionsCS.SetupControllerForPhantoms(controller, AXIS_COUNT, new int[] { FIRST_AXIS_INDEX, SECOND_AXIS_INDEX });
    }
    // configure the controller object counts
    controller.MathBlockCountSet(MATHBLOCK_COUNT);
    controller.MotionCountSet(AXIS_COUNT + 1);
    controller.UserLimitCountSet(USER_LIMIT_COUNT);
    // get both axis objects and check for errors
    Axis axis0 = controller.AxisGet(FIRST_AXIS_INDEX);
    axis0.UserUnitsSet(USER_UNITS); // set the user units (counts per unit)
    axis0.PositionSet(0); // set the initial position to 0
    axis0.ErrorLimitActionSet(RSIAction.RSIActionNONE); // Set Error Limit Action.
    Axis axis1 = controller.AxisGet(SECOND_AXIS_INDEX);
    axis1.UserUnitsSet(USER_UNITS); // set the user units (counts per unit)
    axis1.PositionSet(0); // set the initial position to 0
    axis1.ErrorLimitActionSet(RSIAction.RSIActionNONE); // Set Error Limit Action.
    // configure a multiaxis for the two axes
    MultiAxis multiAxis = controller.MultiAxisGet(AXIS_COUNT);
    multiAxis.AxisRemoveAll();
    multiAxis.AxisAdd(axis0);
    multiAxis.AxisAdd(axis1);
    multiAxis.Abort(); // make sure the multiaxis is not moving
    multiAxis.ClearFaults(); // clear any faults
    // read the configuration of the MathBlock
    MotionController.MathBlockConfig mathBlockConfig = controller.MathBlockConfigGet(MATHBLOCK_INDEX);
    // configure the MathBlock to subtract the position of the second axis from the position of the first axis
    mathBlockConfig.InputAddress0 = axis0.AddressGet(INPUT_AXIS_ADDRESS_TYPE);
    mathBlockConfig.InputDataType0 = RSIDataType.RSIDataTypeDOUBLE;
    mathBlockConfig.InputAddress1 = axis1.AddressGet(INPUT_AXIS_ADDRESS_TYPE);
    mathBlockConfig.InputDataType1 = RSIDataType.RSIDataTypeDOUBLE;
    mathBlockConfig.ProcessDataType = RSIDataType.RSIDataTypeDOUBLE;
    mathBlockConfig.Operation = RSIMathBlockOperation.RSIMathBlockOperationSUBTRACT;
    // set the MathBlock configuration
    controller.MathBlockConfigSet(MATHBLOCK_INDEX, mathBlockConfig);
    // wait a sample so we know the RMP is now processing the newly configured MathBlocks
    controller.SampleWait(1);
    Console.WriteLine("MathBlock configured to subtract the position of the second axis from the position of the first axis.");
    // get the address of the MathBlock's ProcessValue to use in the UserLimit
    ulong mathBlockProcessValueAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeMATHBLOCK_PROCESS_VALUE, MATHBLOCK_INDEX);
    // configure the UserLimit to trigger when the absolute position difference is greater than MAX_POSITION_DIFFERENCE
    controller.UserLimitConditionSet(USER_LIMIT_INDEX, 0, RSIUserLimitLogic.RSIUserLimitLogicABS_GT, mathBlockProcessValueAddress, MAX_POSITION_DIFFERENCE);
    // set the UserLimit action to abort motion (Note: since the axes are in a multiaxis, the other axis will also be aborted)
    controller.UserLimitConfigSet(USER_LIMIT_INDEX, RSIUserLimitTriggerType.RSIUserLimitTriggerTypeSINGLE_CONDITION, USER_LIMIT_ACTION, FIRST_AXIS_INDEX, USER_LIMIT_DURATION);
    Console.WriteLine("UserLimit configured to trigger when the absolute position difference is greater than " + MAX_POSITION_DIFFERENCE + " and abort motion.");
    // command motion to trigger the UserLimit
    Console.WriteLine("Moving the axes to trigger the UserLimit...");
    axis0.AmpEnableSet(true); // Enable the motor.
    axis0.MoveRelative(RELATIVE_POSITION, VELOCITY, ACCELERATION, DECELERATION, JERK_PCT); // Move the axis to trigger the UserLimit.
    axis0.MotionDoneWait(); // Wait for the axis to finish moving.
    // disable the motor and the UserLimit
    axis0.AmpEnableSet(false); // Disable the motor.
    controller.UserLimitDisable(USER_LIMIT_INDEX); // Disable User Limit.
    // the motion should have been aborted and both axes should be in an error state
    if (axis0.StateGet().Equals(RSIState.RSIStateERROR) &&
    axis1.StateGet().Equals(RSIState.RSIStateERROR))
    {
    Console.WriteLine("Both axes are in the error state after the UserLimit triggered (This is the intended behavior).");
    return 0;
    }
    else
    {
    Console.WriteLine("Error: The axes should be in an error state after the UserLimit triggers, but they are not.");
    Console.WriteLine("First Axis State: " + axis0.StateGet());
    Console.WriteLine("Second Axis State: " + axis1.StateGet());
    return -1;
    }
    }
    catch (Exception e)
    {
    Console.WriteLine("Error: " + e.Message);
    return -1;
    }
    finally
    {
    controller.Delete(); // Delete the controller object.
    }

  • C++

    /* CONSTANTS */
    // *NOTICE* The following constants must be configured before attempting to run with hardware.
    const int NUM_AXES = 2; // The number of axes to configure
    const int AXIS_X_INDEX = 0; // The index of the first axis
    const int AXIS_Y_INDEX = 1; // The index of the second axis
    // User Limit Configuration
    const double MAX_POSITION_DIFFERENCE = 0.5; // the maximum position difference before the user limit triggers
    const RSIAction USER_LIMIT_ACTION = RSIAction::RSIActionABORT; // the action to take when the user limit is triggered
    const int USER_LIMIT_DURATION = 0; // the time delay before the action is executed after the User Limit has triggered
    // Motion Parameters
    const double RELATIVE_POSITION = 2 * MAX_POSITION_DIFFERENCE; // the relative position to move the axes
    const double VELOCITY = 1; // the velocity to move the axes
    const double ACCELERATION = 10; // the acceleration of the axes movement
    const double DECELERATION = 10; // the deceleration of the axes movement
    const double JERK_PCT = 0; // the jerk percentage of the axes movement
    /* RAPIDCODE INITIALIZATION */
    // Create the controller
    MotionController *controller = MotionController::Create(&params);
    // Variables to store initial object counts to restore later (starts at -1 to indicate it has not been set)
    int initialUserLimitCount = -1;
    int initialMathBlockCount = -1;
    // Set the exit code to an error value.
    int exitCode = -1;
    try // Ensure that the controller is deleted if an error occurs.
    {
    // Prepare the controller as defined in SampleAppsHelper.h depending on the configuration
    SampleAppsHelper::SetupController(controller, NUM_AXES);
    // Save initial object counts to restore later
    initialUserLimitCount = controller->UserLimitCountGet();
    initialMathBlockCount = controller->MathBlockCountGet();
    /* SAMPLE APP BODY */
    /* Configure the controller object counts */
    // Normally you would set the number of axes here, but for samples that is handled in the SampleAppsHelper::SetupController function
    // controller->AxisCountSet(NUM_AXES);
    // Each axis has a motion object associated with it, so we need to add one more motion object than axes
    const int multiAxisIndex = controller->AxisCountGet();
    controller->MotionCountSet(multiAxisIndex + 1);
    const int userLimitIndex = initialUserLimitCount;
    controller->UserLimitCountSet(initialUserLimitCount + 1);
    const int mathBlockIndex = initialMathBlockCount;
    controller->MathBlockCountSet(initialMathBlockCount + 1);
    // Get the axes
    Axis *axisX = controller->AxisGet(AXIS_X_INDEX);
    Axis *axisY = controller->AxisGet(AXIS_Y_INDEX);
    // Configure a multiaxis for the two axes
    MultiAxis *multiAxis = controller->MultiAxisGet(multiAxisIndex);
    multiAxis->AxisRemoveAll();
    multiAxis->AxisAdd(axisX);
    multiAxis->AxisAdd(axisY);
    multiAxis->Abort(); // make sure the multiaxis is not moving
    multiAxis->ClearFaults();
    // Read the configuration of the MathBlock
    MotionController::MathBlockConfig mathBlockConfig = controller->MathBlockConfigGet(mathBlockIndex);
    // Determine which axis address type to use based on the config USE_HARDWARE flag
    RSIAxisAddressType INPUT_AXIS_ADDRESS_TYPE = (SampleAppsConfig::USE_HARDWARE) ? (RSIAxisAddressType::RSIAxisAddressTypeACTUAL_POSITION)
    : (RSIAxisAddressType::RSIAxisAddressTypeCOMMAND_POSITION);
    // Configure the MathBlock to subtract the position of the second axis from the position of the first axis
    mathBlockConfig.InputAddress0 = axisX->AddressGet(INPUT_AXIS_ADDRESS_TYPE);
    mathBlockConfig.InputDataType0 = RSIDataType::RSIDataTypeDOUBLE;
    mathBlockConfig.InputAddress1 = axisY->AddressGet(INPUT_AXIS_ADDRESS_TYPE);
    mathBlockConfig.InputDataType1 = RSIDataType::RSIDataTypeDOUBLE;
    mathBlockConfig.ProcessDataType = RSIDataType::RSIDataTypeDOUBLE;
    mathBlockConfig.Operation = RSIMathBlockOperation::RSIMathBlockOperationSUBTRACT;
    // Set the MathBlock configuration
    controller->MathBlockConfigSet(mathBlockIndex, mathBlockConfig);
    // Wait a sample so we know the RMP is now processing the newly configured MathBlocks
    controller->SampleWait(1);
    std::cout << "MathBlock configured to subtract the position of the second axis from the position of the first axis." << std::endl;
    // Get the address of the MathBlock's ProcessValue to use in the UserLimit
    uint64_t mathBlockProcessValueAddress =
    controller->AddressGet(RSIControllerAddressType::RSIControllerAddressTypeMATHBLOCK_PROCESS_VALUE, mathBlockIndex);
    // Configure the UserLimit to trigger when the absolute position difference is greater than MAX_POSITION_DIFFERENCE
    controller->UserLimitConditionSet(
    userLimitIndex, 0, RSIUserLimitLogic::RSIUserLimitLogicABS_GT, mathBlockProcessValueAddress, MAX_POSITION_DIFFERENCE
    );
    // Set the UserLimit action to abort motion (Note: since the axes are in a multiaxis, the other axis will also be aborted)
    controller->UserLimitConfigSet(
    userLimitIndex, RSIUserLimitTriggerType::RSIUserLimitTriggerTypeSINGLE_CONDITION, USER_LIMIT_ACTION, AXIS_X_INDEX,
    USER_LIMIT_DURATION
    );
    std::cout << "UserLimit configured to trigger when the absolute position difference is greater than " << MAX_POSITION_DIFFERENCE
    << " and abort motion." << std::endl;
    // Command motion to trigger the UserLimit
    std::cout << "Moving the axes to trigger the UserLimit..." << std::endl;
    axisX->AmpEnableSet(true); // Enable the motor.
    axisX->MoveRelative(RELATIVE_POSITION, VELOCITY, ACCELERATION, DECELERATION, JERK_PCT); // Move the axis to trigger the UserLimit.
    axisX->MotionDoneWait();
    // Disable the motor and the UserLimit
    axisX->AmpEnableSet(false); // Disable the motor.
    controller->UserLimitDisable(userLimitIndex);
    // The motion should have been aborted and both axes should be in an error state
    if ((axisX->StateGet() == RSIState::RSIStateERROR) && (axisY->StateGet() == RSIState::RSIStateERROR))
    {
    std::cout << "Both axes are in the error state after the UserLimit triggered (This is the intended behavior)." << std::endl;
    exitCode = 0;
    }
    else
    {
    std::cout << "Error: The axes should be in an error state after the UserLimit triggers, but they are not." << std::endl;
    std::cout << "First Axis State: " << RSIStateMap.at(axisX->StateGet()) << std::endl;
    std::cout << "Second Axis State: " << RSIStateMap.at(axisY->StateGet()) << std::endl;
    exitCode = -1;
    }
    }
    catch (const std::exception &ex)
    {
    std::cerr << ex.what() << std::endl;
    exitCode = -1;
    }
    // Restore the object counts to the original values
    if (initialUserLimitCount != -1) { controller->UserLimitCountSet(initialUserLimitCount); }
    if (initialMathBlockCount != -1) { controller->MathBlockCountSet(initialMathBlockCount); }
    // Note: The axis and motion counts are handled by the SampleAppsHelper::Cleanup function
    // Clean up the controller and any other objects as needed
    // Delete the controller as the program exits to ensure memory is deallocated in the correct order
    controller->Delete();