.NET Based OPC UA Client/Server SDK  3.1.0.500
Program Example

The Program example is a basic implementation of an OPC UA Program that counts a value down from 1000 to 0. Please read the short introduction to Programs if you’re not familiar with the concept.

The Demo Server address space contains the type CountdownStateMachineType having the child Value. This custom ProgramType is a subtype of the ProgramStateMachineType. An instance of this type can be found in the folder Demo → 016_StateMachines → Program. The sample code can be found in the file DemoNodeManager.StateMachines.cs.

Enumeration for States

For internal processing of the ProgramStateMachine we create an enumeration containing the possible states.

internal enum ProgramState
{
Ready = 1,
Running = 2,
Suspended = 3,
Halted = 4
}

The generated class CountdownStateMachineModel is extended with this enumeration.

public partial class CountdownStateMachineModel
{
internal ProgramState State
{
get
{
return m_state;
}
set
{
m_state = value;
ProgramState m_state;

Implement Methods

There are several transitions defined for the Program Finite State Machine. We define methods for each transition. In these methods the current state and the last transition is set.

The business logic is implemented here as well: e.g. in ReadyToRunning a timer is started that counts the the variable Value down. In addition, the TransitionEvents are fired.

To access all data that is needed to fire the event fields, we extend the generated class CountdownStateMachineModel.

  • We need a reference to DemoNodeManager to call methods implemented by the NodeManager.
  • ProgramTransitionEventModel is needed to store and set the event data.
  • ProgramId and ProgramName are needed to fire events.
private Timer Timer { get; set; }
internal DemoNodeManager NodeManager { get; set; }
internal ProgramTransitionEventModel Event { get; set; }
internal NodeId ProgramId { get; set; }
internal string ProgramName { get; set; }
void ReadyToRunning(Model.CountdownStateMachineModel model)
{
model.LastTransition.Value = new LocalizedText("ReadyToRunning");
model.LastTransition.Id = ObjectIds.ProgramStateMachineType_ReadyToRunning;
model.LastTransition.Number = (uint) ProgramTransitions.ReadyToRunning;
model.LastTransition.TransitionTime = DateTime.UtcNow;
model.State = Model.ProgramState.Running;
model.StartTimer();
FireEvent(model);
void FireEvent(Model.CountdownStateMachineModel model)
{
model.Event.Transition.Id = model.LastTransition.Id;
model.Event.Transition.Value = model.LastTransition.Value;
model.Event.IntermediateResult = model.Value;
model.Event.Time = DateTime.UtcNow;
GenericEvent e = model.Event.CreateEvent(Server.FilterManager, true);
ReportEvent(new NodeId(UnifiedAutomation.Demo.Model.Objects.Demo_StateMachines_Program, DefaultNamespaceIndex), e);
}
internal void StartTimer()
{
Timer = new Timer(OnTimer, null, 100, 100);
}
private void OnTimer(object state)
{
Value--;

The transition methods will be called by the OPC UA Method implementation. When an OPC UA Method is called, we need to check if it is possible to call the Method, since not all Methods can be called for all states of the program. If the Method can be called by the client, the transition method is called in the OPC UA Method implementation.

The methods of the ProgramState are implemented by the DemoNodeManager. So DemoNodeManager needs to implement the Interface IProgramStateMachineMethods.

internal partial class DemoNodeManager : Model.ICountdownStateMachineMethods
public StatusCode Start(RequestContext context, Model.CountdownStateMachineModel model)
{
var countDown = model as Model.CountdownStateMachineModel;
if (!CanStart(countDown))
{
return StatusCodes.BadInvalidState;
}
countDown.Event.SourceName = "Start";
countDown.Event.SourceNode = countDown.StartId;
ReadyToRunning(countDown);
return StatusCodes.Good;
}

LinkModelToNode

After implementing the methods we need to link the CountdownStateMachineModel to the nodes in the address space.

NodeId programId = new NodeId(Demo.Model.Objects.Demo_StateMachines_Program, DefaultNamespaceIndex);
var program = new Demo.Model.CountdownStateMachineModel()
{
Value = 1000,
CountdownStateMachineMethods = this,
NodeManager = this,
Event = new ProgramTransitionEventModel(),
ProgramId = programId,
ProgramName = "Program"
};
program.State = Demo.Model.ProgramState.Ready;
program.Event.Severity = (ushort) EventSeverity.Medium;
program.Event.EventType = ObjectTypeIds.TransitionEventType;
program.Event.Message = new LocalizedText("Transition");
program.GetMethodIds();
LinkModelToNode(
programId,
program,
null,
null,
0);

Set Executable Attribute

Depending on the current state of the Program Finite State Machine, some Methods cannot be called by the client. So we set the Executable attribute to false if a Method cannot be called by a client. We add the NodeIds of the Methods as property to the class CountdownStateMachineModel and use the ServerInternalClient to get them.

The transition methods are setting the Executable attributes.

public NodeId HaltId { get; private set; }
public NodeId ResetId { get; private set; }
public NodeId ResumeId { get; private set; }
public NodeId StartId { get; private set; }
public NodeId SuspendId { get; private set; }
public void GetMethodIds()
{
var result = new BrowsePathResult();
NodeManager.Server.InternalClient.Translate(
NodeManager.Server.DefaultRequestContext,
ProgramId,
0,
result);
StartId = (NodeId) result.Targets[0].TargetId;
SetExecutable(program.ResetId, false);
SetExecutable(program.ResumeId, false);
SetExecutable(program.SuspendId, false);
void SetExecutable(NodeId nodeId, bool value)
{
Server.InternalClient.WriteAttribute(
Server.DefaultRequestContext,
nodeId,
Attributes.Executable,
new Variant(value));
}

Test with UaExpert

Finally, we can test the implementation with a client, e.g. UaExpert.

First we drag and drop the variables Value, CurrentState, and LastTransition to the DA View (see screenshot below). The variable CurrentState has the value Ready, because the program hasn’t been started yet. Thus, LastTransition is empty, because no transition has taken place. The value of Value is 1000, because the countdown hasn’t started yet.

demoserver_program_1.png
DA View before starting the program

Then choose DocumentAdd… from the menu. Select Event View from the drop-down list and confirm with OK. Drag and drop the Program object to the event view (see screenshot). We are now ready to receive transition events.

demoserver_program_2.png
Event View containing the program object

Switch back to the DA View and call the method Start. The value of CurrentState has changed to running. LastTransition has the value ReadyToRunning and the value of Value is decreasing. When switching to the Event View, we can see the transition event in the list of events.

demoserver_program_3.png
DA View containing the changed variable values and Event View with transition event

Switch back to the DA View and call the method Suspend. The countdown stops and the values of CurrentState and LastTransition change accordingly. When calling the method Suspend again, you will get an error, because the method cannot be called in the suspended state.

Call the method Resume and wait for the program to finish if you like.