Click or drag to resize

4.1.6 Card Switch Standalone Tutorial

The C:\Program Files\RAYLASE\ProcessDataAnalyzer\API\SampleCode\Tutorials\CardSwitchStandaloneTutorial demonstrates how to programmatically configure the Process Data Analyzer for multi-card setups using the Standalone API. This tutorial focuses on adding both built-in and virtual signals to card configurations, including advanced features like signal dependencies and custom calculations.

This tutorial is particularly useful for:

  • Understanding programmatic configuration of multiple control cards

  • Learning how to create and configure virtual signals via the API

  • Managing signal dependencies in complex configurations

  • Working with the Standalone API for automated setup workflows

Tutorial Overview

The CardSwitchStandaloneTutorial demonstrates the following key concepts:

  • Starting the Process Data Analyzer service using the Standalone API

  • Retrieving and modifying the PDA configuration programmatically

  • Adding built-in signals (physical signals from the control card)

  • Creating virtual signals with mathematical calculations

  • Building signal dependency chains (virtual signals based on other virtual signals)

  • Managing signal keys and ordering

  • Saving configurations back to the service

Prerequisites

To run this tutorial, you need:

  • Two SP-ICE-3 control cards with valid IP addresses (or one card for basic testing)

  • ProcessDataAnalyzer installed with the Standalone API libraries

  • Visual Studio or .NET 8 SDK for building and running the tutorial

  • Basic understanding of C# and the Process Data Analyzer signal system

Programmatic Configuration

The tutorial demonstrates three approaches to configuration:

  1. Direct API Configuration: Modify configuration objects in code and upload them to the service (demonstrated in this tutorial)

  2. File-Based Configuration: Save configuration to JSON, edit manually, and reload (code included but commented out)

  3. GUI Configuration: Use the PDA GUI to configure, which persists to the service configuration automatically

The tutorial focuses on direct API configuration for maximum flexibility and automation.

Adding Built-In Signals

Built-in signals represent physical signals from the control card. The tutorial demonstrates adding the FPS (frames per second) signal:

Adding a Built-In Signal
var fpsSignal = new SignalConfig
{
    Config = new TraceDataSignalConfig
    {
        // The signal type from the BuiltInSignal enum
        Signal = new Signal { BuiltInSignal = BuiltInSignal.BisFps },

        // Enable the signal to be recorded
        IsEnabled = true,

        // Optional: Scale, offset, and timing can be configured
        //ScaleOffset = new ScaleOffset { Scale = 2, Offset = 3 },
        //Timing = new SignalTiming
        //{
        //    MeasurementDelay = 9,
        //    Offset = 8,
        //    TrackingError = 6,
        //    TransferDelay = 5
        //}
    }
};

// Add to card configuration
firstConnectedCard.Signals.Add( fpsSignal );

Creating Virtual Signals

Virtual signals are calculated from other signals using mathematical operations. The tutorial creates a 2D velocity signal from commanded X and Y positions:

Creating a Virtual Signal
var velocity2D = new SignalConfig
{
    Config = new TraceDataSignalConfig
    {
        // Assign a unique key for this virtual signal
        Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "Virtual0" } },

        // Display name
        CustomName = "XY Velocity",

        // Unit for display and export
        Unit = "m/s",

        // Configure the calculation
        VirtualSignalConfig = new VirtualSignalConfig
        {
            Calculation = CalculationType.CtVelocity2D,

            // Input signals (order matters!)
            InputSignals = {
                new Signal { BuiltInSignal = BuiltInSignal.BisField0TxX },
                new Signal { BuiltInSignal = BuiltInSignal.BisField0TxY }
            }
        },
        IsEnabled = true
    }
};

Available calculation types include velocity, multiplication, division, addition, subtraction, and more. Refer to the CalculationType enum for a complete list.

Signal Dependencies and Chaining

Virtual signals can depend on other virtual signals, creating calculation chains. The tutorial demonstrates creating a "gate-enabled velocity" that multiplies the velocity by the gate signal:

Chained Virtual Signal
var gateEnabledVelocity = new SignalConfig
{
    Config = new TraceDataSignalConfig
    {
        // Use a higher key number to ensure proper ordering
        Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "Virtual1" } },
        CustomName = "Gate ON Velocity",
        Unit = "m/s",

        VirtualSignalConfig = new VirtualSignalConfig
        {
            Calculation = CalculationType.CtMultiplication,
            InputSignals = {
                // Reference the previously created virtual signal
                new Signal { VirtualSignal = velocity2D.Config.Signal.VirtualSignal },
                // Combine with a built-in signal
                new Signal { BuiltInSignal = BuiltInSignal.BisGate }
            }
        },
        IsEnabled = true
    }
};
Important note  Important

When creating signal dependencies, ensure that:

  • Dependent signals have higher key numbers than their dependencies

  • Dependencies are added to the configuration before dependent signals

  • Virtual signal keys are unique within the card configuration

