APIs, concepts, guides, and more
Recorder

Capture positions, velocities, I/O, and any other controller value sample-exactly in real-time RMP firmware, then collect the records from the non-real-time host by polling or interrupts for tuning, debugging, and analysis.

🔹 What is a Recorder?

A Recorder is a data logger that runs inside the RMP firmware, in real-time. You tell it which values to capture (a list of firmware addresses) and how often to capture them (every N samples). From then on, the firmware copies those values into a buffer in controller memory as part of its deterministic real-time cycle. Every record is sample-exact, with no gaps and no host-side jitter.

Your host application, on the other hand, is not real-time. Windows and Linux thread scheduling makes no timing guarantees, and with a Recorder it never needs any. Because the real-time firmware has already captured every sample into the buffer, the host has no trouble getting data for every single sample: it simply drains the buffer at whatever pace the operating system allows, by polling or by waiting on interrupts.

That split is the whole point: the real-time firmware guarantees perfect capture timing, while the non-real-time host only has to keep up on average and still receives every record.

At a glance
Recorders per controller Up to 128 (RecorderCountMaximum)
Values per record Up to 32 addresses, captured together in the same sample
Record period 0 to 32,767 samples between records (RecorderPeriodSet)
Buffering One-shot (stop when full) or circular (RecorderCircularBufferSet)
Start / stop Manual, or automatic on Axis / MultiAxis motion (RecorderConfigureToTriggerOnMotion)
Collection Host polling, or RECORDER_HIGH / RECORDER_FULL / RECORDER_DONE interrupts

🔹 Why use a Recorder?

Reading values from your application loop works for casual monitoring, but your application is not real-time: the operating system schedules your loop, not the sample clock. Host-side polling skips samples, jitters, and aliases exactly when things get interesting. A Recorder moves the capture into the real-time firmware, which logs every sample deterministically, so even a non-real-time host collects a complete, sample-exact record by draining the buffer at its leisure. That makes the Recorder the right tool whenever timing fidelity matters:

  • Servo tuning: record command and actual position every sample and analyze following error, overshoot, and settling behavior.
  • Event correlation: capture an axis position and a digital input in the same record to determine exactly where the axis was when the input changed state.
  • Performance measurement: record firmware and network timing deltas to characterize a system (see the sample below).
  • Fault forensics: run a circular recorder continuously and inspect the last moments of data after a fault trips.
  • Network health: record sync group state transitions alongside your motion signals (see Sync Groups).

If you want the firmware to act on values rather than record them, you are looking for a different tool:

You want to... Use
Log values every sample for later analysis Recorder (this page)
Trigger an action when a condition is met User Limits
Compute new values in firmware each sample Math Blocks
Latch a position on a hardware input edge Capture

🔹 Recorder Lifecycle

Every Recorder follows the same five steps: allocate, configure, start, collect, stop.

Step 1: Allocate

Firmware ships with zero recorders enabled. Reserve however many you need with RecorderCountSet:

controller.RecorderCountSet(1); // before AxisGet / MultiAxisGet!

Set object counts before creating objects
Like all object count methods, RecorderCountSet must be called after MotionController creation but before creating any other RapidCode object (AxisGet, MultiAxisGet, ...). Changing object counts reconfigures the RMP's dynamic memory allocation, which invalidates preexisting objects.

When code you do not control may also use recorders (RapidSetup's tuning tools do, for example), append rather than assume recorder 0 is free:

int recorderIndex = controller.RecorderCountGet();
controller.RecorderCountSet(recorderIndex + 1);

Recorder 0 shorthand
Every Recorder... method has an overload taking a recorderNumber as its first argument. The overloads without it operate on Recorder 0, which is convenient when your application owns the only recorder.

Step 2: Configure

Tell the recorder how often to record, how many values per record, and the address of each value:

controller.RecorderPeriodSet(1); // samples between records
controller.RecorderCircularBufferSet(false); // one-shot: stop when full
controller.RecorderDataCountSet(2); // two values per record
controller.RecorderDataAddressSet(0,
axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeACTUAL_POSITION));
controller.RecorderDataAddressSet(1, digitalInput.AddressGet());

