.NET Based OPC UA Client/Server SDK  3.3.1.531
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. There the DemoNodeManager class is extended to connect the program with the nodeset. Furthermore, the file contains the manual written part of the CountdownStateMachineModel class implementing the business logic of the program. The other part was generated by the UaModeler based on the model and can be found in the ModelClasses.cs file.

Implement the Business Logic

The CountdownStateMachineModel is derived from the ProgramStateMachineModel. The ProgramStateMachineModel already implements the whole state machine logic, including the Enumerations for the states and for the transitions and the method SwitchToState to switch to another state.

When the state machine switch to another state a .NET event is raised. We use this event to start and stop our timer:

public CountdownStateMachineModel(int startValue)
{
Value = startValue;
Switched += (o, e) =>
{
switch (e.OldState)
{
case State.Running:
case State.Suspended:
StopTimer();
break;
}
switch (e.NewState)
{
case State.Ready:
Value = startValue;
RecycleCount++;
break;
case State.Running:
StartTimer();
break;
}
};
}

Once the timer is running the OnTimer method will be called cyclically and decrement the Value until the value zero is reached. The state machine will then be switched to the Halted state.

private void OnTimer(object state)
{
Value--;
if (Value == 0)
{
Timer.Dispose();
Timer = null;
SwitchToState(State.Halted);
}

A .NET progress event will be raised on every hundredth tick. We will later transform this to an OPC UA event.

if (Value % 100 == 0 && Value > 0)
{
RaiseProgressChanged();
}
}

Implement Methods

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

public partial class CountdownStateMachineModel : Model.ICountdownStateMachineMethods
{

The method implementations basically call the SwitchToState() method if the preceding current state validation was positive:

public StatusCode Start(RequestContext context, CountdownStateMachineModel model)
{
if (InternalState != State.Ready)
{
return StatusCodes.BadInvalidState;
}
SwitchToState(State.Running);
return StatusCodes.Good;
}

LinkModelToNode

After implementing the business logic and the methods we need to link the CountdownStateMachineModel to the nodes in the address space. This is done in the SetupProgramStateMachine() method, which is called during startup of the server.

private void SetupProgramStateMachine()
{
NodeId programId = new NodeId(Demo.Model.Objects.Demo_StateMachines_Program, DefaultNamespaceIndex);
string programName = "Program";
var program = new Demo.Model.CountdownStateMachineModel(1000);
program.SwitchToState(ProgramStateMachineModel.State.Ready);
LinkModelToNode(
programId,
program,
null,
null,
0);

This will take care that data access to the model nodes will be redirected to our model class. In addition it will call the previously implemented methods on client's call requests.

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 DemoNodeManager.

StartId = GetMethodId(programId, new QualifiedName(BrowseNames.Start));
SuspendId = GetMethodId(programId, new QualifiedName(BrowseNames.Suspend));
HaltId = GetMethodId(programId, new QualifiedName(BrowseNames.Halt));
ResetId = GetMethodId(programId, new QualifiedName(BrowseNames.Reset));
ResumeId = GetMethodId(programId, new QualifiedName(BrowseNames.Resume));

We use the ServerInternalClient to get the method ids:

private NodeId GetMethodId(NodeId programId, QualifiedName methodName)
{
var result = new BrowsePathResult();
Server.InternalClient.Translate(
Server.DefaultRequestContext,
programId,
new RelativePath(methodName),
0,
result);
return (NodeId)result.Targets[0].TargetId;
}

Whenever the state machine transitions to a new state, the executable attributes are updated.

switch (e.Transition)
{
case ProgramStateMachineModel.Transition.HaltedToReady:
SetExecutable(ResetId, false);
SetExecutable(StartId, true);
break;
case ProgramStateMachineModel.Transition.ReadyToHalted:
SetExecutable(HaltId, false);
SetExecutable(ResetId, true);
break;

We utilize here again the ServerInternalClient, this time to write the Executable attribute.

void SetExecutable(NodeId nodeId, bool value)
{
Server.InternalClient.WriteAttribute(
Server.DefaultRequestContext,
nodeId,
Attributes.Executable,
new Variant(value));
}

Fire Events

This example demonstrates two different ways to generate OPC UA events by means of the ProgramTransitionEventType and the ProgressEventType.

For the transition event we subscribe to the model's Switched event. There an instance of the (generated) ProgramTransitionEventModel class is created and the relevant properties are set.

program.Switched += (o, e) =>
{
var ev = new ProgramTransitionEventModel()
{
Time = DateTime.UtcNow,
IntermediateResult = program.Value,
SourceNode = programId,
SourceName = programName,
Severity = (ushort) EventSeverity.Medium,
EventType = ObjectTypeIds.ProgramTransitionEventType,
Message = new LocalizedText("Transition"),
};

The model class, like all generated event classes, has one method to generated a generic event based on the model. This generic event can then be reported to the NodeManager.

GenericEvent ge = ev.CreateEvent(Server.FilterManager, true);
ReportEvent(programId, ge);

For the progress event we subscribe to the models ProgressChanged. Here the GenericEvent is generated directly and the relevant properties are set with the Set() method.

program.ProgressChanged += (o, e) =>
{
GenericEvent ev = new GenericEvent(Server.FilterManager);
ev.Initialize(
null,
ObjectTypeIds.ProgressEventType,
programId,
programName,
EventSeverity.Medium,
new LocalizedText("Progress"));
ev.Set(BrowseNames.Context, programId);
ev.Set(BrowseNames.Progress, (ushort)e.ProgressPercentage);
ReportEvent(programId, ev);
};

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.