Signal Key Management

The tutorial demonstrates two approaches to virtual signal key management:

1. Manual Key Assignment (Recommended):

C#
Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "Virtual0" } }

This approach gives you full control over signal ordering and dependencies.

2. Automatic Key Assignment:

C#
Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "XY Velocity" } }

Using a descriptive name as the key will auto-generate a unique key, but you must ensure dependencies are added in the correct order.

Multi-Card Configuration

The tutorial requires two card IP addresses and demonstrates how to work with multiple cards:

Multi-Card Setup
// Get current configuration
var config = await api.Configuration.GetPdaConfigAsync();
var cardConfigs = config.CardConfigurations;

// Access specific card configurations
var firstConnectedCard = cardConfigs[0];
var secondConnectedCard = cardConfigs[1];

// Add signals to specific cards
firstConnectedCard.Signals.Add( velocity2D );
firstConnectedCard.Signals.Add( gateEnabledVelocity );

// Save updated configuration back to service
await api.Configuration.SetPdaConfigAsync( cardConfigs );

Each card can have its own signal configuration, allowing for different setups across multiple scan heads or processing stations.

Running the Tutorial

To run the CardSwitchStandaloneTutorial:

  1. Build the tutorial project in Visual Studio or using dotnet build

  2. Run the executable with two card IP addresses as arguments:

    CardSwitchStandaloneTutorial.exe 10.2.0.101 10.2.0.102
  3. Optional: Provide a custom path to ProcessDataAnalyzer.Service.exe as a third argument:

    CardSwitchStandaloneTutorial.exe 10.2.0.101 10.2.0.102 "C:\CustomPath\ProcessDataAnalyzer.Service.exe"
  4. The tutorial will configure the service and exit

  5. View the results by starting the PDA GUI or using the Standalone API Tutorial to acquire data

Complete Source Code

The complete source code demonstrates all concepts in a working example:

CardSwitchStandaloneTutorial
//-----------------------------------------------------------------------------
// This example demonstrates how to configure two cards and switching the
// recording configuration of a card using the PDA API.
//-----------------------------------------------------------------------------

using RAYLASE.PDA.API;
using System.Net;