Anything with a firmware address can be recorded. The usual sources:

Source Method Examples
Axis values AddressGet with RSIAxisAddressType Actual/command position, velocity, position error
Controller values AddressGet with RSIControllerAddressType Sample counter, timing deltas, user buffer, sync group state
I/O points AddressGet Digital/analog inputs and outputs
Network PDO data NetworkInputAddressGet / NetworkOutputAddressGet Any cyclic input or output on the EtherCAT network

Recorded values are raw firmware memory, so you must know each value's type to interpret it. Query it with AddressDataTypeGet (or the equivalent on any RapidCode object) and see the retrieval methods in step 4.

Step 3: Start

Start recording manually with RecorderStart, or arm the recorder to follow motion automatically, as described in Triggering on Motion below. RecorderEnabledGet reports whether a recorder is currently recording.

Step 4: Collect

Poll RecorderRecordCountGet to learn how many records are waiting, retrieve them one at a time (or in bulk), then read each value out of the retrieved record by index:

int recordsAvailable = controller.RecorderRecordCountGet();
for (int i = 0; i < recordsAvailable; i++)
{
controller.RecorderRecordDataRetrieve(); // pull one record
double position =
controller.RecorderRecordDataFirmwareValueGet(0).Double;
int inputs = controller.RecorderRecordDataFirmwareValueGet(1).Int32;
}

Prefer RecorderRecordDataFirmwareValueGet: it returns a FirmwareValue union you can read as the recorded value's actual type (.Double, .Int32, ...), matching what AddressDataTypeGet reported. To move a burst of records host-side in one call, use RecorderRecordDataRetrieveBulk.

For an event-driven alternative to polling, see Collecting via Interrupts below.

Step 5: Stop and Reset

RecorderStop stops recording; RecorderReset restores the recorder's pointers and counters so the next RecorderStart begins a fresh recording. If you appended a temporary recorder, release it by lowering the count again with RecorderCountSet.

🔹 API Summary

Allocate
RecorderCountGet / RecorderCountSet How many recorders the firmware processes. Firmware default is 0.
RecorderBufferSizeGet / RecorderBufferSizeSet Size of a recorder's buffer, in records.
ExternalMemorySizeGet How much controller memory is available for buffers.
Configure
RecorderPeriodSet Samples between records (0 to 32,767).
RecorderDataCountSet Number of values per record (up to 32).
RecorderDataAddressSet / RecorderDataAddressesSet The firmware address recorded at each index / all indexes at once.
RecorderCircularBufferSet true: keep recording (overwriting oldest) when full. false: stop when full.
RecorderBufferHighCountSet Records stored before a RECORDER_HIGH interrupt (1 to 32,767).
Run
RecorderStart / RecorderStop Begin / end recording.
RecorderEnabledGet Whether the recorder is currently recording.
RecorderConfigureToTriggerOnMotion / RecorderTriggerOnMotionGet Arm / query automatic start-stop on motion.
RecorderReset Restore pointers and counters for a fresh recording.
Collect
RecorderRecordCountGet Records stored and ready for reading.
RecorderRecordMaxCountGet Maximum records the buffer can store.
RecorderRecordDataRetrieve / RecorderRecordDataRetrieveBulk Pull the next record (or many) from the buffer.
RecorderRecordDataFirmwareValueGet Read one value from the retrieved record as a typed FirmwareValue.
RecorderRecordDataValueGet / RecorderRecordDataDoubleGet Read one value as a raw int32 / double.

🔹 Triggering on Motion

RecorderConfigureToTriggerOnMotion ties a recorder's start and stop to an Axis or MultiAxis motion supervisor. When a motion starts, the controller starts the recorder automatically; when the motion is done (see settling), the recorder stops and a RECORDER_DONE interrupt fires. This is the easiest way to log every relevant variable for exactly the duration of a move, with no host-side timing required.

