Introduction

When I first explored TwinCAT ADS for C#/.NET, I quickly realised that the official Infosys documentation lacked coherence and omitted essential use cases. Key topics, like reading complex structs, invoking RPC methods, and handling dynamic symbol values, were either incomplete or presented in ways that made practical application challenging.

This book aims to bridge those gaps through clear, example-driven explanations on:

  • Connecting to TwinCAT ADS in .NET – Establish a reliable client for interfacing with TwinCAT PLCs.

  • Dynamic Symbol Management – Accessing and manipulating PLC symbols with flexibility and ease, leveraging dynamic types to avoid rigid, predefined structures.

  • Event Handling – Setting up event-driven reads to monitor symbol changes, connection states, and ADS states, enabling responsive and efficient data interaction.

  • RPC Invocation – Interact directly with function blocks and interfaces on the PLC through remote procedure calls (RPCs), simplifying PLC automation.

I hope by the end of this book, you'll be able to access, control, and manipulate your PLC data in a straightforward and accessible way, so you can focus on building robust, adaptable, high-performance applications.

Prerequisites

To make full use of this guide, ensure you have the following installed and configured:

  1. The Beckhoff TwinCAT XAE (eXtended Automation Engineering) or XAR (eXtended Automation Runtime) – Essential for configuring and deploying PLC applications. Both can be downloaded from the Beckhoff download finder webpage.

  2. Microsoft .NET SDK 8.0 – Provides the tools and libraries for developing and running .NET applications. To install it via winget, run:

    winget install Microsoft.DotNet.SDK.8
    
  3. An IDE for C# Development – For this book, I used Visual Studio Code with the C# extension and the .NET CLI. If you’re new to this setup, please refer to the Visual Studio Code .NET documentation for additional guidance.

    To get started with the .NET CLI in your chosen directory, you can create a new console application with:

    dotnet new console -n <ProjectName>
    

    Use the following command to explore additional project templates:

    dotnet new list
    

    To build and run your project, use:

    dotnet run
    

    A comprehensive guide to the .NET CLI can be found here.

  4. Beckhoff TwinCAT ADS NuGet Package – This package allows you to establish an ADS connection to your device.

    Install it by running this command in your project’s root directory:

    dotnet add package Beckhoff.TwinCAT.Ads
    
  5. Newtonsoft.Json NuGet Package – This package is used for JSON serialisation of dynamic objects, which is particularly helpful for displaying complex symbol values in a structured format.

    Install it by running this command in your project’s root directory:

    dotnet add package Newtonsoft.Json
    

Setting Up the ADS Connection

To begin, let’s establish a connection to the PLC. The code below assumes you have an active TwinCAT project ready to connect.For simplicity, I recommend creating a new .NET project, then pasting the code below into your Program.cs file (or whichever file contains your entry point).

using TwinCAT.Ads;

using (AdsClient client = new())
{
    client.Connect(AmsNetId.Local, 851);
    Console.WriteLine
    (
        "Hello there!\n" +
        $"You're connected to {client.Address} from {client.ClientAddress}.\n" +
        $"The current state of the PLC is: {client.ReadState().AdsState}.\n" +
        "\nPress any key to exit...\n"
    );
    Console.ReadKey(true);
}

This code should produce an output similar to the following when run:

Hello there!
You're connected to 192.168.137.1.1.1:851 from 192.168.137.1.1.1:XXXXX.
The current state of the PLC is: Run.

Press any key to exit...

This code connects to the PLC on the local AMS Net ID with port 851. It displays the connection details, including the PLC state, which should confirm a successful connection if everything is set up correctly. Pressing any key will then terminate the application.

This setup gives you a basic foundation for communicating with your PLC through ADS in .NET.

Defining the PLC Symbols

Now that we’ve established a connection to the PLC, let's examine the symbols we’ll be working with in this book.

The MAIN program in the TwinCAT project is defined as follows:

PROGRAM MAIN
VAR
    nValue  : DINT                  := 42;
    fValue  : LREAL                 := 3.14;
    eValue  : E_Value               := E_Value.Winter;
    arValue : ARRAY[0..2] OF LREAL  := [273.15, 2.71, 9.80665];
    stValue : ST_Value              := (bValue := TRUE, sValue := 'Hello there!');
    fbValue : FB_Value;
    ipValue : I_Value               := fbValue; 
