/* ======================================================================== * 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; } } }