/* ======================================================================== * 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; using System.Collections.Generic; using Opc.Ua; using Opc.Ua.Generators; using Opc.Ua.Server; using Opc.Ua.Server.StateMachines; namespace Generators { /// /// Publishes the operating state machine and the generator-set methods. /// public partial class GeneratorNodeManager { /// /// Attaches the operating state machine and the methods to one set. /// /// The generator set. /// The set's simulation. private void AttachStateMachine(GeneratorSetState set, GeneratorSimulation simulation) { GeneratorStateMachineState? machine = set.OperatingState; if (machine == null) { return; } // The state machine already exists - it is mandatory on the type - so // this is lifecycle mode: attach behaviour rather than define states. StateMachineBuilder.For(machine, SystemContext) .WithInitialState(GeneratorStateMachineTypeIds.StateNumbers.Off); PublishState(machine, GeneratorRunState.Off); PublishOperatingMode(set, GeneratorOperatingModeEnum.Auto); // The simulation decides; the address space follows. simulation.StateChanged = (from, to) => { if (GeneratorStateMap.TransitionNumbers.TryGetValue((from, to), out uint transition)) { machine.DoTransition(SystemContext, transition, 0, default, []); } PublishState(machine, to); // Shutdown alarms latch, so something has to release them. Leaving a // shutdown state is that moment, whether the operator got there with // ResetFaults or the set recovered on its own - otherwise an // unattended plant accumulates alarms that are annunciated forever on // machines that are running again. if (to == GeneratorRunState.Off && from is GeneratorRunState.Fault or GeneratorRunState.EmergencyStopped) { ClearLatchedAlarms(set); } m_logger.GeneratorStateChanged(set.NodeId, to); }; AttachMethods(set, simulation); } /// /// Writes the current state onto the state machine's CurrentState variable. /// /// The set's state machine. /// The state now current. private void PublishState(GeneratorStateMachineState machine, GeneratorRunState state) { if (machine.CurrentState == null || !GeneratorStateMap.StateIds.TryGetValue(state, out uint stateNodeId)) { return; } // CurrentState carries the human-readable name; its Id property carries // the state NODE the model declares (not the state number — that is the // StateNumber property's value), so a client can browse what it reads. machine.CurrentState.Value = new LocalizedText(state.ToString()); PropertyState? id = machine.CurrentState.CreateOrReplaceId(SystemContext, null!); if (id != null) { id.Value = NodeId.Create( stateNodeId, Opc.Ua.Generators.Namespaces.Generators, Server.NamespaceUris); id.ClearChangeMasks(SystemContext, false); } machine.CurrentState.ClearChangeMasks(SystemContext, false); } /// /// Writes the control-panel selector position. /// /// The generator set. /// The mode to publish. private void PublishOperatingMode(GeneratorSetState set, GeneratorOperatingModeEnum mode) { if (set.OperatingMode == null) { return; } set.OperatingMode.Value = mode; set.OperatingMode.ClearChangeMasks(SystemContext, false); } /// /// Wires the six generator-set methods to the simulation. /// /// The generator set. /// The set's simulation. /// /// Legality lives in the simulation rather than in each handler, so a caller /// cannot drive a machine from Off straight to Loaded by picking the right /// method. A refused request answers BadInvalidState instead of /// silently doing nothing. /// private void AttachMethods(GeneratorSetState set, GeneratorSimulation simulation) { Bind(set.Start, (args, outs) => GeneratorCommands.Start(simulation)); Bind(set.Stop, (args, outs) => GeneratorCommands.Stop(simulation)); Bind(set.EmergencyStop, (args, outs) => GeneratorCommands.EmergencyStop(simulation)); Bind(set.ResetFaults, (args, outs) => { ClearLatchedAlarms(set); return GeneratorCommands.ResetFaults(simulation); }); Bind(set.StartTest, (args, outs) => { PublishOperatingMode(set, GeneratorOperatingModeEnum.Test); return GeneratorCommands.StartTest(simulation); }); Bind(set.SetOperatingMode, (args, outs) => { if (!GeneratorCommands.TryReadOperatingMode( args, out GeneratorOperatingModeEnum mode)) { return false; } PublishOperatingMode(set, mode); return true; }); } /// /// Attaches a call handler that reports a refused request as /// BadInvalidState. /// /// The method to bind, when present. /// Returns whether the request was accepted. /// /// Every handler runs under , so a command and /// the simulation tick cannot transition the same set at once. Taking the /// gate here rather than in each handler means a handler added later cannot /// forget it. /// private void Bind( MethodState? method, Func, List, bool> handler) { if (method == null) { return; } method.OnCallMethod2 = (ctx, m, objectId, inputs, outputs) => { bool accepted; lock (m_simulationGate) { accepted = handler(inputs, outputs); } return accepted ? ServiceResult.Good : StatusCodes.BadInvalidState; }; } } }