END_VAR

In this book, we’ll be interacting with a variety of symbols, each representing different data types and complexities, including:

  1. nValue: A basic integer type.
  2. fValue: A floating-point value (LREAL).
  3. eValue: An enumeration.
  4. arValue: An array containing floating-point numbers (LREAL).
  5. stValue: A structured type that we’ll define below.
  6. fbValue: A function block that includes an RPC method and a property.
  7. ipValue: An interface that includes an RPC method.

Enum Definition

The eValue variable is an instance of type E_Value, an enumeration that represents seasons. Here’s its definition:

TYPE E_Value :
(
    _ := 0,
    Summer,
    Autumn,
    Winter,
    Spring
)UINT;
END_TYPE

Struct Definition

The stValue variable is an instance of the structured data type ST_Value. Here’s its definition:

TYPE ST_Value :
STRUCT
    bValue : BOOL;
    sValue : STRING;
END_STRUCT
END_TYPE

The struct ST_Value contains a boolean (bValue) and a string (sValue). These members allow us to test interactions with complex data types.

Interface Definition

The I_Value interface defines a method that will enable Remote Procedure Call (RPC) access. Here’s the definition:

INTERFACE I_Value
{attribute 'TcRpcEnable'}
METHOD Sum : LREAL
VAR_INPUT
    fA, fB : LREAL;
END_VAR
VAR_OUTPUT
    sMessage : STRING;
END_VAR
END_METHOD
END_INTERFACE

The Sum method is enabled for RPC through the {attribute 'TcRpcEnable'} pragma, allowing it to be called remotely via ADS. This method takes two LREAL inputs and returns a sum along with a descriptive message in sMessage.

Function Block Definition

The fbValue variable is an instance of the function block FB_Value which implements the I_Value interface. This function block will be used to demonstrate remote procedure calls (RPC) and property access through ADS.

The function block FB_Value includes:

  • An RPC method called Sum that calculates the sum of two LREAL inputs and returns both the sum and a descriptive string message.

  • A property called Value that can be accessed and modified remotely. This property uses the {attribute 'monitoring' := 'call'} pragma to allow read and write operations over ADS.

    Note: This feature isn’t supported on Windows CE-based devices.

Below is the full implementation of FB_Value:

FUNCTION_BLOCK FB_Value IMPLEMENTS I_Value
VAR
   _fValue : LREAL;
END_VAR

{attribute 'TcRpcEnable'}
METHOD Sum : LREAL
VAR_INPUT
    fA, fB : LREAL;
END_VAR
VAR_OUTPUT
    sMessage : STRING;
END_VAR
    Sum := fA + fB;
    sMessage := 
        CONCAT('The sum of ', 
        CONCAT(TO_STRING(fA),
        CONCAT(' and ', 
        CONCAT(TO_STRING(fB), 
        CONCAT(' is ', TO_STRING(Sum)
    )))));
END_METHOD

{attribute 'monitoring' := 'call'}
PROPERTY Value : LREAL
GET
    Value := THIS^._fValue;
SET
    THIS^._fValue := Value * 2;
END_PROPERTY

END_FUNCTION_BLOCK
  • Method: Sum: This RPC-enabled method takes two LREAL parameters (fA and fB) as inputs and returns their sum. The output sMessage provides a summary of the calculation in text format, giving both the input values and the result.
  • Property: Value: This property provides controlled access to the internal _fValue variable. In the GET accessor, it simply returns the current _fValue. In the SET accessor, it doubles the input value before storing it back into _fValue.

Interacting with PLC Symbols

Now that we’ve defined the symbols, we can begin interacting with them in C# .NET.

In this section, we’ll use the .NET Dynamic Language Runtime (DLR) to create dynamic objects representing each PLC symbol. These objects mirror the structure and data of the symbols on the PLC, including standard IEC 61131 types. For example, the PLC symbol "MAIN.nValue" can be accessed as MAIN.nValue in C#, allowing straightforward interaction with your PLC’s variables.

Importing Required Libraries

Before we begin we’ll need to import several essential libraries. These libraries provide tools for establishing ADS communication, loading symbols dynamically, managing the PLC’s symbol system, and handling JSON data if required.

Below are the required imports:

