Please ensure Javascript is enabled for purposes of website accessibility
Powered by Zoomin Software. For more details please contactZoomin

AVEVA Pipeline Operations for Liquids

Create a class that implements the interface

  • Last UpdatedApr 24, 2025
  • 8 minute read

You can create multiple plugins, each one corresponding to their own class that implements the interface. The class must be public so it can be discovered and used externally.

When working with multiple tank variants, there are two implementation strategies for creating plugins:

  • Create multiple plugins, one for each tank variant โ€” Whether a tank variant represents completely different geometries, such as a sphere or upright cylinder, or smaller variants such as cylinder variations, a plugin is created for each individual variant. Individual tanks are then configured to use their corresponding plugin. A benefit to this approach is that the code for each plugin is truly isolated, and the control flow of deciding which variant to use is taken care of in the baseline code.

  • Create a single plugin for all tank variants โ€” A single plugin is created that internally switches between the different tank variants. All tanks can be configured to use this single plugin. A benefit to this approach is that reduced configuration is needed. There is a risk that the control flow in deciding which variant to use within the plugin may need to be determined in multiple locations. That decision making process inside the plugin may also have performance implications if it is more computationally expensive than switching on a single enum value.

For a sample implementation, refer to the following ExamplePlugin class.

Sample: ExamplePlugin Class

using OASySDNA.LMS.Tank.Conversions.LevelAndVolumes;

using System;

namespace OASySDNA.LMS.SDKTests.RealTime.Tanking.LevelAndVolumePluginExample