🔹 Collecting via Interrupts

Instead of polling for records, sleep in InterruptWait and let the controller tell you when to collect. Enable interrupt delivery with InterruptEnableSet (the RapidCodeInterrupt methods are available on the MotionController), then wait for these events:

RSIEventType Fires when
RECORDER_HIGH The record count reaches the threshold set by RecorderBufferHighCountSet. This is your cue to drain the buffer.
RECORDER_FULL The buffer is full. A non-circular recorder has stopped; a circular one is now overwriting the oldest records.
RECORDER_DONE The recorder stopped (for example, a motion-triggered recording's motion completed).

MotionController interrupts can also latch up to six arbitrary firmware values at the moment the recorder event fires; see InterruptUserDataAddressSet.

🔹 Sizing the Buffer

Recorder buffers live in controller memory and are dynamically allocated. The defaults are fine for most uses; size a larger buffer with RecorderBufferSizeSet when recording is fast and collection is slow:

  • high sample rates and/or every-sample recording periods,
  • many values per record,
  • a slow host, or a host connected via client/server,
  • long recordings that must not drop a single record.

For a recording longer than the memory available, configure a circular buffer with RecorderCircularBufferSet and drain it continuously. The host only has to copy records to RAM faster than the buffer rolls over, which is not an issue on most systems. Check how much controller memory is available with ExternalMemorySizeGet.

📜 Sample Code

Basic Recorder Usage

Record an axis position together with a digital input, then scan the records to find the position at the moment the input triggered.

  • C#

    /* This sample demonstrates how to use the Recorder to track multiple controller parameters.
    Shows how to configure the recorder, record axis position and digital input values,
    and retrieve recorded data to find when specific events occurred.
    */
    using RSI.RapidCode; // RSI.RapidCode.dotNET;
    Console.WriteLine("📜 Recorder");
    int exitCode = 0;
    // set sample config params
    const int VALUES_PER_RECORD = 2; // how many values to store in each record
    const int RECORD_PERIOD_SAMPLES = 1; // how often to record data (samples between consecutive records)
    const int RECORD_TIME = 250; // how long to record (milliseconds)
    const int INPUT_INDEX = 0;
    // get rmp objects
    try
    {
    Helpers.CheckErrors(controller);
    Helpers.VerifyHardwareUsage(controller);
    Helpers.VerifyAxisCount(controller);
    // set recorder count before any RapidCodeObject get/create other than the controller
    controller.RecorderCountSet(1);
    // get axis
    Axis axis = controller.AxisGet(Constants.AXIS_0_INDEX);
    Helpers.CheckErrors(axis);
    // configure phantom axis
    if (!Constants.USE_HARDWARE) Helpers.PhantomAxisReset(axis);
    // create simulated digital input using user buffer memory
    ulong userBufferAddress = controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeUSER_BUFFER, 0);
    IOPoint digitalInput = IOPoint.CreateDigitalInput(controller, userBufferAddress, INPUT_INDEX);
    // stop recorder if already running
    if (controller.RecorderEnabledGet() == true)
    {
    controller.RecorderStop(); // stop recording
    controller.RecorderReset(); // reset controller
    }
    // configure recorder
    controller.RecorderPeriodSet(RECORD_PERIOD_SAMPLES); // record every n samples
    controller.RecorderCircularBufferSet(false); // do not use circular buffer
    controller.RecorderDataCountSet(VALUES_PER_RECORD); // number of values per record
    controller.RecorderDataAddressSet(0, axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeACTUAL_POSITION)); // record axis position
    RSIDataType actualPositionDataType = axis.AddressDataTypeGet(RSIAxisAddressType.RSIAxisAddressTypeACTUAL_POSITION);
    Console.WriteLine($"Axis Actual Position data type: {actualPositionDataType}"); // print actual position type
    controller.RecorderDataAddressSet(1, digitalInput.AddressGet()); // record digital input state
    // start recording
    controller.RecorderStart();
    controller.OS.Sleep(RECORD_TIME / 2); // record for specified time
    controller.MemorySet(digitalInput.AddressGet(), 1 << INPUT_INDEX); // simulate input trigger
    controller.OS.Sleep(RECORD_TIME / 2); // continue recording for specified time
    // retrieve recorded data
    int recordsAvailable = controller.RecorderRecordCountGet();
    Console.WriteLine($"There are {recordsAvailable} records available");
    // process records to find when input triggered
    for (int i = 0; i < recordsAvailable; i++)
    {
    controller.RecorderRecordDataRetrieve(); // retrieve one record
    FirmwareValue positionFirmwareValue = controller.RecorderRecordDataFirmwareValueGet(0); // get axis position value
    double positionRecord = positionFirmwareValue.Double;
    FirmwareValue digitalInputFirmwareValue = controller.RecorderRecordDataFirmwareValueGet(1); // get digital input value
    int digitalInputValue = digitalInputFirmwareValue.Int32;
    // check if digital input bit is high
    if ((digitalInputValue & digitalInput.MaskGet()) == digitalInput.MaskGet())
    {
    Console.WriteLine($"Encoder position was: {positionRecord} when input triggered");
    break;
    }
    }
    // stop and reset recorder
    controller.RecorderStop();
    controller.RecorderReset();
    exitCode = Constants.EXIT_SUCCESS;
    }
    // handle errors as needed
    catch (Exception e)
    {
    Console.WriteLine($"❌ Error: {e.Message}");
    exitCode = Constants.EXIT_FAILURE;
    }
    finally
    {
    controller.Delete(); // dispose
    }
    return exitCode;
    Constants used in the C# sample apps.
    Definition _constants.cs:3
    const bool USE_HARDWARE
    Default: false.
    Definition _constants.cs:10
    const int EXIT_FAILURE
    Exit code for failed execution.
    Definition _constants.cs:69
    const int AXIS_0_INDEX
    Default: 0.
    Definition _constants.cs:20
    const int EXIT_SUCCESS
    Exit code for successful execution.
    Definition _constants.cs:68
    uint64_t AddressGet(RSIAxisAddressType addressType)
    Get the an address for some location on the Axis.
    RSIDataType AddressDataTypeGet(RSIAxisAddressType type)
    Get the data type for an address the Axis.
    Represents a single axis of motion control. This class provides an interface for commanding motion,...
    Definition rsi.h:6515
    uint64_t AddressGet()
    Get the Host Address for the I/O point.
    static IOPoint * CreateDigitalInput(Axis *axis, RSIMotorDedicatedIn motorDedicatedInNumber)
    Create a Digital Input from an Axis' Dedicated Input bits.
    int32_t MaskGet()
    Get the bit mask for the I/O point.
    Represents one specific point: Digital Output, Digital Input, Analog Output, or Analog Input....
    Definition rsi.h:12240
    static MotionController * Get(int32_t controllerIndex=CreationParameters::ControllerIndexDefault)
    Get an already running RMP EtherCAT controller.
    Represents the RMP soft motion controller. This class provides an interface to general controller con...
    Definition rsi.h:796
    RSIControllerAddressType
    Used to get firmware address used in User Limits, Recorders, etc.
    Definition rsienums.h:405
    RSIDataType
    Data types for User Limits and other triggers.
    Definition rsienums.h:730
    RSIAxisAddressType
    Used to get firmware address used in User Limits, Recorders, etc.
    Definition rsienums.h:447
    Helpers namespace provides utility functions for common tasks in RMP applications.
    Definition helpers.h:21
    Union representing a generic RMP firmware value with multiple data types, stored in 64-bits.
    Definition rsi.h:464
    double Double
    Double precision (64-bit) floating-point.
    Definition rsi.h:473
    int32_t Int32
    32-bit signed integer.
    Definition rsi.h:470