using TwinCAT;
using TwinCAT.Ads;
using TwinCAT.Ads.TypeSystem;
using TwinCAT.TypeSystem;
using Newtonsoft.Json;

Loading Symbols with the Symbol Loader

To interact with PLC variables, we first need to load the symbols using a dynamic symbol loader.

The following code uses the SymbolLoaderFactory to create and return an instance of an object that implements IDynamicSymbolLoader internally. This object which provides access to the DynamicSymbolsCollection. A collection holds all the symbols available in the PLC, allowing us to browse and interact with them dynamically:

var symbolLoader = (IDynamicSymbolLoader)SymbolLoaderFactory.Create
(
    client,
    new SymbolLoaderSettings(SymbolsLoadMode.DynamicTree)
);

var symbols = (DynamicSymbolsCollection)symbolLoader.SymbolsDynamic;

To list the top-level PLC symbols, use the following snippet:

foreach (var symbol in symbols) 
{
    Console.WriteLine(symbol.InstancePath);
}

Assuming a fresh TwinCAT project is active, this should output the following symbols in the console:

Constants
Global_Version
MAIN
TwinCAT_SystemInfoVarList

These top-level symbols represent either global variable lists (GVLs) or programs (PROGRAMs) in the PLC. From here, you can drill down further into specific PROGRAMs or GVLs to explore their contained variables, properties and methods.

Accessing the MAIN Program Symbol

The MAIN program symbol usually represents the primary program block in our TwinCAT PLC setup.

To retrieve the MAIN program symbol from the DynamicSymbolsCollection, we can use either of these commands:

dynamic MAIN = symbols["MAIN"];

or more succinctly:

dynamic MAIN = symbols.MAIN;

Here, we're creating a DynamicSymbol object representing MAIN using the Dynamic Language Runtime (DLR). This approach allows us to interact with symbols flexibly. However, since it’s a dynamic type, IntelliSense won't provide auto-suggestions for properties or methods. Please, familiarising yourself with the DynamicSymbol documentation. It will be useful as you work with these objects.

To inspect the symbols under MAIN, we can iterate over its SubSymbols property. This provides a list of all variables and data structures contained within MAIN, which will vary depending on your PLC configuration:

foreach (var symbol in MAIN.SubSymbols) 
{
    Console.WriteLine(symbol.InstancePath);
}

If you've setup your TwinCAT project using the symbols we defined earlier, the above code snippet should output:

MAIN.arValue
MAIN.eValue
MAIN.fbValue
MAIN.fValue
MAIN.ipValue
MAIN.nValue
MAIN.stValue

Complete Code Example

Now that we've explored accessing the MAIN program symbol and listing its sub-symbols, let’s put it all together in a full example. This complete code snippet covers each step we've discussed: connecting to the PLC, loading symbols dynamically, and accessing the MAIN program symbol and its contents.

The code example below demonstrates how to import the necessary libraries, establish a connection to your device, load the available symbols using the SymbolLoaderFactory, and list both the top-level symbols and the sub-symbols within MAIN.

using TwinCAT;
using TwinCAT.Ads;
using TwinCAT.Ads.TypeSystem;
using TwinCAT.TypeSystem;

using (AdsClient client = new())
{
    client.Connect(AmsNetId.Local, 851);
    var symbolLoader = (IDynamicSymbolLoader)SymbolLoaderFactory.Create
    (
        client,
        new SymbolLoaderSettings(SymbolsLoadMode.DynamicTree)
    );

    var symbols = (DynamicSymbolsCollection)symbolLoader.SymbolsDynamic;

    foreach (var symbol in symbols) Console.WriteLine(symbol.InstancePath);
    Console.WriteLine();

    dynamic MAIN = symbols["MAIN"];
    
    foreach (var symbol in MAIN.SubSymbols) Console.WriteLine(symbol.InstancePath);

    Console.WriteLine("\nPress any key to exit...\n");
    Console.ReadKey(true);
}

Running this code will display the following:

Constants
Global_Version
MAIN
TwinCAT_SystemInfoVarList

MAIN.arValue
MAIN.eValue
MAIN.fbValue
MAIN.fValue
MAIN.ipValue
MAIN.nValue
MAIN.stValue

Press any key to exit...

This is a list of the top-level symbols, followed by all sub-symbols within MAIN, demonstrating how to examine the structure of your PLC program and begin working with its data. This setup provides a solid foundation for the dynamic handling of symbols that will be used the following sections of this book.