public class Program
{
    public static async Task<int> Main( string[] args )
    {
        Console.WriteLine( "\n\nCard Switch Standalone sample\n------------------\n\n" );

        if ( args.Length < 2 )
        {
            Console.WriteLine( "This tutorial requires two valid card IP addresses as argument" );
            return -1;
        }

        var cardIP1 = args[0];
        if ( !IPAddress.TryParse( cardIP1, out _ ) )
        {
            Console.WriteLine( "The supplied first IP address argument is not a valid IP address" );
            return -1;
        }
        var cardIP2 = args[1];
        if ( !IPAddress.TryParse( cardIP2, out _ ) )
        {
            Console.WriteLine( "The supplied second IP address argument is not a valid IP address" );
            return -1;
        }

        // Default installation path of the ProcessDataAnalyzer.Service.exe
        var defaultServiceExePath = @$"{Environment.ExpandEnvironmentVariables( "%ProgramFiles%" )}\RAYLASE\ProcessDataAnalyzer\bin\ProcessDataAnalyzer.Service.exe";

        // Check if a different path was specified in the arguments.
        var serviceExePath = args.Length <= 1 ? defaultServiceExePath : args[2];

        // Get the API which auto-starts the PDA Server and is valid for this (Main()) scope.
        using var api = new RAYLASE.PDA.ClientLib.Api( serviceExePath );

        // Get the current service configurations
        var config = await api.Configuration.GetPdaConfigAsync();
        var cardConfigs = config.CardConfigurations;

        // The config can be saved to a service relative path as well and be
        // inspected/adapted manually and re-uploaded to the service
        // await api.Configuration.SavePdaConfigAsync( ".\servicePdaConfig.json" );
        // Console.WriteLine( "Edit the servicePdaConfig.json, save and press any button to continue." );
        // Console.Read();
        // await api.Configuration.SetPdaConfig( ".\servicePdaConfig.json" );

        // Alternatively, the configuration can be done in the PDA GUI and is reloaded on a service restart.

        // This example will focus on changing the configuration programmatically.

        //-----------------------------------------------------------------------------
        // Create a built-in and a virtual signal.
        //-----------------------------------------------------------------------------

        // First, let's create a physical and a virtual signal we want to record.
        // BuiltInSignals are physical signals on the card, VirtualSignals
        // represent a new signal created by the PDA and are defined through a mathematical
        // equation.
        var fpsSignal = new SignalConfig
        {
            // The new signal type.
            Signal = new Signal { BuiltInSignal = BuiltInSignal.BisFps },
            // Enable the signal to be recorded and not only configured
            IsEnabled = true,
            // Scale offset and timing could be set as well.
            //ScaleOffset = new ScaleOffset { Scale = 2, Offset = 3 },
            //Timing = new SignalTiming
            //{
            //    MeasurementDelay = 9,
            //    Offset = 8,
            //    PropagationDelay = 7,
            //    TrackingError = 6,
            //    TransferDelay = 5
            //}
        };

        var velocity2D = new SignalConfig
        {


            // It is also possible to set the name directly to the signal key in which case
            // the key is copied to the CustomName property and a valid key is auto assigned:
            // Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "XY Velocity" } },
            // This has the drawback that the order of virtual signals is determined by the order
            // in which the signals are added to the configuration below. We manually assign
            // the ID Virtual0 to this signal.
            Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "Virtual0" } },
            // Add a name to the new virtual signal. Multiple virtual signals can be
            // created using the same calculation type and thus only differ in their name.
            CustomName = "XY Velocity",
            // Display and export unit name
            Unit = "m/s",
            // Interpolate values
            //IsUserInterpolating = true,
            // Configure the virtual signal to take two input signals and compute the velocity.
            // We use commanded X and Y here since they are also available if no scanhead
            // is connected to the card.
            VirtualSignalConfig = new VirtualSignalConfig
            {
                Calculation = CalculationType.CtVelocity2D,
                // For calculations, the order of input signals is from left/first to right/last.
                // Dividing x by y would need x as first input signal and y as second.
                // If this calculation would be a division rather than velocity, we would be
                // dividing Field0TxY by Field0TxX.
                InputSignals = {
                        // Filed 0: First field
                        // Tx: Commanded signal (Rx would be received, measured)
                        // X: Signal in X direction
                        new Signal { BuiltInSignal = BuiltInSignal.BisField0TxX },
                        new Signal { BuiltInSignal = BuiltInSignal.BisField0TxY }
                    }
            },
            IsEnabled = true
        };

        // It is possible to combine built-in and/or virtual signals in new virtual signals.
        // For demonstration purpose, let's multiply above velocity by the gate signal, which
        // will result in a velocity which will only be "active" during gate ON (otherwise the
        // velocity is multiplied by 0).
        var gateEnabledVelocity = new SignalConfig
        {            
                // Make sure that this signal's ID is higher than the virtual input signal's ID.
                Signal = new Signal { VirtualSignal = new VirtualSignal { Key = "Virtual1" } },
                CustomName = "Gate ON Velocity",
                Unit = "m/s",
                // Configure the new virtual signal.
                VirtualSignalConfig = new VirtualSignalConfig
                {
                    Calculation = CalculationType.CtMultiplication,
                    InputSignals = {
                        new Signal {VirtualSignal = velocity2D.Signal.VirtualSignal },
                        new Signal { BuiltInSignal = BuiltInSignal.BisGate }
                    }
                },
                IsEnabled = true
        };

        //-----------------------------------------------------------------------------
        // Add the new signals to the configuration
        //-----------------------------------------------------------------------------

        // The PDA configuration can be done for multiple devices at the same time so
        // a device ID must be specified to access the device specific configurations.
        // The device ID corresponds to the order in which the devices are connected to
        // the service. We have no device connected yet, but want to edit the first device
        // configuration since it will be created by default.
        var firstConnectedCard = cardConfigs[0];
        firstConnectedCard.Signals.Add( fpsSignal );
        // Make sure that the virtual signal key is not used already. If you want to change the signal, adopt the existing entry from the config.
        if ( !firstConnectedCard.Signals.Any( x => x.Signal.VirtualSignal is not null && x.Signal.VirtualSignal.Key == gateEnabledVelocity.Signal.VirtualSignal.Key ) )
        {
            // Since we set the signal keys to Virtual1 for gateEnabledVelocity and to Virtual0 for velocity2D,
            // it is ok now to switch the order in which the signals are added to the card configuration.
            // If the signal key would be a name, make sure that virtual signal dependencies are added to the
            // config first.
            firstConnectedCard.Signals.Add( gateEnabledVelocity );
        }
        if ( !firstConnectedCard.Signals.Any( x => x.Signal.VirtualSignal is not null && x.Signal.VirtualSignal.Key == velocity2D.Signal.VirtualSignal.Key ) )
        {
            firstConnectedCard.Signals.Add( velocity2D );
        }

        // Save the new configuration to the service
        // The configuration is shared between the GUI and the Service/Server and saved under
        // %ProgramData%\RAYLASE\ProcessDataAnalyzer\Configurations.json
        await api.Configuration.SetPdaConfigAsync( cardConfigs );

        // Now we connect a card to the service, this could be done before configuring as well.
        // Starting the acquisition will get the traces of all configured built-in signals
        // and the PDA will process all virtual signals.
        // See the PDA GUI or other tutorials for acquiring and exporting data.

        return 0;
    }
}
Next Steps

After completing this tutorial, you can:

See Also