{

/// <summary>

/// To create a level and volume plugin, create a public class that

/// implements <see cref="ILevelAndVolumePlugin"/>.

/// Similar to tank strapping, the input and output units for the

/// level and volume are configured on the tank and its unit profile.

/// </summary>

public sealed class ExamplePlugin : ILevelAndVolumePlugin

{

/// <summary>

/// In order for this plugin to be instantiated, it is required that

/// its default (parameterless) constructor is public. Explicitly

/// creating a public default constructor is not needed if you do not

/// define any constructors at all, as one will be implicitly created.

/// </summary>

public ExamplePlugin()

{

}

/// <summary>

/// If you decide to create another constructor (for unit testing for

/// example), ensure it is not public. There should be exactly 1 public

/// constructor (the parameterless one).

/// </summary>

internal ExamplePlugin(int _)

{

}

public Volume ConvertLevelToVolume<TArgs, TTank>(in TArgs args)

where TArgs : IConvertLevelToVolumeArgs<TTank>

where TTank : IReadOnlyTank

{

TTank tank = args.Tank;

// You can read arbitrary tank fields using ReadField.

// The field name must match the name in RealTime exactly,

// case-sensitive.

// The tank record already has a read-lock on it (at minimum), so

// no additional locking is required.

int numerator = tank.ReadField<int>("ATNumerator");

int denominator = tank.ReadField<int>("ATDenominator");

// While any preconditions to ensure that the tank is in a valid

// state should be determined in the GetState method by returning

// State.IsGood or State.IsBad, you can safely throw

// exceptions in any of these methods without crashing any

// processes.

if (numerator == 3)

{

// This will result in the value returned to equal 0 (so in

// this case, volume will be 0) and the exception will be

// logged along with identifying information for the specified

// tank. The same behavior occurs for throwing exceptions in

// every other method here.

// If this is not the desired behavior, then consider doing

// your validation in the GetState method instead of here.

throw new ArgumentOutOfRangeException(

nameof(numerator),

"ATNumerator cannot be 3");

}

// The args provide the level that need to converted to

// volume.

// The units of the level/increments are configured on the tank in

// the same manner as tank strapping, which is through the tank's

// strapping fraction.

// If the remote type of the tank is configured as Level

// (increments) or Level (gray code), then the value from the

// source is interpreted as increments and is directly provided

// here.

// If the remote type of the tank is configured as Level

// (engineering units), then the value from the source is

// interpreted as level and is converted to increments based on the

// strapping fraction, and then provided here.

int increments = args.Increments;

// The output volume unit is configured on the tank in the same

// manner as tank strapping, which is through the tank's unit

// profile.

double volume = increments * ((double)numerator / denominator);

// The volume value to return is wrapped in a Volume instance.

// The unit profile of the tank determines the unit of volume.

return Volume.FromValue(volume);

}

public Level ConvertVolumeToLevel<TArgs, TTank>(in TArgs args)

where TArgs : IConvertVolumeToLevelArgs<TTank>

where TTank : IReadOnlyTank

{

TTank tank = args.Tank;

// The args provide the volume that need to converted to level.

// The units of the volume are configured on the tank in the

// same manner as tank strapping.

double volume = args.Volume;

// The type must match and the field name is case-sensitive.

// If there is a mistake, then an exception is thrown.

int numerator = tank.ReadField<int>("ATNumerator");

int denominator = tank.ReadField<int>("ATDenominator");

// The output level unit is configured on the tank in the same

// manner as tank strapping.

int increments = (int)(volume * ((double)denominator / numerator));

// The level value to return, which is represented in terms of

// number of increments, is wrapped in a Level instance.

// The strapping fraction of the tank determines the unit of each

// increment, as well as how many increments equal to 1 level.

return Level.FromIncrements(increments);

}

public Level GetMaximumLevel<TArgs, TTank>(in TArgs args)

where TArgs : IGetMaximumLevelArgs<TTank>

where TTank : IReadOnlyTank

{

// Gets the maximum level of the specified tank.

// The tank in question is provided for you in the args in case the

// maximum level is derived from some fields of the tank, custom or

// otherwise.

TTank tank = args.Tank;

int maximumIncrements = tank.ReadField<int>("ATMaxIncrements");

// It is possible that the maximum level is derived soley from the

// plugin (from a formula, or some other source), and that you will

// not need to read anything from the tank. In this case, it is the

// administrator's responsibility to ensure that all tanks that use

// this particular plugin abide by the plugin's maximum level.

// The level value to return, which is represented in terms of

// number of increments, is wrapped in a Level instance.

// The strapping fraction of the tank determines the unit of each

// increment, as well as how many increments equal to 1 level.

return Level.FromIncrements(maximumIncrements);

}

public Volume GetMaximumVolume<TArgs, TTank>(in TArgs args)

where TArgs : IGetMaximumVolumeArgs<TTank>

where TTank : IReadOnlyTank

{

// Gets the maximum volume of the specified tank.

// The tank in question is provided for you in the args in case the

// maximum volume is derived from some fields of the tank, custom or

// otherwise.

TTank tank = args.Tank;

double maximumVolume = tank.ReadField<double>("ATMaxVolume");

// It is possible that the maximum volume is derived soley from the

// plugin (from a formula, or some other source), and that you will

// not need to read anything from the tank. In this case, it is the

// administrator's responsibility to ensure that all tanks that use

// this particular plugin abide by the plugin's maximum volume.

// The volume value to return is wrapped in a Volume instance.

// The unit profile of the tank determines the unit of volume.

return Volume.FromValue(maximumVolume);

}

public State GetState<TArgs, TTank>(in TArgs args)

where TArgs : IGetStateArgs<TTank>

where TTank : IReadOnlyTank

{

// Each time we call any other method in this plugin, we

// immediately call this GetState method to check that the tank is

// in a state where we can validly perform these calculations.

// This is the place to validate the tank's state or other state of

// the plugin to ensure that the calculations can be performed

// without error.

TTank tank = args.Tank;

int denominator = tank.ReadField<int>("ATDenominator");

if (denominator <= 0)

{

// Returning State.IsBad(String) indicates the tank is not in a

// valid state to perform any level or volume calculations. The

// failure reason is logged and the tank is set into error

// state. Specifying the identity of the tank (such as its

// name) is not necessary and will automatically be included in

// the log. An alarm will be raised for the specified tank and

// will persist until the state is fixed such that this method

// returns State.IsGood.

return State.IsBad(

$"ATDenominator value of {denominator} must be positive");

}

int numerator = tank.ReadField<int>("ATNumerator");

if (numerator <= 0)

{

return State.IsBad(

$"ATNumerator value of {numerator} must be positive");

}

// Return State.IsGood to indicate that the tank and plugin is in a

// valid state to perform level and volume conversions from the

// other plugin methods.

return State.IsGood;

}

/// <summary>

/// Example of a helper method.

/// </summary>

/// <remarks>

/// Notice how the <see langword="in"/> keyword is added to the

/// <paramref name="args"/> parameter to ensure any arguments provided

/// are passed in by reference. This will prevent any defensive copies

/// from being created if <paramref name="args"/> is a

/// <see langword="struct"/>.

///

/// Also notice how the type of the <paramref name="args"/> parameter

/// is <typeparamref name="TArgs"/> and not

/// <see cref="IConvertVolumeToLevelArgs{TTank}"/>. This ensures that

/// if the <paramref name="args"/> provided is a

/// <see langword="struct"/>, it does not get boxed into an

/// <see cref="IConvertVolumeToLevelArgs{TTank}"/> object allocated

/// on the heap, and later garbage-collected.

///

/// Depending on the frequency that this plugin is called, these

/// optimizations may have a meaningful performance impact.

///

/// If you decide to use some helper methods that take in the args

/// or the tank, consider adding the "in" keyword in the helper

/// method signature. This will ensure the args (or tank) is passed

/// by reference, which you can safely do since the args is read-only.

/// There is no guarantee that args is an object allocated on the heap.

/// It may be a struct allocated on the stack, so passing it by reference

/// will prevent defensive copies from being created.

///

/// Including the "in" keyword at the call-site is optional.

/// Including "in" in the method signature is the important part.

///

/// // An example of the "in" keyword not added at the call-site.

/// GetRecordNumberFromArgs(args);

///

/// // An example of the "in" keyword added at the call-site.

/// // It makes no difference.

/// GetRecordNumberFromArgs(in args);

///

/// Alternatively, you can avoid passing the <paramref name="args"/>

/// between methods and instead pass the values derived from the args'

/// properties.

/// </remarks>

private static string GetNameFromArgs<TArgs, TTank>(in TArgs args)

where TArgs : IConvertVolumeToLevelArgs<TTank>

where TTank : IReadOnlyTank

{

return args.Tank.ReadField<string>("name");

}

/// <summary>

/// Another example of a helper method that passes in the tank directly.

/// </summary>

/// <remarks>

/// Notice how the <see langword="in"/> keyword is added to the

/// <paramref name="tank"/> parameter to ensure any arguments provided

/// are passed in by reference. This will prevent any defensive copies

/// from being created if <paramref name="tank"/> is a

/// <see langword="struct"/>.

///

/// Also notice how the type of the <paramref name="tank"/> parameter

/// is <typeparamref name="TTank"/> and not

/// <see cref="IReadOnlyTank"/>. This ensures that

/// if the <paramref name="tank"/> provided is a

/// <see langword="struct"/>, it does not get boxed into an

/// <see cref="IReadOnlyTank"/> object allocated

/// on the heap, and later garbage-collected.

///

/// Depending on the frequency that this plugin is called, these

/// optimizations may have a meaningful performance impact.

///

/// Alternatively, you can avoid passing the <paramref name="tank"/>

/// between methods and instead pass the values derived from reading

/// the tank's fields.

/// </remarks>

private static string GetNameFromTank<TTank>(in TTank tank)

where TTank : IReadOnlyTank

{

return tank.ReadField<string>("name");

}

}

}