Reading and Writing Values from PLC Symbols

In this section, we’ll explore how to retrieve and set values for various PLC symbols, focusing on those defined in the MAIN program from earlier sections using the dynamic symbol loader. Remember, it’s essential that the names of dynamic symbols in C# .NET precisely match those in the PLC program. For instance, the symbol "MAIN.nValue" in the PLC is accessed as MAIN.nValue in .NET.

We’ll begin by working with primitive types, then move on to more complex data structures, demonstrating how to interact with each type through dynamic symbols.

Primitives

Primitive types, such as booleans, integers, and floating-point numbers, are handled in TwinCAT ADS .NET library by instances of the DynamicSymbol class. To retrieve values from these symbols, call the ReadValue() method. Assigning the output to a typed variable ensures type safety, enhances readability, and minimises round-trips to the PLC. For updating values, the WriteValue(Object) method allows you to set new values by passing the desired data as a parameter.

Reading Primitive Values

Here’s an example of reading primitive values from the MAIN program:

dynamic MAIN = symbols["MAIN"];

int plcIntValue = MAIN.nValue.ReadValue();
double plcDblValue = MAIN.fValue.ReadValue();

Writing Primitive Values

To modify these values, use WriteValue() as shown below:

MAIN.nValue.WriteValue(888);
MAIN.fValue.WriteValue(6.626);

Enums

Enums, similar to primitive types, are represented by instances of the DynamicSymbol class. Reading an enum’s value involves calling the ReadValue() method. Since PLC enums are represented by their underlying numeric type (e.g., byte, ushort, int, etc.), it’s best to specify the numeric type directly in the PLC code to maintain consistency between the PLC and .NET.

Here’s an example of an enum type in Structured Text with UINT as its underlying type:

TYPE E_Value :
(
    _ := 0,
    Summer,
    Autumn,
    Winter,
    Spring
) UINT;
END_TYPE

This setup allows for consistent casting to the correct numeric type in .NET when reading or writing values.

Reading Enum Values

To read an enum value, use the ReadValue() method and cast it to the specified type:

dynamic MAIN = symbols["MAIN"];
ushort plcEnumValue = (ushort)MAIN.eValue.ReadValue();

Writing Enum Values

To set an enum value, use the WriteValue(Object) method. You can either pass the integer representation directly or use .NET features to translate the enum member to its numeric value.

Here’s how to set eValue to Autumn, which corresponds to 2:

MAIN.eValue.WriteValue(2);

Alternatively, if you prefer converting an enum name to its numeric equivalent, the IDataTypeCollection and IEnumType interfaces allow for parsing:

var enumType = (IEnumType)symbolLoader.DataTypes["E_Value"];
var enumValue = (ushort)enumType.Parse("Autumn");
MAIN.eValue.WriteValue(enumValue);

Accessing Enum Names and Values

The IEnumType interface also provides convenient access to the enum members’ names and values:

Names: Retrieve an array of member names:

string[] enumNames = enumType.GetNames();

Values: Retrieve an array of the corresponding numeric values as IConvertible[]:

IConvertible[] enumValues = enumType.GetValues();

Arrays

Arrays are represented as instances of the DynamicArrayInstance class, supporting read and write operations for both individual elements and entire arrays.

Reading Arrays

To read an entire array, use the ReadValue() method:

dynamic MAIN = symbols["MAIN"];
double[] plcDblArray = MAIN.arValue.ReadValue();

To retrieve a single element in an array, there are multiple approaches. Each of these reads the first element:

MAIN.arValue[0].ReadValue();
MAIN.arValue.Elements[0].ReadValue();
MAIN.arValue.SubSymbols[0].ReadValue();

Using MAIN.arValue[0] is recommended, particularly for multidimensional arrays, as it simplifies syntax.

Writing to Array Elements

To modify a single element, use the WriteValue(Object) method:

MAIN.arValue[0].WriteValue(6.626);

Attempting to update an entire array in a single call will raise an ArgumentOutOfRangeException due to current limitations in WriteValue(Object) for array symbols:

// This will cause an error
MAIN.arValue.WriteValue(new double[] { 1.602, 6.022, 1.380 });

Instead, each element must be updated individually:

