/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
namespace Opc.Ua.Server.StateMachines
{
///
/// A generic concrete whose
/// state, transition and cause tables are sourced from a mutable
/// definition holder managed by the unified
/// . Construct via
/// :
///
///
///
/// FluentFiniteStateMachineState sm = StateMachineBuilder
/// .Create(parent, context, nodeId, browseName)
/// .AddState(1, "Off", isInitial: true)
/// .AddState(2, "On")
/// .AddTransition(10, "OffToOn", from: 1, to: 2)
/// .OnCause(causeId: 100, from: 1, transition: 10)
/// .WithInitialState(1)
/// .StateMachine;
///
///
/// Vendors who need additional properties or methods on top of the
/// standard finite state-machine surface can subclass
/// and use the
/// (protected) property to feed
/// the same builder-driven tables.
///
///
public class FluentFiniteStateMachineState : FiniteStateMachineState
{
///
/// Cached projections of the mutable holder. Invalidated by
/// version counter.
///
private ElementInfo[]? m_stateTable;
private ElementInfo[]? m_transitionTable;
private uint[,]? m_transitionMappings;
private uint[,]? m_causeMappings;
private int m_cacheVersion = -1;
///
/// When true, the state machine rejects all incoming
/// transitions and cause invocations with
/// . Used to model
/// the inactive state of a sub-state-machine whose parent
/// has exited the attached state. Set by
///
/// lifecycle hooks.
///
public bool IsSuspended { get; set; }
///
public override bool IsCausePermitted(
ISystemContext context,
uint causeId,
bool checkUserAccessRights)
{
if (IsSuspended)
{
return false;
}
return base.IsCausePermitted(context, causeId, checkUserAccessRights);
}
///
public override ServiceResult DoCause(
ISystemContext context,
MethodState causeMethod,
uint causeId,
ArrayOf inputArguments,
System.Collections.Generic.List outputArguments)
{
if (IsSuspended)
{
return StatusCodes.BadInvalidState;
}
return base.DoCause(context, causeMethod, causeId, inputArguments, outputArguments);
}
///
public override ServiceResult DoTransition(
ISystemContext context,
uint transitionId,
uint causeId,
ArrayOf inputArguments,
System.Collections.Generic.List outputArguments)
{
if (IsSuspended)
{
return StatusCodes.BadInvalidState;
}
return base.DoTransition(context, transitionId, causeId, inputArguments, outputArguments);
}
///
/// Initializes a new state machine instance from the given
/// immutable definition. This overload exists for callers that
/// construct definitions directly; the recommended path is the
/// unified .
///
/// The parent node (may be null).
/// The fluent definition snapshot.
///
/// is null.
///
public FluentFiniteStateMachineState(
NodeState parent,
StateMachineDefinition definition)
: this(parent, FromSnapshot(definition), useHolder: true)
{
}
///
/// Internal factory used by the unified builder so it can mutate
/// the definition holder incrementally.
///
internal static FluentFiniteStateMachineState CreateWithHolder(
NodeState parent,
MutableStateMachineDefinition holder)
{
return new FluentFiniteStateMachineState(parent, holder, useHolder: true);
}
private FluentFiniteStateMachineState(
NodeState parent,
MutableStateMachineDefinition holder,
bool useHolder)
: base(parent)
{
_ = useHolder;
MutableDefinition = holder ?? throw new ArgumentNullException(nameof(holder));
}
///
/// The mutable definition holder backing this state machine.
/// Visible to assembly-internal callers (the unified
/// ) so they can
/// populate the definition incrementally.
///
internal MutableStateMachineDefinition MutableDefinition { get; }
///
/// The immutable definition snapshot this state machine reads
/// from. Allocates a fresh snapshot on each access — use
/// sparingly (for introspection / testing).
///
public StateMachineDefinition Definition => MutableDefinition.Snapshot();
///
protected override string ElementNamespaceUri
=> MutableDefinition.ElementNamespaceUri;
///
protected override ElementInfo[]? StateTable
{
get
{
RefreshCache();
return m_stateTable;
}
}
///
protected override ElementInfo[]? TransitionTable
{
get
{
RefreshCache();
return m_transitionTable;
}
}
///
protected override uint[,]? TransitionMappings
{
get
{
RefreshCache();
return m_transitionMappings;
}
}
///
protected override uint[,]? CauseMappings
{
get
{
RefreshCache();
return m_causeMappings;
}
}
private void RefreshCache()
{
int currentVersion = MutableDefinition.Version;
if (m_cacheVersion == currentVersion)
{
return;
}
int stateCount = MutableDefinition.States.Count;
var stateTable = new ElementInfo[stateCount];
for (int i = 0; i < stateCount; i++)
{
StateMachineStateDefinition s = MutableDefinition.States[i];
stateTable[i] = new ElementInfo(s.Id, s.BrowseName, s.Id);
}
int transitionCount = MutableDefinition.Transitions.Count;
var transitionTable = new ElementInfo[transitionCount];
uint[,] transitionMappings = new uint[transitionCount, 4];
for (int i = 0; i < transitionCount; i++)
{
StateMachineTransitionDefinition t = MutableDefinition.Transitions[i];
transitionTable[i] = new ElementInfo(t.Id, t.BrowseName, t.Id);
transitionMappings[i, 0] = t.Id;
transitionMappings[i, 1] = t.FromStateId;
transitionMappings[i, 2] = t.ToStateId;
transitionMappings[i, 3] = t.HasEffect ? 1u : 0u;
}
int causeCount = MutableDefinition.CauseMappings.Count;
uint[,] causeMappings = new uint[causeCount, 3];
for (int i = 0; i < causeCount; i++)
{
StateMachineCauseMapping c = MutableDefinition.CauseMappings[i];
causeMappings[i, 0] = c.CauseId;
causeMappings[i, 1] = c.FromStateId;
causeMappings[i, 2] = c.TransitionId;
}
m_stateTable = stateTable;
m_transitionTable = transitionTable;
m_transitionMappings = transitionMappings;
m_causeMappings = causeMappings;
m_cacheVersion = currentVersion;
}
private static MutableStateMachineDefinition FromSnapshot(
StateMachineDefinition definition)
{
if (definition == null)
{
throw new ArgumentNullException(nameof(definition));
}
var holder = new MutableStateMachineDefinition
{
InitialStateId = definition.InitialStateId,
ElementNamespaceUri = definition.ElementNamespaceUri
};
holder.States.AddRange(definition.States);
holder.Transitions.AddRange(definition.Transitions);
holder.CauseMappings.AddRange(definition.CauseMappings);
holder.Version = 1;
holder.Frozen = true;
return holder;
}
}
}