Recording Performance Metrics

Append a recorder at runtime and capture firmware and network timing deltas to characterize system performance.

  • C++

    /*
    This sample demonstrates how to record and analyze performance metrics
    including firmware and network timing statistics.
    */
    #include <iomanip>
    #include "helpers.h" // import our helper functions.
    #include "config.h" // import our configuration.
    #include "rsi.h" // import our RapidCode Library.
    using namespace RSI::RapidCode; // Import the RapidCode namespace
    // helper function to calculate the minimum, maximum, and average of a vector of integers
    static std::vector<int> MinMaxAvg(std::vector<int> data)
    {
    int min = data[0];
    int max = data[0];
    int sum = 0;
    for (int i = 0; i < data.size(); i++)
    {
    if (data[i] < min)
    {
    min = data[i];
    }
    if (data[i] > max)
    {
    max = data[i];
    }
    sum += data[i];
    }
    int avg = sum / data.size();
    return {min, max, avg};
    }
    int main()
    {
    const std::string SAMPLE_APP_NAME = "📜 Utilities: Record Performance";
    // print a start message to indicate that the sample app has started
    Helpers::PrintHeader(SAMPLE_APP_NAME);
    /* CONSTANTS */
    const int RECORD_PERIOD_SAMPLES = 1; // Number of samples between each record.
    const int RECORD_TIME = 1000; // Time in milliseconds to record for.
    /* RAPIDCODE INITIALIZATION */
    // create the controller
    int exitCode = -1; // Set the exit code to an error value.
    try // Ensure that the controller is deleted if an error occurs.
    {
    Helpers::CheckErrors(controller);
    /* SAMPLE APP BODY */
    // check if the network is started
    int valuesPerRecord;
    bool networkTimingEnabled = false;
    if (controller->NetworkStateGet() != RSINetworkState::RSINetworkStateOPERATIONAL)
    {
    std::cout << "Network is not operational. Only Firmware Timing Deltas will be recorded." << std::endl;
    valuesPerRecord = 1;
    }
    else
    {
    // enable network timing
    controller->NetworkTimingEnableSet(true);
    networkTimingEnabled = true;
    valuesPerRecord = 3;
    }
    // add a recorder
    int recorderIndex = controller->RecorderCountGet();
    controller->RecorderCountSet(recorderIndex + 1);
    // check if the recorder is running already, if it is then stop it
    if (controller->RecorderEnabledGet(recorderIndex))
    {
    controller->RecorderStop(recorderIndex);
    controller->RecorderReset(recorderIndex);
    }
    // configure the recorder
    controller->RecorderPeriodSet(RECORD_PERIOD_SAMPLES);
    controller->RecorderCircularBufferSet(false);
    controller->RecorderDataCountSet(valuesPerRecord);
    controller->RecorderDataAddressSet(0, controller->AddressGet(RSIControllerAddressType::RSIControllerAddressTypeFIRMWARE_TIMING_DELTA));
    if (networkTimingEnabled)
    {
    controller->RecorderDataAddressSet(1, controller->AddressGet(RSIControllerAddressType::RSIControllerAddressTypeNETWORK_TIMING_DELTA));
    controller->RecorderDataAddressSet(2, controller->AddressGet(RSIControllerAddressType::RSIControllerAddressTypeNETWORK_TIMING_RECEIVE_DELTA));
    }
    // start the recorder
    controller->RecorderStart(recorderIndex);
    controller->OS->Sleep(RECORD_TIME);
    controller->RecorderStop(recorderIndex);
    int recordCount = controller->RecorderRecordCountGet(recorderIndex);
    std::cout << "There are " << recordCount << " records available." << std::endl;
    // read the records
    std::vector<int> firmawareTimingDeltas(recordCount);
    std::vector<int> networkTimingDeltas(recordCount);
    std::vector<int> networkTimingReceiveDeltas(recordCount);
    for (int i = 0; i < recordCount; i++)
    {
    controller->RecorderRecordDataRetrieve(recorderIndex);
    firmawareTimingDeltas[i] = controller->RecorderRecordDataFirmwareValueGet(recorderIndex, 0).Int32;
    if (networkTimingEnabled)
    {
    networkTimingDeltas[i] = controller->RecorderRecordDataFirmwareValueGet(recorderIndex, 1).Int32;
    networkTimingReceiveDeltas[i] = controller->RecorderRecordDataFirmwareValueGet(recorderIndex, 2).Int32;
    }
    }
    // calculate the statistics
    std::vector<int> firmwareTimingStats = MinMaxAvg(firmawareTimingDeltas);
    std::cout << "Firmware Timing Deltas (us): ";
    std::cout << " Min = " << std::setw(4) << firmwareTimingStats[0];
    std::cout << " Max = " << std::setw(4) << firmwareTimingStats[1];
    std::cout << " Avg = " << std::setw(4) << firmwareTimingStats[2] << std::endl;
    if (networkTimingEnabled)
    {
    std::vector<int> networkTimingStats = MinMaxAvg(networkTimingDeltas);
    std::cout << "Network Timing Deltas (us): ";
    std::cout << " Min = " << std::setw(4) << networkTimingStats[0];
    std::cout << " Max = " << std::setw(4) << networkTimingStats[1];
    std::cout << " Avg = " << std::setw(4) << networkTimingStats[2] << std::endl;
    std::vector<int> networkTimingReceiveStats = MinMaxAvg(networkTimingReceiveDeltas);
    std::cout << "Network Timing Receive Deltas (us):";
    std::cout << " Min = " << std::setw(4) << networkTimingReceiveStats[0];
    std::cout << " Max = " << std::setw(4) << networkTimingReceiveStats[1];
    std::cout << " Avg = " << std::setw(4) << networkTimingReceiveStats[2] << std::endl;
    }
    // delete the recorder
    controller->RecorderCountSet(recorderIndex);
    exitCode = 0; // set the exit code to success.
    }
    catch (const std::exception &ex)
    {
    std::cerr << ex.what() << std::endl;
    exitCode = -1;
    }
    // delete the controller as the program exits to ensure memory is deallocated in the correct order
    controller->Delete();
    // print a message to indicate the sample app has finished and if it was successful or not
    Helpers::PrintFooter(SAMPLE_APP_NAME, exitCode);
    return exitCode;
    }
    static MotionController * Create(CreationParameters *creationParameters)
    Initialize and start the RMP EtherCAT controller.
    @ RSINetworkStateOPERATIONAL
    EtherCAT operational, good state.
    Definition rsienums.h:622
    @ RSIControllerAddressTypeNETWORK_TIMING_DELTA
    the latest time delta between the current network packet send time and the previous (microseconds)....
    Definition rsienums.h:416
    @ RSIControllerAddressTypeNETWORK_TIMING_RECEIVE_DELTA
    the latest time delta between the current network packet receive time and the previous (microseconds)...
    Definition rsienums.h:417
    @ RSIControllerAddressTypeFIRMWARE_TIMING_DELTA
    the latest time delta between the current RMP sample and the previous (microseconds)
    Definition rsienums.h:415
    MotionController::CreationParameters GetCreationParameters()
    Returns a MotionController::CreationParameters object with user-defined parameters.
    Definition config.h:81
    void CheckErrors(RapidCodeObject *rsiObject, const std::source_location &location=std::source_location::current())
    Checks for errors in the given RapidCodeObject and throws an exception if any non-warning errors are ...
    Definition helpers.h:87
    void PrintHeader(std::string sampleAppName)
    [NetworkShutdown]
    Definition helpers.h:205
    void PrintFooter(std::string sampleAppName, int exitCode)
    [PrintHeader]
    Definition helpers.h:221
    CreationParameters for MotionController::Create.
    Definition rsi.h:872