double[] newValues = { 1.602, 6.022, 1.380 };
for (int i = 0; i < Math.Min(newValues.Length, MAIN.arValue.Elements.Count); i++)
    MAIN.arValue[i].WriteValue(newValues[i]);

While functional, this approach involves multiple write operations, which may be less efficient. If this behaviour appears to be a limitation, consider reaching out to Beckhoff Technical Support for clarification.

Tip: For multidimensional arrays, extend this approach by adding nested loops for each dimension.

Handling Non-Zero-Based Arrays

The IEC-61131-3 standard permits arrays with custom, non-zero-based bounds. To interact with these arrays, use their specific indices:

double plcArrayValue = MAIN.arNewValues[-1].ReadValue();
MAIN.arNewValues[-1].WriteValue(10_973_731.568160);

Array Metadata with Dimensions

The Dimensions property offers valuable metadata about an array, such as bounds, dimensions, and whether it’s non-zero-based:

int[] lowerBounds = MAIN.arValue.Dimensions.LowerBounds;
int[] upperBounds = MAIN.arValue.Dimensions.UpperBounds;
int numOfDimensions = MAIN.arValue.Dimensions.Count;
int[] dimensionLengths = MAIN.arValue.Dimensions.GetDimensionLengths();
bool isNonZeroBased = MAIN.arValue.Dimensions.IsNonZeroBased;

foreach (var dim in MAIN.arValue.Dimensions) Console.WriteLine(dim.ElementCount);

The Dimensions property is especially useful when working with complex arrays of various sizes and bounds, providing flexibility in managing diverse array structures.

Structs

Structs are represented by the DynamicStructInstance class, which enables both reading and writing operations for individual members or the entire structure.

Reading Structs

To retrieve all members of a struct at once, use the ReadValue() method:

dynamic MAIN = symbols["MAIN"];
dynamic plcStructValue = MAIN.stValue.ReadValue();

Individual struct members can be accessed directly by name:

bool plcBoolValue = MAIN.stValue.bValue.ReadValue();
string plcStrValue = MAIN.stValue.sValue.ReadValue();

Writing to Struct Members

To modify specific struct members, use the WriteValue(Object) method:

MAIN.stValue.bValue.WriteValue(false);
MAIN.stValue.sValue.WriteValue("General Kenobi!");

Limitations of Writing Entire Structs

Attempting to write an entire struct at once using an anonymous object will raise an error, as shown below:

// This produces an error: "Struct member 'bValue' (of ValueType: <>f__AnonymousType0`2) not found!"
MAIN.stValue.WriteValue(new
{
    bValue = true,
    sValue = "Another happy landing!"
});

This error occurs because DynamicValueMarshaler expects either a DynamicValue or a compatible ADS struct type, not a .NET anonymous object. If writing an entire struct in one call would be useful for your application, consider contacting Beckhoff Technical Support to confirm if this behavior is a limitation or intended.

Note: If you find an alternative approach or improvement, feel free to submit a pull request to enhance this guide for other developers!

Function Blocks

Function blocks can be accessed similarly to structs, allowing access to both local variables and properties. Properties marked with the {attribute 'monitoring' := 'call'} pragma can be accessed just like struct members. When properties contain code within their getter or setter blocks, that code will execute upon reading or writing to the property directly.

Reading Function Blocks

To retrieve the entire function block, use the ReadValue() method:

dynamic MAIN = symbols["MAIN"];
dynamic plcFBValue = MAIN.fbValue.ReadValue();

Once you have retrieved the function block as a whole, you can access its local variables directly as shown below:

double localValue = plcFBValue._fValue; 

You can also retrieve property values using this method, but be aware that doing so will NOT trigger any logic within the getter. The following code retrieves the property value without executing its getter logic:

double propValue = plcFBValue.Value;

To ensure that the getter logic is called, you need to access the property directly and use the ReadValue() method, as shown here:

double propValue = MAIN.fbValue.Value.ReadValue();

You can also directly read the value of any function block local variable:

double localValue = MAIN.fbValue._fValue.ReadValue();

Writing Function Blocks

The only way to write to function block local variables and properties is to use WriteValue(Object) directly on them:

MAIN.fbValue._fValue.WriteValue(2001);
MAIN.fbValue.Value.WriteValue(66);

You cannot modify PLC symbols in a function block that has been retrieved as a whole:

// Compiles but does nothing.
plcFBValue._fValue = 1999.0;
plcFBValue.Value = 300.0;

Handling Events

At times you may need to respond to changes in the ADS device’s state or track updates to specific symbol values dynamically. The TwinCAT ADS .NET library provides several built-in events that allow you to do just that. This section demonstrates how to handle connection state changes, monitor ADS state transitions, and listen for updates to symbol values.

Monitoring Connection State Changes

The ConnectionStateChanged event allows you to track changes in the connection status of the ADS client. This is useful for managing connectivity and taking appropriate actions when the connection to the PLC is established or lost.

Note: The connection state changes only when the client actively connects to or disconnects from the PLC, or when communication is initiated by the user.

The code below demonstrates how to register an event handler to the ConnectionStateChanged event:

var connectionStateChangedHandler = new EventHandler<ConnectionStateChangedEventArgs>
(
    (sender, e) =>
    {
        Console.WriteLine($"Client connection state: {e.NewState}.");
    }
);

client.ConnectionStateChanged += connectionStateChangedHandler;

Listening to ADS State Changes

The AdsStateChanged event enables you to monitor when the ADS client transitions between different operational states. This event is beneficial for tracking ADS state transitions and responding to conditions where the client moves between states such as Run, Stop, or Error.

The code below demonstrates how to register an event handler to the AdsStateChanged event:

var adsStateChangedHandler = new EventHandler<AdsStateChangedEventArgs>
(
    (sender, e) =>
    {
        Console.WriteLine
        (
            $"ADS state changed. New state: {e.State.AdsState}, " +
            $"Device state: {e.State.DeviceState}"
        );
    }
);

client.AdsStateChanged += adsStateChangedHandler;

Listening for Value Changes

One of the most valuable features of the ADS client is its capability to track real-time updates to PLC symbols via the ValueChanged event. This event notifies you whenever a subscribed symbol’s value changes, making it essential for applications that need to respond to dynamic data.

The code below demonstrates how to register an event handler to the ValueChanged event:

var valueChangedHandler = new EventHandler<ValueChangedEventArgs>
(
    (sender, e) =>
    {
        dynamic val = e.Value;
        Console.WriteLine
        (
            $"Value of {e.Symbol.InstancePath} changed to " +
            (e.Symbol.IsPrimitiveType ? val : JsonConvert.SerializeObject(val))
        );
    }
);

MAIN.fValue.ValueChanged += valueChangedHandler;
MAIN.stValue.ValueChanged += valueChangedHandler;

Best Practices

  • Minimise Processing in Handlers: Keep event handlers efficient to avoid performance bottlenecks. Offload intensive tasks to a separate thread or process if needed.

  • Unsubscribe Appropriately: Always unsubscribe from events when they are no longer needed to prevent memory leaks.

  • Ensure Thread Safety: If your application is multi-threaded, ensure that event handlers are thread-safe and can handle concurrent updates properly.

Remote Procedure Calls (RPCs)

Remote Procedure Calls (RPCs) provide a powerful way to interact with PLC function blocks and interfaces by invoking methods directly from your .NET client application. This approach is particularly useful for handling structured data and executing business logic encapsulated within the PLC, allowing for streamlined communication and control.

In the TwinCAT ADS .NET library, function blocks and interfaces are represented as dynamic types (DynamicStructInstance for function blocks and DynamicInterfaceInstance for interfaces). DynamicStructInstance inherits from DynamicInterfaceInstance, giving both classes shared access to RPC-related properties and methods.

This section will guide you through checking for RPC support, listing RPC methods, and invoking them to extend your control over PLC operations.

Checking for RPC Methods

Before attempting to invoke an RPC method, it’s helpful to confirm whether a PLC symbol supports RPC functionality. Both DynamicInterfaceInstance and DynamicStructInstance classes include an HasRpcMethods property, which returns true if RPC methods are available for the symbol. Checking this property can help you avoid errors from attempting to call unsupported methods.

Here’s how you can check if a function block or interface has RPC methods:

var formatYesNoResponse = (bool answer) => answer ? "Yes" : "No";

Console.WriteLine
(
    $"Does {MAIN.fbValue.InstancePath} have RPC methods?: " +
    formatYesNoResponse(MAIN.fbValue.HasRpcMethods)
);

Console.WriteLine
(
    $"Does {MAIN.ipValue.InstancePath} have RPC methods?: " +
    formatYesNoResponse(MAIN.ipValue.HasRpcMethods)
);

In this example, the HasRpcMethods property provides a straightforward way to determine whether a given symbol, such as fbValue or ipValue, supports RPC calls. If HasRpcMethods returns true, you can proceed with further steps, such as listing and invoking available RPC methods.

Listing Available RPC Methods

You can retrieve a list of available methods using the RpcMethods property. This property provides an IRpcMethodCollection, containing instances of IRpcMethod for each RPC method available on the symbol. Each IRpcMethod instance provides details like the method name, parameters, return type, and additional comments, making it easy to inspect and understand the functionality of each RPC method.

The following example demonstrates how to list the available RPC methods for two PLC symbols, fbValue and ipValue:

Console.WriteLine("RPC Methods for fbValue:");
foreach (IRpcMethod method in MAIN.fbValue.RpcMethods)
{
    Console.WriteLine(method.Name);
}

Console.WriteLine("\nRPC Methods for ipValue:");
foreach (IRpcMethod method in MAIN.ipValue.RpcMethods)
{
    Console.WriteLine(method.Name);
}

Inspecting RPC Method Parameters

Each RPC method may have parameters, which you can inspect using the Parameters property of an IRpcMethod instance. This property provides a collection of IRpcMethodParameter instances, where each instance represents a specific parameter with details like its name and type.

For example, to view the parameters of the Sum method on ipValue, use the following code:

Console.WriteLine("\nParameters for the 'Sum' method on ipValue:");
foreach (IRpcMethodParameter param in MAIN.ipValue.RpcMethods["Sum"].Parameters)
{
    Console.WriteLine($"Parameter Name: {param.Name}, Type: {param.TypeName}");
}

In this example, the Parameters property allows you to access and display each parameter’s name and type, which is useful for understanding what inputs the RPC method expects. This step ensures that you supply the correct parameter types and values when you invoke the method.

Invoking RPC Methods

The ADS .NET library provides two approaches to call an RPC method:

  1. Using the InvokeRpcMethod method — This method provides flexibility, especially when handling output parameters, and ensures compatibility with the RPC’s structure.
  2. Direct method invocation syntax — For conciseness, if you don’t require handling output parameters.

Using InvokeRpcMethod

To call an RPC method using InvokeRpcMethod, provide the name of the RPC method, an array containing input parameters, and an optional array for output parameters. This method is suitable when you need precise control over both input and output parameters.

object[] inParams = { 1, 2 };
object[] outParams;

double result = MAIN.ipValue.InvokeRpcMethod("Sum", inParams, out outParams);

Console.WriteLine($"Sum result: {result}");
Console.WriteLine($"Output message: {outParams[0]}");

Using Direct Method Invocation Syntax

For a more concise call, you can use direct method invocation syntax. This approach simplifies the code, although it may not populate output parameters as expected.

string message;
double result = MAIN.ipValue.Sum(1, 2, out message);

Console.WriteLine($"Sum result: {result}");
Console.WriteLine($"Output message: {message}");

Note: When using direct invocation syntax, output parameters might not be populated. This may be a limitation in the ADS .NET library, so consider reaching out to Beckhoff Technical Support for clarification. If output parameter values are essential, it’s recommended to use InvokeRpcMethod instead for reliable handling of output data.

Conclusion

As you continue to explore and work with TwinCAT ADS, remember that Beckhoff's extensive documentation and community resources are valuable allies in expanding your knowledge and troubleshooting challenges. The ADS ecosystem is continually evolving, with regular updates introducing new features and addressing limitations. Staying informed about these developments will ensure your applications remain at the forefront of industrial automation.

For questions, ideas, or further discussion, feel free to use the discussions section of the GitHub repository: GitHub Discussions

If you’d like to contribute improvements to this book, please submit a pull request.

If you encounter any issues, please raise a GitHub issue, and we’ll address it as soon as possible.

Here are additional resources for further assistance:

I hope this guide has helped you get started with TwinCAT ADS in the .NET environment, and I wish you success in your future automation projects.

Contributors

Here is a list of the contributors who have helped improving this guide. Big shout-out to them!

If you feel you're missing from this list, feel free to add yourself in a PR.